Add mechanism to update cache only in main thread.

git-svn-id: svn+ssh://svn.gna.org/svn/gnustep/libs/sqlclient/trunk@26117 72102866-910b-0410-8b05-ffd578937521
This commit is contained in:
rfm 2008-02-21 16:23:23 +00:00
parent 43f1c9d6d6
commit 833a815c96
3 changed files with 257 additions and 69 deletions

View file

@ -1,3 +1,14 @@
2008-02-21 Richard Frith-Macdonald <rfm@gnu.org>
* SQLClient.h:
* SQLClient.m:
Experimental new method to set a thread to do all cached
queries on and to perform asynchronous updates if other
threads request information which is in the cache but
past its expiry date. Should allow threads to use
config information from a database without blocking
unnecessarily.
2008-02-15 Richard Frith-Macdonald <rfm@gnu.org>
* SQLClient.m: Fix memory leak when executing transaction.

View file

@ -190,6 +190,7 @@
@class NSMutableSet;
@class NSRecursiveLock;
@class NSString;
@class NSThread;
@class SQLTransaction;
/**
@ -377,6 +378,7 @@ extern unsigned SQLClientTimeTick();
NSTimeInterval _duration;
unsigned int _debugging; /** The current debugging level */
GSCache *_cache; /** The cache for query results */
NSThread *_cacheThread; /** Thread for cache queries */
}
/**
@ -1097,7 +1099,7 @@ extern unsigned SQLClientTimeTick();
@end
/**
* This category porovides methods for caching the results of queries
* This category provides methods for caching the results of queries
* in order to reduce the number of client-server trips and the database
* load produced by an application which needs update its information
* from the database frequently.
@ -1106,30 +1108,20 @@ extern unsigned SQLClientTimeTick();
/**
* Returns the cache used by the receiver for storing the results of
* requests made through it.
* requests made through it. Creates a new cache if necessary.
*/
- (GSCache*) cache;
/**
* If the result of the query is already cached and is still valid,
* return it. Otherwise, perform the query and cache the result
* giving it the specified lifetime in seconds.<br />
* If seconds is negative, the query is performed irrespective of
* whether it is already cached, and its absolute value is used to
* set the lifetime of the results.<br />
* If seconds is zero, the cache for this query is emptied.
* Calls -cache:simpleQuery:recordClass: with the default record class and
* wth a query string formed from stmt and the following values (if any).
*/
- (NSMutableArray*) cache: (int)seconds
query: (NSString*)stmt,...;
/**
* If the result of the query is already cached and is still valid,
* return it. Otherwise, perform the query and cache the result
* giving it the specified lifetime in seconds.<br />
* If seconds is negative, the query is performed irrespective of
* whether it is already cached, and its absolute value is used to
* set the lifetime of the results.<br />
* If seconds is zero, the cache for this query is emptied.
* Calls -cache:simpleQuery:recordClass: with the default record class and
* wth a query string formed from stmt and values.
*/
- (NSMutableArray*) cache: (int)seconds
query: (NSString*)stmt
@ -1141,7 +1133,7 @@ extern unsigned SQLClientTimeTick();
- (NSMutableArray*) cache: (int)seconds simpleQuery: (NSString*)stmt;
/**
* If the result of the query is already cached and is still valid,
* If the result of the query is already cached and has not expired,
* return it. Otherwise, perform the query and cache the result
* giving it the specified lifetime in seconds.<br />
* If seconds is negative, the query is performed irrespective of
@ -1151,7 +1143,14 @@ extern unsigned SQLClientTimeTick();
* Handles locking.<br />
* Maintains -lastOperation date.<br />
* The value of cls must be a class which responds to the
* [SQLRecord+newWithValues:keys:count:] method.
* [SQLRecord+newWithValues:keys:count:] method.<br />
* If a cache thread has been set using the -setCacheThread: method, and the
* -cache:simpleQuery:recordClass: method is called from a thread other
* than the cache thread, then any query to retrieve uncached data will
* be performed in the cache thread, and for cached (but expired) data,
* the old (expired) results may be returned ... in which case an
* asynchronous query to update the cache will be executed as soon
* as possible in the cache thread.
*/
- (NSMutableArray*) cache: (int)seconds
simpleQuery: (NSString*)stmt
@ -1165,6 +1164,19 @@ extern unsigned SQLClientTimeTick();
*/
- (void) setCache: (GSCache*)aCache;
/** Sets the thread to be used to retrieve data to populate the cache.<br />
* All cached queries will be performed in this thread (if non-nil).<br />
* The setting of a thread for the cache also implies that expired items in
* the cache may not be removed when they are queried from another thread,
* rather they can be kept (if they are not <em>too</em> old) and an
* asynchronous query to update them will be run on the cache thread.<br />
* The rule is that, if the item's age is more than twice its nominal
* lifetime, it will be retrieved immediately, otherwise it will be
* retrieved asynchrnonously.<br />
* Currently this may only be the main thread or nil. Any attempt to set
* another thread will use the main thread instead.
*/
- (void) setCacheThread: (NSThread*)aThread;
@end
/**

View file

@ -44,8 +44,10 @@
#import <Foundation/NSNull.h>
#import <Foundation/NSPathUtilities.h>
#import <Foundation/NSProcessInfo.h>
#import <Foundation/NSRunLoop.h>
#import <Foundation/NSSet.h>
#import <Foundation/NSString.h>
#import <Foundation/NSThread.h>
#import <Foundation/NSTimer.h>
#import <Foundation/NSUserDefaults.h>
#import <Foundation/NSValue.h>
@ -70,11 +72,30 @@ NSString * const SQLClientDidDisconnectNotification
= @"SQLClientDidDisconnectNotification";
static NSNull *null = nil;
static NSArray *queryModes = nil;
static NSThread *mainThread = nil;
static Class NSStringClass = 0;
static Class NSArrayClass = 0;
static Class NSDateClass = 0;
static Class NSSetClass = 0;
@interface CacheQuery : NSObject
{
@public
NSString *query;
Class recordClass;
unsigned lifetime;
}
@end
@implementation CacheQuery
- (void) dealloc
{
RELEASE(query);
[super dealloc];
}
@end
@interface SQLClientPool : NSObject
{
unsigned pool;
@ -842,11 +863,20 @@ static NSArray *rollbackStatement = nil;
@interface SQLClient (Private)
- (void) _configure: (NSNotification*)n;
- (void) _populateCache: (CacheQuery*)a;
- (NSArray*) _prepare: (NSString*)stmt args: (va_list)args;
- (void) _recordMainThread;
- (NSArray*) _substitute: (NSString*)str with: (NSDictionary*)vals;
+ (void) _tick: (NSTimer*)t;
@end
@interface SQLClient (GSCacheDelegate)
- (BOOL) shouldKeepItem: (id)anObject
withKey: (id)aKey
lifetime: (unsigned)lifetime
after: (unsigned)delay;
@end
@implementation SQLClient
static unsigned int maxConnections = 8;
@ -915,6 +945,10 @@ static unsigned int maxConnections = 8;
+ (void) initialize
{
static id modes[1];
modes[0] = NSDefaultRunLoopMode;
queryModes = [[NSArray alloc] initWithObjects: modes count: 1];
GSTickerTimeNow();
[SQLRecord class]; // Force initialisation
if (clientsMap == 0)
@ -1159,6 +1193,7 @@ static unsigned int maxConnections = 8;
DESTROY(_name);
DESTROY(_statements);
DESTROY(_cache);
DESTROY(_cacheThread);
[super dealloc];
}
@ -1166,6 +1201,7 @@ static unsigned int maxConnections = 8;
{
NSMutableString *s = AUTORELEASE([NSMutableString new]);
[lock lock];
[s appendFormat: @"Database - %@\n", [self clientName]];
[s appendFormat: @" Name - %@\n", [self name]];
[s appendFormat: @" DBase - %@\n", [self database]];
@ -1182,6 +1218,7 @@ static unsigned int maxConnections = 8;
{
[s appendFormat: @" Cache - %@\n", _cache];
}
[lock unlock];
return s;
}
@ -2068,6 +2105,32 @@ static unsigned int maxConnections = 8;
[self setPassword: s];
}
/** Internal method to populate the cache with the result of a query.
*/
- (void) _populateCache: (CacheQuery*)a
{
GSCache *cache;
id result;
[lock lock];
NS_DURING
{
result = [self backendQuery: a->query recordClass: a->recordClass];
}
NS_HANDLER
{
[lock unlock];
[localException raise];
}
NS_ENDHANDLER
[lock unlock];
cache = [self cache];
[cache setObject: result
forKey: a->query
lifetime: a->lifetime];
}
/**
* Internal method to build an sql string by quoting any non-string objects
* and concatenating the resulting strings in a nil terminated list.<br />
@ -2116,6 +2179,11 @@ static unsigned int maxConnections = 8;
return ma;
}
- (void) _recordMainThread
{
mainThread = [NSThread currentThread];
}
/**
* Internal method to substitute values from the dictionary into
* a string containing markup identifying where the values should
@ -2308,6 +2376,39 @@ static unsigned int maxConnections = 8;
}
@end
@implementation SQLClient (GSCacheDelegate)
- (BOOL) shouldKeepItem: (id)anObject
withKey: (id)aKey
lifetime: (unsigned)lifetime
after: (unsigned)delay
{
CacheQuery *a;
a = [CacheQuery new];
ASSIGN(a->query, aKey);
a->recordClass = [[[NSThread currentThread] threadDictionary]
objectForKey: @"SQLClientRecordClass"];
a->lifetime = lifetime;
AUTORELEASE(a);
if (_cacheThread == nil)
{
[self _populateCache: a];
}
else
{
/* We schedule an asynchronous update if the item is not too old,
* otherwise (more than lifetime seconds past its expiry) we wait
* for the update to complete.
*/
[self performSelectorOnMainThread: @selector(_populateCache:)
withObject: a
waitUntilDone: (delay > lifetime) ? YES : NO
modes: queryModes];
}
return YES; // Always keep items ...
}
@end
@implementation SQLClient(Convenience)
@ -2439,11 +2540,20 @@ static unsigned int maxConnections = 8;
- (GSCache*) cache
{
GSCache *c;
[lock lock];
if (_cache == nil)
{
_cache = [GSCache new];
if (_cacheThread != nil)
{
[_cache setDelegate: self];
}
}
return _cache;
c = RETAIN(_cache);
[lock unlock];
return AUTORELEASE(c);
}
- (NSMutableArray*) cache: (int)seconds
@ -2476,79 +2586,134 @@ static unsigned int maxConnections = 8;
simpleQuery: (NSString*)stmt
recordClass: (Class)cls
{
NSMutableArray *result = nil;
NSMutableArray *result;
NSTimeInterval start;
GSCache *c;
id toCache;
if (cls == 0)
{
[NSException raise: NSInvalidArgumentException
format: @"nil class passed to cache:simpleQuery:recordClass:"];
}
[lock lock];
NS_DURING
{
NSTimeInterval start = GSTickerTimeNow();
GSCache *c = [self cache];
id toCache = nil;
if (seconds < 0)
{
seconds = -seconds;
[[[NSThread currentThread] threadDictionary] setObject: cls
forKey: @"SQLClientRecordClass"];
start = GSTickerTimeNow();
c = [self cache];
toCache = nil;
if (seconds < 0)
{
seconds = -seconds;
result = nil;
}
else
{
result = [c objectForKey: stmt];
}
if (result == nil)
{
CacheQuery *a;
a = [CacheQuery new];
ASSIGN(a->query, stmt);
a->recordClass = cls;
a->lifetime = seconds;
AUTORELEASE(a);
if (_cacheThread == nil)
{
[self _populateCache: a];
}
else
{
result = [c objectForKey: stmt];
}
if (result == nil)
/* Not really an asynchronous query becuse we wait until it's
* done in order to have a result we can return.
*/
[self performSelectorOnMainThread: @selector(_populateCache:)
withObject: a
waitUntilDone: YES
modes: queryModes];
}
result = [c objectForKey: stmt];
_lastOperation = GSTickerTimeNow();
if (_duration >= 0)
{
result = toCache = [self backendQuery: stmt recordClass: cls];
_lastOperation = GSTickerTimeNow();
if (_duration >= 0)
{
NSTimeInterval d;
NSTimeInterval d;
d = _lastOperation - start;
if (d >= _duration)
{
[self debug: @"Duration %g for query %@", d, stmt];
}
d = _lastOperation - start;
if (d >= _duration)
{
[self debug: @"Duration %g for query %@", d, stmt];
}
}
if (seconds == 0)
{
// We have been told to remove the existing cached item.
[c setObject: nil forKey: stmt lifetime: seconds];
toCache = nil;
}
if (toCache != nil)
{
// We have a newly retrieved object ... cache it.
[c setObject: toCache forKey: stmt lifetime: seconds];
}
if (result != nil)
{
/*
* Return an autoreleased copy ... not the original cached data.
*/
result = [NSMutableArray arrayWithArray: result];
}
}
NS_HANDLER
if (seconds == 0)
{
[lock unlock];
[localException raise];
// We have been told to remove the existing cached item.
[c setObject: nil forKey: stmt lifetime: seconds];
toCache = nil;
}
if (toCache != nil)
{
// We have a newly retrieved object ... cache it.
[c setObject: toCache forKey: stmt lifetime: seconds];
}
if (result != nil)
{
/*
* Return an autoreleased copy ... not the original cached data.
*/
result = [NSMutableArray arrayWithArray: result];
}
NS_ENDHANDLER
[lock unlock];
return result;
}
- (void) setCache: (GSCache*)aCache
{
[lock lock];
if (_cacheThread != nil)
{
[_cache setDelegate: nil];
}
ASSIGN(_cache, aCache);
if (_cacheThread != nil)
{
[_cache setDelegate: self];
}
[lock unlock];
}
- (void) setCacheThread: (NSThread*)aThread
{
if (mainThread == nil)
{
[self performSelectorOnMainThread: @selector(_recordMainThread)
withObject: nil
waitUntilDone: NO
modes: queryModes];
}
if (aThread != nil && aThread != mainThread)
{
NSLog(@"SQLClient: only the main thread is usable as cache thread");
aThread = mainThread;
}
[lock lock];
if (_cacheThread != nil)
{
[_cache setDelegate: nil];
}
ASSIGN(_cacheThread, aThread);
if (_cacheThread != nil)
{
[_cache setDelegate: self];
}
[lock unlock];
}
@end