/* * Copyright 2017 Google * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #import #import "FPersistenceManager.h" #import "FLevelDBStorageEngine.h" #import "FCacheNode.h" #import "FIndexedNode.h" #import "FTrackedQueryManager.h" #import "FTrackedQuery.h" #import "FUtilities.h" #import "FPruneForest.h" #import "FClock.h" @interface FPersistenceManager () @property (nonatomic, strong) id storageEngine; @property (nonatomic, strong) id cachePolicy; @property (nonatomic, strong) FTrackedQueryManager *trackedQueryManager; @property (nonatomic) NSUInteger serverCacheUpdatesSinceLastPruneCheck; @end @implementation FPersistenceManager - (id)initWithStorageEngine:(id)storageEngine cachePolicy:(id)cachePolicy { self = [super init]; if (self != nil) { self->_storageEngine = storageEngine; self->_cachePolicy = cachePolicy; self->_trackedQueryManager = [[FTrackedQueryManager alloc] initWithStorageEngine:self.storageEngine clock:[FSystemClock clock]]; } return self; } - (void)close { [self.storageEngine close]; self.storageEngine = nil; self.trackedQueryManager = nil; } - (void)saveUserOverwrite:(id)node atPath:(FPath *)path writeId:(NSUInteger)writeId { [self.storageEngine saveUserOverwrite:node atPath:path writeId:writeId]; } - (void)saveUserMerge:(FCompoundWrite *)merge atPath:(FPath *)path writeId:(NSUInteger)writeId { [self.storageEngine saveUserMerge:merge atPath:path writeId:writeId]; } - (void)removeUserWrite:(NSUInteger)writeId { [self.storageEngine removeUserWrite:writeId]; } - (void)removeAllUserWrites { [self.storageEngine removeAllUserWrites]; } - (NSArray *)userWrites { return [self.storageEngine userWrites]; } - (FCacheNode *)serverCacheForQuery:(FQuerySpec *)query { NSSet *trackedKeys; BOOL complete; // TODO[offline]: Should we use trackedKeys to find out if this location is a child of a complete query? if ([self.trackedQueryManager isQueryComplete:query]) { complete = YES; FTrackedQuery *trackedQuery = [self.trackedQueryManager findTrackedQuery:query]; if (!query.loadsAllData && trackedQuery.isComplete) { trackedKeys = [self.storageEngine trackedQueryKeysForQuery:trackedQuery.queryId]; } else { trackedKeys = nil; } } else { complete = NO; trackedKeys = [self.trackedQueryManager knownCompleteChildrenAtPath:query.path]; } id node; if (trackedKeys != nil) { node = [self.storageEngine serverCacheForKeys:trackedKeys atPath:query.path]; } else { node = [self.storageEngine serverCacheAtPath:query.path]; } FIndexedNode *indexedNode = [FIndexedNode indexedNodeWithNode:node index:query.index]; return [[FCacheNode alloc] initWithIndexedNode:indexedNode isFullyInitialized:complete isFiltered:(trackedKeys != nil)]; } - (void)updateServerCacheWithNode:(id)node forQuery:(FQuerySpec *)query { BOOL merge = !query.loadsAllData; [self.storageEngine updateServerCache:node atPath:query.path merge:merge]; [self setQueryComplete:query]; [self doPruneCheckAfterServerUpdate]; } - (void)updateServerCacheWithMerge:(FCompoundWrite *)merge atPath:(FPath *)path { [self.storageEngine updateServerCacheWithMerge:merge atPath:path]; [self doPruneCheckAfterServerUpdate]; } - (void)applyUserMerge:(FCompoundWrite *)merge toServerCacheAtPath:(FPath *)path { // TODO[offline]: rework this to be more efficient [merge enumerateWrites:^(FPath *relativePath, id node, BOOL *stop) { [self applyUserWrite:node toServerCacheAtPath:[path child:relativePath]]; }]; } - (void)applyUserWrite:(id)write toServerCacheAtPath:(FPath *)path { // This is a hack to guess whether we already cached this because we got a server data update for this // write via an existing active default query. If we didn't, then we'll manually cache this and add a // tracked query to mark it complete and keep it cached. // Unfortunately this is just a guess and it's possible that we *did* get an update (e.g. via a filtered // query) and by overwriting the cache here, we'll actually store an incorrect value (e.g. in the case // that we wrote a ServerValue.TIMESTAMP and the server resolved it to a different value). // TODO[offline]: Consider reworking. if (![self.trackedQueryManager hasActiveDefaultQueryAtPath:path]) { [self.storageEngine updateServerCache:write atPath:path merge:NO]; [self.trackedQueryManager ensureCompleteTrackedQueryAtPath:path]; } } - (void)setQueryComplete:(FQuerySpec *)query { if (query.loadsAllData) { [self.trackedQueryManager setQueriesCompleteAtPath:query.path]; } else { [self.trackedQueryManager setQueryComplete:query]; } } - (void)setQueryActive:(FQuerySpec *)spec { [self.trackedQueryManager setQueryActive:spec]; } - (void)setQueryInactive:(FQuerySpec *)spec { [self.trackedQueryManager setQueryInactive:spec]; } - (void)doPruneCheckAfterServerUpdate { self.serverCacheUpdatesSinceLastPruneCheck++; if ([self.cachePolicy shouldCheckCacheSize:self.serverCacheUpdatesSinceLastPruneCheck]) { FFDebug(@"I-RDB078001", @"Reached prune check threshold. Checking..."); NSDate *date = [NSDate date]; self.serverCacheUpdatesSinceLastPruneCheck = 0; BOOL canPrune = YES; NSUInteger cacheSize = [self.storageEngine serverCacheEstimatedSizeInBytes]; FFDebug(@"I-RDB078002", @"Server cache size: %lu", (unsigned long)cacheSize); while (canPrune && [self.cachePolicy shouldPruneCacheWithSize:cacheSize numberOfTrackedQueries:self.trackedQueryManager.numberOfPrunableQueries]) { FPruneForest *pruneForest = [self.trackedQueryManager pruneOldQueries:self.cachePolicy]; if (pruneForest.prunesAnything) { [self.storageEngine pruneCache:pruneForest atPath:[FPath empty]]; } else { canPrune = NO; } cacheSize = [self.storageEngine serverCacheEstimatedSizeInBytes]; FFDebug(@"I-RDB078003", @"Cache size after pruning: %lu", (unsigned long)cacheSize); } FFDebug(@"I-RDB078004", @"Pruning round took %fms", [date timeIntervalSinceNow]*-1000); } } - (void)setTrackedQueryKeys:(NSSet *)keys forQuery:(FQuerySpec *)query { NSAssert(!query.loadsAllData, @"We should only track keys for filtered queries"); FTrackedQuery *trackedQuery = [self.trackedQueryManager findTrackedQuery:query]; NSAssert(trackedQuery.isActive, @"We only expect tracked keys for currently-active queries."); [self.storageEngine setTrackedQueryKeys:keys forQueryId:trackedQuery.queryId]; } - (void)updateTrackedQueryKeysWithAddedKeys:(NSSet *)added removedKeys:(NSSet *)removed forQuery:(FQuerySpec *)query { NSAssert(!query.loadsAllData, @"We should only track keys for filtered queries"); FTrackedQuery *trackedQuery = [self.trackedQueryManager findTrackedQuery:query]; NSAssert(trackedQuery.isActive, @"We only expect tracked keys for currently-active queries."); [self.storageEngine updateTrackedQueryKeysWithAddedKeys:added removedKeys:removed forQueryId:trackedQuery.queryId]; } @end