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