From c01fef570abb90a12ad62236d40425a50c434533 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Fri, 4 May 2012 10:06:01 -0700 Subject: [PATCH] Fixed a bunch of memory leaks and refcount cycles. Most of these only become apparent when trying to close a CouchServer or TDServer. Conflicts: Couch/CouchTouchDBServer.h Couch/CouchTouchDBServer.m CouchCocoa.xcodeproj/project.pbxproj Change-Id: I544eb633fe2b7437bda2cbba68f3f2c1131b021a --- Couch/CouchDatabase.m | 4 +++ Couch/CouchInternal.h | 1 + Couch/CouchQuery.h | 12 ++++--- Couch/CouchQuery.m | 65 ++++++++++++++++++++------------------ Couch/CouchServer.h | 3 ++ Couch/CouchServer.m | 10 +++++- Couch/CouchTouchDBServer.h | 7 ++++ Couch/CouchTouchDBServer.m | 41 +++++++++++++++++++++++- REST/RESTCache.h | 6 ++++ REST/RESTCache.m | 10 ++++++ 10 files changed, 123 insertions(+), 36 deletions(-) diff --git a/Couch/CouchDatabase.m b/Couch/CouchDatabase.m index 6e2a7fa..7527216 100644 --- a/Couch/CouchDatabase.m +++ b/Couch/CouchDatabase.m @@ -157,6 +157,10 @@ - (void) clearDocumentCache { [_docCache forgetAllResources]; } +- (void) unretainDocumentCache { + [_docCache unretainResources]; +} + #pragma mark - #pragma mark BATCH CHANGES diff --git a/Couch/CouchInternal.h b/Couch/CouchInternal.h index f9b011d..9045a38 100644 --- a/Couch/CouchInternal.h +++ b/Couch/CouchInternal.h @@ -38,6 +38,7 @@ typedef void (^OnDatabaseChangeBlock)(CouchDocument*, BOOL externalChange); - (void) beginDocumentOperation: (CouchResource*)resource; - (void) endDocumentOperation: (CouchResource*)resource; - (void) onChange: (OnDatabaseChangeBlock)block; // convenience for unit tests +- (void) unretainDocumentCache; @end diff --git a/Couch/CouchQuery.h b/Couch/CouchQuery.h index 72205b7..c771a08 100644 --- a/Couch/CouchQuery.h +++ b/Couch/CouchQuery.h @@ -35,7 +35,7 @@ typedef enum { NSString* _startKeyDocID; NSString* _endKeyDocID; CouchStaleness _stale; - BOOL _descending, _prefetch; + BOOL _descending, _prefetch, _sequences; NSArray *_keys; NSUInteger _groupLevel; } @@ -80,6 +80,8 @@ typedef enum { These can be accessed via CouchQueryRow's -documentContents property. */ @property BOOL prefetch; +@property BOOL sequences; + /** Starts an asynchronous query of the CouchDB view. When complete, the operation's resultObject will be the CouchQueryEnumerator. */ @@ -122,7 +124,7 @@ typedef enum { @interface CouchQueryEnumerator : NSEnumerator { @private - CouchQuery* _query; + CouchDatabase* _database; NSArray* _rows; NSUInteger _totalCount; NSUInteger _nextRow; @@ -151,11 +153,10 @@ typedef enum { @interface CouchQueryRow : NSObject { @private - CouchQuery* _query; + CouchDatabase* _database; id _result; } -@property (readonly) CouchQuery* query; @property (readonly) id key; @property (readonly) id value; @@ -187,4 +188,7 @@ typedef enum { /** Convenience for use in keypaths. Returns the key at the given index. */ @property (readonly) id key0, key1, key2, key3; +/** The local sequence number of the associated doc/revision. + Valid only if the 'sequences' and 'prefetch' properties were set in the query; otherwise returns 0. */ +@property (readonly) UInt64 localSequence; @end diff --git a/Couch/CouchQuery.m b/Couch/CouchQuery.m index c8efd7e..07e50cb 100644 --- a/Couch/CouchQuery.m +++ b/Couch/CouchQuery.m @@ -23,12 +23,12 @@ @interface CouchQueryEnumerator () -- (id) initWithQuery: (CouchQuery*)query result: (NSDictionary*)result; +- (id) initWithDatabase: (CouchDatabase*)db result: (NSDictionary*)result; @end @interface CouchQueryRow () -- (id) initWithQuery: (CouchQuery*)query result: (id)result; +- (id) initWithDatabase: (CouchDatabase*)db result: (id)result; @end @@ -69,7 +69,7 @@ - (void) dealloc @synthesize limit=_limit, skip=_skip, descending=_descending, startKey=_startKey, endKey=_endKey, prefetch=_prefetch, keys=_keys, groupLevel=_groupLevel, startKeyDocID=_startKeyDocID, - endKeyDocID=_endKeyDocID, stale=_stale; + endKeyDocID=_endKeyDocID, stale=_stale, sequences=_sequences; - (CouchDesignDocument*) designDocument { @@ -111,6 +111,8 @@ - (NSMutableDictionary*) requestParams { [params setObject: @"true" forKey: @"?descending"]; if (_prefetch) [params setObject: @"true" forKey: @"?include_docs"]; + if (_sequences) + [params setObject: @"true" forKey: @"?local_seq"]; if (_groupLevel > 0) [params setObject: [NSNumber numberWithUnsignedLong: _groupLevel] forKey: @"?group_level"]; [params setObject: @"true" forKey: @"?update_seq"]; @@ -148,8 +150,8 @@ - (NSError*) operation: (RESTOperation*)op willCompleteWithError: (NSError*)erro NSArray* rows = $castIf(NSArray, [result objectForKey: @"rows"]); if (rows) { [self cacheResponse: op]; - op.resultObject = [[[CouchQueryEnumerator alloc] initWithQuery: self - result: result] autorelease]; + op.resultObject = [[[CouchQueryEnumerator alloc] initWithDatabase: self.database + result: result] autorelease]; } else { Warn(@"Couldn't parse rows from CouchDB view response"); error = [RESTOperation errorWithHTTPStatus: 502 message: nil URL: self.URL]; @@ -269,7 +271,8 @@ - (NSError*) operation: (RESTOperation*)op willCompleteWithError: (NSError*)erro if (rows && ![rows isEqual: _rows]) { COUCHLOG(@"CouchLiveQuery: ...Rows changed! (now %lu)", (unsigned long)rows.count); self.rows = rows; // Triggers KVO notification - self.prefetch = NO; // (prefetch disables conditional GET shortcut on next fetch) + if (!self.sequences) + self.prefetch = NO; // (prefetch disables conditional GET shortcut on next fetch) // If this query isn't up-to-date (race condition where the db updated again after sending // the response), start another fetch. @@ -293,19 +296,19 @@ @implementation CouchQueryEnumerator @synthesize totalCount=_totalCount, sequenceNumber=_sequenceNumber; -- (id) initWithQuery: (CouchQuery*)query - rows: (NSArray*)rows - totalCount: (NSUInteger)totalCount - sequenceNumber: (NSUInteger)sequenceNumber +- (id) initWithDatabase: (CouchDatabase*)database + rows: (NSArray*)rows + totalCount: (NSUInteger)totalCount + sequenceNumber: (NSUInteger)sequenceNumber { - NSParameterAssert(query); + NSParameterAssert(database); self = [super init]; if (self ) { if (!rows) { [self release]; return nil; } - _query = [query retain]; + _database = database; _rows = [rows retain]; _totalCount = totalCount; _sequenceNumber = sequenceNumber; @@ -313,24 +316,23 @@ - (id) initWithQuery: (CouchQuery*)query return self; } -- (id) initWithQuery: (CouchQuery*)query result: (NSDictionary*)result { - return [self initWithQuery: query - rows: $castIf(NSArray, [result objectForKey: @"rows"]) - totalCount: [[result objectForKey: @"total_rows"] intValue] - sequenceNumber: [[result objectForKey: @"update_seq"] intValue]]; +- (id) initWithDatabase: (CouchDatabase*)db result: (NSDictionary*)result { + return [self initWithDatabase: db + rows: $castIf(NSArray, [result objectForKey: @"rows"]) + totalCount: [[result objectForKey: @"total_rows"] intValue] + sequenceNumber: [[result objectForKey: @"update_seq"] intValue]]; } - (id) copyWithZone: (NSZone*)zone { - return [[[self class] alloc] initWithQuery: _query - rows: _rows - totalCount: _totalCount - sequenceNumber: _sequenceNumber]; + return [[[self class] alloc] initWithDatabase: _database + rows: _rows + totalCount: _totalCount + sequenceNumber: _sequenceNumber]; } - (void) dealloc { - [_query release]; [_rows release]; [super dealloc]; } @@ -352,8 +354,8 @@ - (NSUInteger) count { - (CouchQueryRow*) rowAtIndex: (NSUInteger)index { - return [[[CouchQueryRow alloc] initWithQuery: _query - result: [_rows objectAtIndex:index]] + return [[[CouchQueryRow alloc] initWithDatabase: _database + result: [_rows objectAtIndex:index]] autorelease]; } @@ -378,7 +380,7 @@ - (id) nextObject { @implementation CouchQueryRow -- (id) initWithQuery: (CouchQuery*)query result: (id)result { +- (id) initWithDatabase: (CouchDatabase*)database result: (id)result { self = [super init]; if (self) { if (![result isKindOfClass: [NSDictionary class]]) { @@ -386,7 +388,7 @@ - (id) initWithQuery: (CouchQuery*)query result: (id)result { [self release]; return nil; } - _query = [query retain]; + _database = database; _result = [result retain]; } return self; @@ -394,14 +396,11 @@ - (id) initWithQuery: (CouchQuery*)query result: (id)result { - (void)dealloc { - [_query release]; [_result release]; [super dealloc]; } -@synthesize query=_query; - - (id) key {return [_result objectForKey: @"key"];} - (id) value {return [_result objectForKey: @"value"];} - (NSString*) sourceDocumentID {return [_result objectForKey: @"id"];} @@ -451,12 +450,18 @@ - (CouchDocument*) document { NSString* docID = self.documentID; if (!docID) return nil; - CouchDocument* doc = [_query.database documentWithID: docID]; + CouchDocument* doc = [_database documentWithID: docID]; [doc loadCurrentRevisionFrom: self]; return doc; } +- (UInt64) localSequence { + id seq = [self.documentProperties objectForKey: @"_local_seq"]; + return $castIf(NSNumber, seq).unsignedLongLongValue; +} + + - (NSString*) description { return [NSString stringWithFormat: @"%@[key=%@; value=%@; id=%@]", [self class], diff --git a/Couch/CouchServer.h b/Couch/CouchServer.h index ebb2b5e..ac2dbd0 100644 --- a/Couch/CouchServer.h +++ b/Couch/CouchServer.h @@ -36,6 +36,9 @@ /** Without a URL, connects to localhost on default port 5984. */ - (id) init; +/** Releases all resources used by the CouchServer instance. */ +- (void) close; + /** Fetches the server's current version string. (Synchronous) */ - (NSString*) getVersion: (NSError**)outError; diff --git a/Couch/CouchServer.m b/Couch/CouchServer.m index 8cea586..5529bf6 100644 --- a/Couch/CouchServer.m +++ b/Couch/CouchServer.m @@ -53,6 +53,14 @@ - (void)dealloc { } +- (void) close { + [_replicationsQuery release]; + _replicationsQuery = nil; + for (CouchDatabase* db in _dbCache.allCachedResources) + [db unretainDocumentCache]; +} + + - (RESTResource*) childWithPath: (NSString*)name { return [[[CouchResource alloc] initWithParent: self relativePath: name] autorelease]; } @@ -122,7 +130,7 @@ - (CouchLiveQuery*) replicationsQuery { if (!_replicationsQuery) { CouchDatabase* replicatorDB = [self replicatorDatabase]; replicatorDB.tracksChanges = YES; - _replicationsQuery = [[replicatorDB getAllDocuments] asLiveQuery]; + _replicationsQuery = [[[replicatorDB getAllDocuments] asLiveQuery] retain]; [_replicationsQuery wait]; } return _replicationsQuery; diff --git a/Couch/CouchTouchDBServer.h b/Couch/CouchTouchDBServer.h index 35d79a1..0f23a27 100644 --- a/Couch/CouchTouchDBServer.h +++ b/Couch/CouchTouchDBServer.h @@ -28,9 +28,16 @@ /** Preferred initializer. Starts up an in-process server. */ - (id)init; +/** Starts up a server that stores its data at the given path. + @param serverPath The filesystem path to the server directory. If it doesn't already exist it will be created. */ +- (id) initWithServerPath: (NSString*)serverPath; + /** Inherited initializer, if you want to connect to a remote server for debugging purposes. */ - (id) initWithURL: (NSURL*)url; +/** Shuts down the TouchDB server. */ +- (void) close; + /** If this is non-nil, the server failed to initialize. */ @property (readonly) NSError* error; diff --git a/Couch/CouchTouchDBServer.m b/Couch/CouchTouchDBServer.m index d6ea84f..8ba1667 100644 --- a/Couch/CouchTouchDBServer.m +++ b/Couch/CouchTouchDBServer.m @@ -25,11 +25,13 @@ @interface TDServer : NSObject - (id) initWithDirectory: (NSString*)dirPath error: (NSError**)outError; - (void) queue: (void(^)())block; - (void) tellDatabaseNamed: (NSString*)dbName to: (void (^)(TDDatabase*))block; +- (void) close; @end @interface TDURLProtocol : NSURLProtocol + (NSURL*) rootURL; + (void) setServer: (TDServer*)server; ++ (NSURL*) registerServer: (TDServer*)server; @end @@ -84,6 +86,35 @@ - (id)init { } +- (id) initWithServerPath: (NSString*)serverPath { +#if TARGET_OS_IPHONE + Class classTDURLProtocol = [TDURLProtocol class]; + Class classTDServer = [TDServer class]; +#else + // On Mac OS TouchDB.framework is linked dynamically, so avoid explicit references to its + // classes because they'd create link errors building CouchCocoa. + Class classTDURLProtocol = NSClassFromString(@"TDURLProtocol"); + Class classTDServer = NSClassFromString(@"TDServer"); + NSAssert(classTDURLProtocol && classTDServer, @"Not linked with TouchDB framework"); +#endif + + NSError* error; + TDServer* server = [[classTDServer alloc] initWithDirectory: serverPath error: &error]; + NSURL* rootURL = server ? [classTDURLProtocol registerServer: server] + : [classTDURLProtocol rootURL]; + + self = [super initWithURL: rootURL]; + if (self) { + _touchServer = server; + if (!server) + _error = [error retain]; + } else { + [server release]; + } + return self; +} + + - (id) initWithURL:(NSURL *)url { if (url) return [super initWithURL: url]; @@ -94,7 +125,7 @@ - (id) initWithURL:(NSURL *)url { - (void) dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; - [_touchServer release]; + [self close]; [_error release]; [super dealloc]; } @@ -114,6 +145,14 @@ - (void) tellTDDatabaseNamed: (NSString*)dbName to: (void (^)(TDDatabase*))block } +- (void) close { + [super close]; + [_touchServer close]; + [_touchServer release]; + _touchServer = nil; +} + + #pragma mark - ACTIVITY: // I don't have to resort to polling the /_activity URL; I can listen for direct notifications diff --git a/REST/RESTCache.h b/REST/RESTCache.h index 7f8c0de..0a44284 100644 --- a/REST/RESTCache.h +++ b/REST/RESTCache.h @@ -55,4 +55,10 @@ /** Removes all resources from the cache. */ - (void) forgetAllResources; +/** Removes retained references to objects. + All objects that don't have anything else retaining them will be removed from the cache. */ +- (void) unretainResources; + +- (NSArray*) allCachedResources; + @end diff --git a/REST/RESTCache.m b/REST/RESTCache.m index 50e3881..fa4e33b 100644 --- a/REST/RESTCache.m +++ b/REST/RESTCache.m @@ -100,6 +100,16 @@ - (void) resourceBeingDealloced:(RESTResource*)resource { } +- (NSArray*) allCachedResources { + return _map.allValues; +} + + +- (void) unretainResources { + [_cache removeAllObjects]; +} + + - (void) forgetAllResources { [_map removeAllObjects]; [_cache removeAllObjects];