/* Copyright (C) 2004-2005 SKYRIX Software AG This file is part of OpenGroupware.org. OGo is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. OGo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with OGo; see the file COPYING. If not, write to the Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #import #import #import #import #import #import #import #import #import #import #import #import #import #import "GCSChannelManager.h" #import "NSURL+GCS.h" #import "EOAdaptorChannel+GCS.h" /* TODO: - implemented pooling - auto-close channels which are very old?! (eg missing release due to an exception) */ @interface GCSChannelHandle : NSObject { @public NSURL *url; EOAdaptorChannel *channel; NSDate *creationTime; NSDate *lastReleaseTime; NSDate *lastAcquireTime; } - (EOAdaptorChannel *) channel; - (BOOL) canHandleURL: (NSURL *) _url; - (NSTimeInterval) age; @end @implementation GCSChannelManager static BOOL debugOn = NO; static BOOL debugPools = NO; static int ChannelExpireAge = 180; static NSTimeInterval ChannelCollectionTimer = 5 * 60; + (void) initialize { NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; debugOn = [ud boolForKey: @"GCSChannelManagerDebugEnabled"]; debugPools = [ud boolForKey: @"GCSChannelManagerPoolDebugEnabled"]; ChannelExpireAge = [[ud objectForKey: @"GCSChannelExpireAge"] intValue]; if (ChannelExpireAge < 1) ChannelExpireAge = 180; ChannelCollectionTimer = [[ud objectForKey: @"GCSChannelCollectionTimer"] intValue]; if (ChannelCollectionTimer < 1) ChannelCollectionTimer = 5*60; } + (NSString *) adaptorNameForURLScheme: (NSString *) _scheme { // TODO: map scheme to adaptors (eg 'postgresql: //' to PostgreSQL return @"PostgreSQL"; } + (id) defaultChannelManager { static GCSChannelManager *cm = nil; if (!cm) cm = [self new]; return cm; } - (id) init { if ((self = [super init])) { urlToAdaptor = [[NSMutableDictionary alloc] initWithCapacity: 4]; lastFailures = [[NSMutableDictionary alloc] initWithCapacity: 4]; availableChannels = [[NSMutableArray alloc] initWithCapacity: 16]; busyChannels = [[NSMutableArray alloc] initWithCapacity: 16]; gcTimer = [[NSTimer scheduledTimerWithTimeInterval: ChannelCollectionTimer target: self selector: @selector (_garbageCollect:) userInfo: nil repeats: YES] retain]; } return self; } - (void) dealloc { if (gcTimer) [gcTimer invalidate]; [busyChannels release]; [availableChannels release]; [lastFailures release]; [urlToAdaptor release]; [super dealloc]; } /* adaptors */ - (NSDictionary *) connectionDictionaryForURL: (NSURL *) _url { NSMutableDictionary *md; id tmp; md = [NSMutableDictionary dictionaryWithCapacity: 4]; if ((tmp = [_url host])) [md setObject: tmp forKey: @"hostName"]; if ((tmp = [_url port])) [md setObject: tmp forKey: @"port"]; if ((tmp = [_url user])) [md setObject: tmp forKey: @"userName"]; if ((tmp = [_url password])) [md setObject: tmp forKey: @"password"]; if ((tmp = [_url gcsDatabaseName])) [md setObject: tmp forKey: @"databaseName"]; [self debugWithFormat: @"build connection dictionary for URL %@: %@", [_url absoluteString], md]; return md; } - (EOAdaptor *) adaptorForURL: (NSURL *) _url { EOAdaptor *adaptor; NSString *key; NSString *adaptorName; NSDictionary *condict; adaptor = nil; if (_url) { if ((key = [_url gcsURLId])) { adaptor = [urlToAdaptor objectForKey: key]; if (adaptor) [self debugWithFormat: @"using cached adaptor: %@", adaptor]; else { [self debugWithFormat: @"creating new adaptor for URL: %@", _url]; if ([EOAdaptor respondsToSelector: @selector (adaptorForURL:)]) adaptor = [EOAdaptor adaptorForURL: _url]; else { adaptorName = [[self class] adaptorNameForURLScheme: [_url scheme]]; if ([adaptorName length]) { condict = [self connectionDictionaryForURL: _url]; adaptor = [EOAdaptor adaptorWithName: adaptorName]; if (adaptor) [adaptor setConnectionDictionary: condict]; else [self errorWithFormat: @"did not find adaptor '%@' for URL: %@", adaptorName, _url]; } else [self errorWithFormat: @"cannot handle URL: %@", _url]; } [urlToAdaptor setObject: adaptor forKey: key]; } } } return adaptor; } /* channels */ - (GCSChannelHandle *) findBusyChannelHandleForChannel: (EOAdaptorChannel *) _ch { NSEnumerator *e; GCSChannelHandle *handle, *currentHandle; handle = NULL; e = [busyChannels objectEnumerator]; while (!handle && (currentHandle = [e nextObject])) if ([currentHandle channel] == _ch) handle = currentHandle; return handle; } - (GCSChannelHandle *) findAvailChannelHandleForURL: (NSURL *) _url { NSEnumerator *e; GCSChannelHandle *handle, *currentHandle; handle = nil; e = [availableChannels objectEnumerator]; while (!handle && (currentHandle = [e nextObject])) if ([currentHandle canHandleURL: _url]) handle = currentHandle; else if (debugPools) [self logWithFormat: @"DBPOOL: cannot use handle (%@ vs %@) ", [_url absoluteString], [handle->url absoluteString]]; return handle; } - (EOAdaptorChannel *) _createChannelForURL: (NSURL *) _url { EOAdaptor *adaptor; EOAdaptorContext *adContext; EOAdaptorChannel *adChannel; adChannel = nil; adaptor = [self adaptorForURL: _url]; if (adaptor) { adContext = [adaptor createAdaptorContext]; if (adContext) { adChannel = [adContext createAdaptorChannel]; if (!adChannel) [self errorWithFormat: @"could not create adaptor channel!"]; } else [self errorWithFormat: @"could not create adaptor context!"]; } return adChannel; } - (EOAdaptorChannel *) acquireOpenChannelForURL: (NSURL *) _url { // TODO: naive implementation, add pooling! EOAdaptorChannel *channel; GCSChannelHandle *handle; NSCalendarDate *now, *lastFailure; NSString *urlId; channel = nil; urlId = [_url gcsURLId]; now = [NSCalendarDate date]; lastFailure = [lastFailures objectForKey: urlId]; if ([[lastFailure dateByAddingYears: 0 months: 0 days: 0 hours: 0 minutes: 0 seconds: 5] earlierDate: now] != now) { /* look for cached handles */ handle = [self findAvailChannelHandleForURL: _url]; if (handle) { // TODO: check age? [busyChannels addObject: handle]; [availableChannels removeObject: handle]; ASSIGN (handle->lastAcquireTime, now); channel = [handle channel]; if (debugPools) [self logWithFormat: @"DBPOOL: reused cached DB channel! (%p)", channel]; } else { if (debugPools) { [self logWithFormat: @"DBPOOL: create new DB channel for URL: %@", [_url absoluteString]]; } /* create channel */ channel = [self _createChannelForURL: _url]; if (channel) { if ([channel isOpen] || [channel openChannel]) { /* create handle for channel */ handle = [[GCSChannelHandle alloc] init]; handle->url = [_url retain]; handle->channel = [channel retain]; handle->creationTime = [now retain]; handle->lastAcquireTime = [now retain]; [busyChannels addObject: handle]; [handle release]; if (lastFailure) { [self logWithFormat: @"db for %@ is now back up", [_url absoluteString]]; [lastFailures removeObjectForKey: urlId]; } } else { [self errorWithFormat: @"could not open channel %@ for URL: %@", channel, [_url absoluteString]]; channel = nil; [lastFailures setObject: now forKey: urlId]; [self warnWithFormat: @" will prevent opening of this" @" channel 5 seconds after %@", now]; } } } } return channel; } - (void) releaseChannel: (EOAdaptorChannel *) _channel { [self releaseChannel: _channel immediately: NO]; } - (void) releaseChannel: (EOAdaptorChannel *) _channel immediately: (BOOL) _immediately { GCSChannelHandle *handle; BOOL keepOpen; handle = [self findBusyChannelHandleForChannel: _channel]; if (handle) { [handle retain]; ASSIGN (handle->lastReleaseTime, [NSCalendarDate date]); [busyChannels removeObject: handle]; keepOpen = NO; if (!_immediately && [_channel isOpen] && [handle age] < ChannelExpireAge) { keepOpen = YES; // TODO: consider age [availableChannels addObject: handle]; if (debugPools) [self logWithFormat: @"DBPOOL: keeping channel (age %ds, #%d, %p) : %@", (int) [handle age], [availableChannels count], [handle->url absoluteString], _channel]; } else if (debugPools) { [self logWithFormat: @"DBPOOL: freeing old channel (age %ds, %p) ", (int) [handle age], _channel]; } if (!keepOpen && [_channel isOpen]) [_channel closeChannel]; [handle release]; } else { if ([_channel isOpen]) [_channel closeChannel]; [_channel release]; } } /* checking for tables */ - (BOOL) canConnect: (NSURL *) _url { /* this can check for DB connect as well as for table URLs (whether a table exists) */ EOAdaptorChannel *channel; NSString *table; BOOL result; channel = [self acquireOpenChannelForURL: _url]; if (channel) { if (debugOn) [self debugWithFormat: @"acquired channel: %@", channel]; /* check whether table exists */ table = [_url gcsTableName]; if ([table length] > 0) result = [channel tableExistsWithName: table]; else result = YES; /* could open channel */ /* release channel */ [self releaseChannel: channel]; } else { if (debugOn) [self debugWithFormat: @"could not acquire channel: %@", _url]; result = NO; } return result; } /* collect old channels */ - (void) _garbageCollect: (NSTimer *) _timer { NSMutableArray *handlesToRemove; unsigned i, count; GCSChannelHandle *handle; count = [availableChannels count]; if (count) { /* collect channels to expire */ handlesToRemove = [[NSMutableArray alloc] initWithCapacity: count]; for (i = 0; i < count; i++) { handle = [availableChannels objectAtIndex: i]; if ([[handle channel] isOpen]) { if ([handle age] > ChannelExpireAge) [handlesToRemove addObject: handle]; } else [handlesToRemove addObject: handle]; } /* remove channels */ count = [handlesToRemove count]; if (debugPools) [self logWithFormat: @"DBPOOL: garbage collecting %d channels.", count]; for (i = 0; i < count; i++) { handle = [handlesToRemove objectAtIndex: i]; [handle retain]; [availableChannels removeObject: handle]; if ([[handle channel] isOpen]) [[handle channel] closeChannel]; [handle release]; } [handlesToRemove release]; } } /* debugging */ - (BOOL) isDebuggingEnabled { return debugOn; } /* description */ - (NSString *) description { NSMutableString *ms; ms = [NSMutableString stringWithCapacity: 256]; [ms appendFormat: @"<0x%p[%@]: ", self, NSStringFromClass ([self class])]; [ms appendFormat: @" #adaptors=%d", [urlToAdaptor count]]; [ms appendString: @">"]; return ms; } @end /* GCSChannelManager */ @implementation GCSChannelHandle - (void) dealloc { [channel release]; [creationTime release]; [lastReleaseTime release]; [lastAcquireTime release]; [super dealloc]; } /* accessors */ - (EOAdaptorChannel *) channel { return channel; } - (BOOL) canHandleURL: (NSURL *) _url { BOOL result; result = NO; if (_url) { if (_url == url || [[_url scheme] isEqualToString: @"sqlite"]) result = YES; else if ([[url host] isEqual: [_url host]]) { if ([[url gcsDatabaseName] isEqualToString: [_url gcsDatabaseName]]) { if ([[url user] isEqual: [_url user]]) { if ([[url port] intValue] == [[_url port] intValue]) result = YES; else if (debugOn) [self logWithFormat: @"MISMATCH: different port (%@ vs %@) ..", [url port], [_url port]]; } else if (debugOn) [self logWithFormat: @"MISMATCH: different user .."]; } else if (debugOn) [self logWithFormat: @"MISMATCH: different db .."]; } else if (debugOn) [self logWithFormat: @"MISMATCH: different host (%@ vs %@) ", [url host], [_url host]]; } else if (debugOn) [self logWithFormat: @"MISMATCH: no url .."]; return result; } - (NSTimeInterval) age { return [[NSCalendarDate calendarDate] timeIntervalSinceDate: creationTime]; } /* NSCopying */ - (id) copyWithZone: (NSZone *) _zone { return [self retain]; } /* description */ - (NSString *) description { NSMutableString *ms; ms = [NSMutableString stringWithCapacity: 256]; [ms appendFormat: @"<0x%p[%@]: ", self, NSStringFromClass ([self class])]; [ms appendFormat: @" channel=0x%p", channel]; if (creationTime) [ms appendFormat: @" created=%@", creationTime]; if (lastReleaseTime) [ms appendFormat: @" last-released=%@", lastReleaseTime]; if (lastAcquireTime) [ms appendFormat: @" last-acquired=%@", lastAcquireTime]; [ms appendString: @">"]; return ms; } @end /* GCSChannelHandle */