1/* 2 Copyright (C) 2004-2005 SKYRIX Software AG 3 4 This file is part of OpenGroupware.org. 5 6 OGo is free software; you can redistribute it and/or modify it under 7 the terms of the GNU Lesser General Public License as published by the 8 Free Software Foundation; either version 2, or (at your option) any 9 later version. 10 11 OGo is distributed in the hope that it will be useful, but WITHOUT ANY 12 WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 14 License for more details. 15 16 You should have received a copy of the GNU Lesser General Public 17 License along with OGo; see the file COPYING. If not, write to the 18 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 19 02111-1307, USA. 20*/ 21 22#import <Foundation/NSArray.h> 23#import <Foundation/NSCalendarDate.h> 24#import <Foundation/NSDictionary.h> 25#import <Foundation/NSTimer.h> 26#import <Foundation/NSUserDefaults.h> 27#import <Foundation/NSValue.h> 28 29#import <NGExtensions/NSNull+misc.h> 30#import <NGExtensions/NSObject+Logs.h> 31 32#import <GDLAccess/EOAdaptor.h> 33#import <GDLAccess/EOAdaptorContext.h> 34 35#import "GCSChannelManager.h" 36#import "NSURL+GCS.h" 37#import "EOAdaptorChannel+GCS.h" 38 39/* 40 TODO: 41 - implemented pooling 42 - auto-close channels which are very old?! 43 (eg missing release due to an exception) 44*/ 45 46@interface GCSChannelHandle : NSObject 47{ 48@public 49 NSURL *url; 50 EOAdaptorChannel *channel; 51 NSDate *creationTime; 52 NSDate *lastReleaseTime; 53 NSDate *lastAcquireTime; 54} 55 56- (EOAdaptorChannel *) channel; 57- (BOOL) canHandleURL: (NSURL *) _url; 58- (NSTimeInterval) age; 59 60@end 61 62@implementation GCSChannelManager 63 64static BOOL debugOn = NO; 65static BOOL debugPools = NO; 66static int ChannelExpireAge = 180; 67static NSTimeInterval ChannelCollectionTimer = 5 * 60; 68 69+ (void) initialize 70{ 71 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; 72 73 debugOn = [ud boolForKey: @"GCSChannelManagerDebugEnabled"]; 74 debugPools = [ud boolForKey: @"GCSChannelManagerPoolDebugEnabled"]; 75 76 ChannelExpireAge = [[ud objectForKey: @"GCSChannelExpireAge"] intValue]; 77 if (ChannelExpireAge < 1) 78 ChannelExpireAge = 180; 79 80 ChannelCollectionTimer = 81 [[ud objectForKey: @"GCSChannelCollectionTimer"] intValue]; 82 if (ChannelCollectionTimer < 1) 83 ChannelCollectionTimer = 5*60; 84} 85 86+ (NSString *) adaptorNameForURLScheme: (NSString *) _scheme 87{ 88 // TODO: map scheme to adaptors (eg 'postgresql: //' to PostgreSQL 89 return @"PostgreSQL"; 90} 91 92+ (id) defaultChannelManager 93{ 94 static GCSChannelManager *cm = nil; 95 96 if (!cm) 97 cm = [self new]; 98 99 return cm; 100} 101 102- (id) init 103{ 104 if ((self = [super init])) 105 { 106 urlToAdaptor = [[NSMutableDictionary alloc] initWithCapacity: 4]; 107 lastFailures = [[NSMutableDictionary alloc] initWithCapacity: 4]; 108 availableChannels = [[NSMutableArray alloc] initWithCapacity: 16]; 109 busyChannels = [[NSMutableArray alloc] initWithCapacity: 16]; 110 111 gcTimer = [[NSTimer scheduledTimerWithTimeInterval: 112 ChannelCollectionTimer 113 target: self selector: @selector (_garbageCollect:) 114 userInfo: nil repeats: YES] retain]; 115 } 116 117 return self; 118} 119 120- (void) dealloc 121{ 122 if (gcTimer) 123 [gcTimer invalidate]; 124 125 [busyChannels release]; 126 [availableChannels release]; 127 [lastFailures release]; 128 [urlToAdaptor release]; 129 [super dealloc]; 130} 131 132/* adaptors */ 133 134- (NSDictionary *) connectionDictionaryForURL: (NSURL *) _url 135{ 136 NSMutableDictionary *md; 137 id tmp; 138 139 md = [NSMutableDictionary dictionaryWithCapacity: 4]; 140 141 if ((tmp = [_url host])) 142 [md setObject: tmp forKey: @"hostName"]; 143 if ((tmp = [_url port])) 144 [md setObject: tmp forKey: @"port"]; 145 if ((tmp = [_url user])) 146 [md setObject: tmp forKey: @"userName"]; 147 if ((tmp = [_url password])) 148 [md setObject: tmp forKey: @"password"]; 149 150 if ((tmp = [_url gcsDatabaseName])) 151 [md setObject: tmp forKey: @"databaseName"]; 152 153 [self debugWithFormat: @"build connection dictionary for URL %@: %@", 154 [_url absoluteString], md]; 155 156 return md; 157} 158 159- (EOAdaptor *) adaptorForURL: (NSURL *) _url 160{ 161 EOAdaptor *adaptor; 162 NSString *key; 163 NSString *adaptorName; 164 NSDictionary *condict; 165 166 adaptor = nil; 167 168 if (_url) 169 { 170 if ((key = [_url gcsURLId])) 171 { 172 adaptor = [urlToAdaptor objectForKey: key]; 173 if (adaptor) 174 [self debugWithFormat: @"using cached adaptor: %@", adaptor]; 175 else 176 { 177 [self debugWithFormat: @"creating new adaptor for URL: %@", _url]; 178 179 if ([EOAdaptor respondsToSelector: @selector (adaptorForURL:)]) 180 adaptor = [EOAdaptor adaptorForURL: _url]; 181 else 182 { 183 adaptorName = [[self class] 184 adaptorNameForURLScheme: [_url scheme]]; 185 if ([adaptorName length]) 186 { 187 condict = [self connectionDictionaryForURL: _url]; 188 189 adaptor = [EOAdaptor adaptorWithName: adaptorName]; 190 if (adaptor) 191 [adaptor setConnectionDictionary: condict]; 192 else 193 [self errorWithFormat: 194 @"did not find adaptor '%@' for URL: %@", 195 adaptorName, _url]; 196 } 197 else 198 [self errorWithFormat: @"cannot handle URL: %@", _url]; 199 } 200 201 [urlToAdaptor setObject: adaptor forKey: key]; 202 } 203 } 204 } 205 206 return adaptor; 207} 208 209/* channels */ 210 211- (GCSChannelHandle *) 212 findBusyChannelHandleForChannel: (EOAdaptorChannel *) _ch 213{ 214 NSEnumerator *e; 215 GCSChannelHandle *handle, *currentHandle; 216 217 handle = NULL; 218 219 e = [busyChannels objectEnumerator]; 220 while (!handle && (currentHandle = [e nextObject])) 221 if ([currentHandle channel] == _ch) 222 handle = currentHandle; 223 224 return handle; 225} 226 227- (GCSChannelHandle *) findAvailChannelHandleForURL: (NSURL *) _url 228{ 229 NSEnumerator *e; 230 GCSChannelHandle *handle, *currentHandle; 231 232 handle = nil; 233 234 e = [availableChannels objectEnumerator]; 235 while (!handle && (currentHandle = [e nextObject])) 236 if ([currentHandle canHandleURL: _url]) 237 handle = currentHandle; 238 else if (debugPools) 239 [self logWithFormat: @"DBPOOL: cannot use handle (%@ vs %@) ", 240 [_url absoluteString], [currentHandle->url absoluteString]]; 241 242 return handle; 243} 244 245- (EOAdaptorChannel *) _createChannelForURL: (NSURL *) _url 246{ 247 EOAdaptor *adaptor; 248 EOAdaptorContext *adContext; 249 EOAdaptorChannel *adChannel; 250 251 adChannel = nil; 252 253 adaptor = [self adaptorForURL: _url]; 254 if (adaptor) 255 { 256 adContext = [adaptor createAdaptorContext]; 257 if (adContext) 258 { 259 adChannel = [adContext createAdaptorChannel]; 260 if (!adChannel) 261 [self errorWithFormat: @"could not create adaptor channel!"]; 262 } 263 else 264 [self errorWithFormat: @"could not create adaptor context!"]; 265 } 266 267 return adChannel; 268} 269 270- (EOAdaptorChannel *) acquireOpenChannelForURL: (NSURL *) _url 271{ 272 // TODO: naive implementation, add pooling! 273 EOAdaptorChannel *channel; 274 GCSChannelHandle *handle; 275 NSCalendarDate *now, *lastFailure; 276 NSString *urlId, *url; 277 278 channel = nil; 279 urlId = [_url gcsURLId]; 280 281 now = [NSCalendarDate date]; 282 lastFailure = [lastFailures objectForKey: urlId]; 283 if ([[lastFailure dateByAddingYears: 0 months: 0 days: 0 284 hours: 0 minutes: 0 seconds: 5] 285 earlierDate: now] != now) 286 { 287 /* look for cached handles */ 288 289 handle = [self findAvailChannelHandleForURL: _url]; 290 if (handle) 291 { 292 // TODO: check age? 293 [busyChannels addObject: handle]; 294 [availableChannels removeObject: handle]; 295 ASSIGN (handle->lastAcquireTime, now); 296 297 channel = [handle channel]; 298 if (debugPools) 299 [self logWithFormat: @"DBPOOL: reused cached DB channel! (%p)", 300 channel]; 301 } 302 else 303 { 304 url = [NSString stringWithFormat: @"%@://%@%@", [_url scheme], [_url host], [_url path]]; 305 if (debugPools) 306 { 307 [self logWithFormat: @"DBPOOL: create new DB channel for %@", url]; 308 } 309 310 /* create channel */ 311 channel = [self _createChannelForURL: _url]; 312 if (channel) 313 { 314 if ([channel isOpen] 315 || [channel openChannel]) 316 { 317 /* create handle for channel */ 318 319 handle = [[GCSChannelHandle alloc] init]; 320 handle->url = [_url retain]; 321 handle->channel = [channel retain]; 322 handle->creationTime = [now retain]; 323 handle->lastAcquireTime = [now retain]; 324 325 [busyChannels addObject: handle]; 326 [handle release]; 327 328 if (lastFailure) 329 { 330 [self logWithFormat: @"db for %@ is now back up", url]; 331 [lastFailures removeObjectForKey: urlId]; 332 } 333 } 334 else 335 { 336 [self errorWithFormat: @"could not open channel %@ for %@", channel, url]; 337 channel = nil; 338 [lastFailures setObject: now forKey: urlId]; 339 [self warnWithFormat: @" will prevent opening of this" 340 @" channel 5 seconds after %@", now]; 341 } 342 } 343 } 344 } 345 346 return channel; 347} 348 349- (void) releaseChannel: (EOAdaptorChannel *) _channel 350{ 351 [self releaseChannel: _channel immediately: NO]; 352} 353 354- (void) releaseChannel: (EOAdaptorChannel *) _channel 355 immediately: (BOOL) _immediately 356{ 357 GCSChannelHandle *handle; 358 BOOL keepOpen; 359 360 handle = [self findBusyChannelHandleForChannel: _channel]; 361 if (handle) 362 { 363 [handle retain]; 364 365 ASSIGN (handle->lastReleaseTime, [NSCalendarDate date]); 366 [busyChannels removeObject: handle]; 367 368 keepOpen = NO; 369 if (!_immediately && [_channel isOpen] 370 && [handle age] < ChannelExpireAge) 371 { 372 keepOpen = YES; 373 // TODO: consider age 374 [availableChannels addObject: handle]; 375 if (debugPools) 376 [self logWithFormat: 377 @"DBPOOL: keeping channel (age %ds, #%d, %p) : %@", 378 (int) 379 [handle age], [availableChannels count], 380 [handle->url absoluteString], 381 _channel]; 382 } 383 else if (debugPools) 384 { 385 [self logWithFormat: 386 @"DBPOOL: freeing old channel (age %ds, %p) ", (int) 387 [handle age], _channel]; 388 } 389 if (!keepOpen && [_channel isOpen]) 390 [_channel closeChannel]; 391 [handle release]; 392 } 393 else 394 { 395 if ([_channel isOpen]) 396 [_channel closeChannel]; 397 398 [_channel release]; 399 } 400} 401 402- (void) releaseAllChannels 403{ 404 EOAdaptorChannel *channel; 405 GCSChannelHandle *handle; 406 NSEnumerator *e; 407 408 e = [busyChannels objectEnumerator]; 409 while ((handle = [e nextObject])) 410 { 411 [handle retain]; 412 ASSIGN (handle->lastReleaseTime, [NSCalendarDate date]); 413 [busyChannels removeObject: handle]; 414 channel = [handle channel]; 415 if (debugPools) 416 [self logWithFormat: @"releaseAllChannels: freeing old channel (age %ds, %p) ", (int)[handle age], channel]; 417 if ([channel isOpen]) 418 [channel closeChannel]; 419 [handle release]; 420 } 421} 422 423/* checking for tables */ 424 425- (BOOL) canConnect: (NSURL *) _url 426{ 427 /* 428 this can check for DB connect as well as for table URLs (whether a table 429 exists) 430 */ 431 EOAdaptorChannel *channel; 432 NSString *table; 433 BOOL result; 434 435 channel = [self acquireOpenChannelForURL: _url]; 436 if (channel) 437 { 438 if (debugOn) 439 [self debugWithFormat: @"acquired channel: %@", channel]; 440 441 /* check whether table exists */ 442 table = [_url gcsTableName]; 443 if ([table length] > 0) 444 result = [channel tableExistsWithName: table]; 445 else 446 result = YES; /* could open channel */ 447 448 /* release channel */ 449 [self releaseChannel: channel]; 450 } 451 else 452 { 453 if (debugOn) 454 [self debugWithFormat: @"could not acquire channel: %@", _url]; 455 result = NO; 456 } 457 458 return result; 459} 460 461/* collect old channels */ 462 463- (void) _garbageCollect: (NSTimer *) _timer 464{ 465 NSMutableArray *handlesToRemove; 466 unsigned i, count; 467 GCSChannelHandle *handle; 468 469 count = [availableChannels count]; 470 if (count) 471 { 472 /* collect channels to expire */ 473 474 handlesToRemove = [[NSMutableArray alloc] initWithCapacity: count]; 475 for (i = 0; i < count; i++) 476 { 477 handle = [availableChannels objectAtIndex: i]; 478 if ([[handle channel] isOpen]) 479 { 480 if ([handle age] > ChannelExpireAge) 481 [handlesToRemove addObject: handle]; 482 } 483 else 484 [handlesToRemove addObject: handle]; 485 } 486 487 /* remove channels */ 488 count = [handlesToRemove count]; 489 if (debugPools) 490 [self logWithFormat: @"DBPOOL: garbage collecting %d channels.", count]; 491 for (i = 0; i < count; i++) 492 { 493 handle = [handlesToRemove objectAtIndex: i]; 494 [handle retain]; 495 [availableChannels removeObject: handle]; 496 if ([[handle channel] isOpen]) 497 [[handle channel] closeChannel]; 498 [handle release]; 499 } 500 501 [handlesToRemove release]; 502 } 503} 504 505/* debugging */ 506 507- (BOOL) isDebuggingEnabled 508{ 509 return debugOn; 510} 511 512/* description */ 513 514- (NSString *) description 515{ 516 NSMutableString *ms; 517 518 ms = [NSMutableString stringWithCapacity: 256]; 519 [ms appendFormat: @"<0x%p[%@]: ", self, NSStringFromClass ([self class])]; 520 521 [ms appendFormat: @" #adaptors=%d", (int)[urlToAdaptor count]]; 522 523 [ms appendString: @">"]; 524 return ms; 525} 526 527@end /* GCSChannelManager */ 528 529@implementation GCSChannelHandle 530 531- (void) dealloc 532{ 533 [channel release]; 534 [creationTime release]; 535 [lastReleaseTime release]; 536 [lastAcquireTime release]; 537 [super dealloc]; 538} 539 540/* accessors */ 541 542- (EOAdaptorChannel *) channel 543{ 544 return channel; 545} 546 547- (BOOL) canHandleURL: (NSURL *) _url 548{ 549 BOOL result; 550 551 result = NO; 552 553 if (_url) 554 { 555 if (_url == url 556 || [[_url scheme] isEqualToString: @"sqlite"]) 557 result = YES; 558 else if ([[url host] isEqual: [_url host]]) 559 { 560 if ([[url gcsDatabaseName] 561 isEqualToString: [_url gcsDatabaseName]]) 562 { 563 if ([[url user] isEqual: [_url user]]) 564 { 565 if ([[url port] intValue] == [[_url port] intValue]) 566 result = YES; 567 else if (debugOn) 568 [self logWithFormat: 569 @"MISMATCH: different port (%@ vs %@) ..", 570 [url port], [_url port]]; 571 } 572 else if (debugOn) 573 [self logWithFormat: @"MISMATCH: different user .."]; 574 } 575 else if (debugOn) 576 [self logWithFormat: @"MISMATCH: different db .."]; 577 } 578 else if (debugOn) 579 [self logWithFormat: @"MISMATCH: different host (%@ vs %@) ", 580 [url host], [_url host]]; 581 } 582 else if (debugOn) 583 [self logWithFormat: @"MISMATCH: no url .."]; 584 585 return result; 586} 587 588- (NSTimeInterval) age 589{ 590 return [[NSCalendarDate calendarDate] 591 timeIntervalSinceDate: creationTime]; 592} 593 594/* NSCopying */ 595 596- (id) copyWithZone: (NSZone *) _zone 597{ 598 return [self retain]; 599} 600 601/* description */ 602 603- (NSString *) description 604{ 605 NSMutableString *ms; 606 607 ms = [NSMutableString stringWithCapacity: 256]; 608 [ms appendFormat: @"<0x%p[%@]: ", self, NSStringFromClass ([self class])]; 609 610 [ms appendFormat: @" channel=0x%p", channel]; 611 if (creationTime) 612 [ms appendFormat: @" created=%@", creationTime]; 613 if (lastReleaseTime) 614 [ms appendFormat: @" last-released=%@", lastReleaseTime]; 615 if (lastAcquireTime) 616 [ms appendFormat: @" last-acquired=%@", lastAcquireTime]; 617 618 [ms appendString: @">"]; 619 620 return ms; 621} 622 623@end /* GCSChannelHandle */ 624