Files
firebase-ios-sdk/FirebaseDatabase/Sources/Persistence/FLevelDBStorageEngine.m
T

1002 lines
42 KiB
Objective-C
Raw Normal View History

2017-05-15 12:27:07 -07:00
/*
* 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 <Foundation/Foundation.h>
2020-06-22 07:58:52 -07:00
#import "FirebaseDatabase/Sources/Persistence/FLevelDBStorageEngine.h"
2017-05-15 12:27:07 -07:00
#import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"
2020-06-22 07:58:52 -07:00
#import "FirebaseDatabase/Sources/Core/FQueryParams.h"
#import "FirebaseDatabase/Sources/Core/FWriteRecord.h"
#import "FirebaseDatabase/Sources/Persistence/FPendingPut.h"
#import "FirebaseDatabase/Sources/Persistence/FPruneForest.h"
#import "FirebaseDatabase/Sources/Persistence/FTrackedQuery.h"
#import "FirebaseDatabase/Sources/Snapshot/FEmptyNode.h"
#import "FirebaseDatabase/Sources/Snapshot/FSnapshotUtilities.h"
#import "FirebaseDatabase/Sources/Utilities/FUtilities.h"
#import "FirebaseDatabase/Sources/third_party/Wrap-leveldb/APLevelDB.h"
2017-05-15 12:27:07 -07:00
@interface FLevelDBStorageEngine ()
2019-08-05 13:27:47 -07:00
@property(nonatomic, strong) NSString *basePath;
@property(nonatomic, strong) APLevelDB *writesDB;
@property(nonatomic, strong) APLevelDB *serverCacheDB;
2017-05-15 12:27:07 -07:00
@end
// WARNING: If you change this, you need to write a migration script
2019-08-05 13:27:47 -07:00
static NSString *const kFPersistenceVersion = @"1";
2017-05-15 12:27:07 -07:00
2019-08-05 13:27:47 -07:00
static NSString *const kFServerDBPath = @"server_data";
static NSString *const kFWritesDBPath = @"writes";
2017-05-15 12:27:07 -07:00
2019-08-05 13:27:47 -07:00
static NSString *const kFUserWriteId = @"id";
static NSString *const kFUserWritePath = @"path";
static NSString *const kFUserWriteOverwrite = @"o";
static NSString *const kFUserWriteMerge = @"m";
2017-05-15 12:27:07 -07:00
2019-08-05 13:27:47 -07:00
static NSString *const kFTrackedQueryId = @"id";
static NSString *const kFTrackedQueryPath = @"path";
static NSString *const kFTrackedQueryParams = @"p";
static NSString *const kFTrackedQueryLastUse = @"lu";
static NSString *const kFTrackedQueryIsComplete = @"c";
static NSString *const kFTrackedQueryIsActive = @"a";
2017-05-15 12:27:07 -07:00
2019-08-05 13:27:47 -07:00
static NSString *const kFServerCachePrefix = @"/server_cache/";
2017-05-15 12:27:07 -07:00
// '~' is the last non-control character in the ASCII table until 127
// We wan't the entire range of thing stored in the DB
2019-08-05 13:27:47 -07:00
static NSString *const kFServerCacheRangeEnd = @"/server_cache~";
static NSString *const kFTrackedQueriesPrefix = @"/tracked_queries/";
static NSString *const kFTrackedQueryKeysPrefix = @"/tracked_query_keys/";
2017-05-15 12:27:07 -07:00
2019-08-05 13:27:47 -07:00
// Failed to load JSON because a valid JSON turns out to be NaN while
// deserializing
2017-05-15 12:27:07 -07:00
static const NSInteger kFNanFailureCode = 3840;
2019-08-05 13:27:47 -07:00
static NSString *writeRecordKey(NSUInteger writeId) {
2017-05-15 12:27:07 -07:00
return [NSString stringWithFormat:@"%lu", (unsigned long)(writeId)];
}
2019-08-05 13:27:47 -07:00
static NSString *serverCacheKey(FPath *path) {
return [NSString stringWithFormat:@"%@%@", kFServerCachePrefix,
([path toStringWithTrailingSlash])];
2017-05-15 12:27:07 -07:00
}
2019-08-05 13:27:47 -07:00
static NSString *trackedQueryKey(NSUInteger trackedQueryId) {
return [NSString stringWithFormat:@"%@%lu", kFTrackedQueriesPrefix,
(unsigned long)trackedQueryId];
2017-05-15 12:27:07 -07:00
}
2019-08-05 13:27:47 -07:00
static NSString *trackedQueryKeysKeyPrefix(NSUInteger trackedQueryId) {
return [NSString stringWithFormat:@"%@%lu/", kFTrackedQueryKeysPrefix,
(unsigned long)trackedQueryId];
2017-05-15 12:27:07 -07:00
}
2019-08-05 13:27:47 -07:00
static NSString *trackedQueryKeysKey(NSUInteger trackedQueryId, NSString *key) {
return [NSString stringWithFormat:@"%@%lu/%@", kFTrackedQueryKeysPrefix,
(unsigned long)trackedQueryId, key];
2017-05-15 12:27:07 -07:00
}
@implementation FLevelDBStorageEngine
#pragma mark - Constructors
2019-08-05 13:27:47 -07:00
- (id)initWithPath:(NSString *)dbPath {
2017-05-15 12:27:07 -07:00
self = [super init];
if (self) {
2019-08-05 13:27:47 -07:00
self.basePath = [[FLevelDBStorageEngine firebaseDir]
stringByAppendingPathComponent:dbPath];
2017-05-15 12:27:07 -07:00
/* For reference:
serverDataDB = [aPersistence createDbByName:@"server_data"];
2019-08-05 13:27:47 -07:00
FPangolinDB *completenessDb = [aPersistence
createDbByName:@"server_complete"];
2017-05-15 12:27:07 -07:00
*/
[FLevelDBStorageEngine ensureDir:self.basePath markAsDoNotBackup:YES];
[self runMigration];
[self openDatabases];
}
return self;
}
- (void)runMigration {
2019-08-05 13:27:47 -07:00
// Currently we're at version 1, so all we need to do is write that to a
// file
NSString *versionFile =
[self.basePath stringByAppendingPathComponent:@"version"];
2017-05-15 12:27:07 -07:00
NSError *error;
2019-08-05 13:27:47 -07:00
NSString *oldVersion =
[NSString stringWithContentsOfFile:versionFile
encoding:NSUTF8StringEncoding
error:&error];
2017-05-15 12:27:07 -07:00
if (!oldVersion) {
// This is probably fine, we don't have a version file yet
2019-08-05 13:27:47 -07:00
BOOL success = [kFPersistenceVersion writeToFile:versionFile
atomically:NO
encoding:NSUTF8StringEncoding
error:&error];
2017-05-15 12:27:07 -07:00
if (!success) {
2019-08-05 13:27:47 -07:00
FFWarn(@"I-RDB076001", @"Failed to write version for database: %@",
error);
2017-05-15 12:27:07 -07:00
}
} else if ([oldVersion isEqualToString:kFPersistenceVersion]) {
// Everythings fine no need for migration
} else if ([oldVersion length] == 0) {
FFWarn(@"I-RDB076036",
@"Version file empty. Assuming database version 1.");
2017-05-15 12:27:07 -07:00
} else {
// If we add more versions in the future, we need to run migration here
2019-08-05 13:27:47 -07:00
[NSException raise:NSInternalInconsistencyException
format:@"Unrecognized database version: %@", oldVersion];
2017-05-15 12:27:07 -07:00
}
}
- (void)runLegacyMigration:(FRepoInfo *)info {
2019-08-05 13:27:47 -07:00
NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory, NSUserDomainMask, YES);
2017-05-15 12:27:07 -07:00
NSString *documentsDir = [dirPaths objectAtIndex:0];
2019-08-05 13:27:47 -07:00
NSString *firebaseDir =
[documentsDir stringByAppendingPathComponent:@"firebase"];
NSString *repoHashString =
[NSString stringWithFormat:@"%@_%@", info.host, info.namespace];
NSString *legacyBaseDir =
[NSString stringWithFormat:@"%@/1/%@/v1", firebaseDir, repoHashString];
2017-05-15 12:27:07 -07:00
if ([[NSFileManager defaultManager] fileExistsAtPath:legacyBaseDir]) {
FFWarn(@"I-RDB076002", @"Legacy database found, migrating...");
// We only need to migrate writes
NSError *error = nil;
2019-08-05 13:27:47 -07:00
APLevelDB *writes = [APLevelDB
levelDBWithPath:[legacyBaseDir stringByAppendingPathComponent:
@"outstanding_puts"]
error:&error];
2017-05-15 12:27:07 -07:00
if (writes != nil) {
__block NSUInteger numberOfWritesRestored = 0;
2019-08-05 13:27:47 -07:00
// Maybe we could use write batches, but what the heck, I'm sure
// it'll go fine :P
[writes enumerateKeysAndValuesAsData:^(NSString *key, NSData *data,
BOOL *stop) {
2020-10-09 16:27:10 -07:00
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
// Update the deprecated API when minimum iOS version is 11+.
2019-08-05 13:27:47 -07:00
id pendingPut = [NSKeyedUnarchiver unarchiveObjectWithData:data];
2020-10-09 16:27:10 -07:00
#pragma clang diagnostic pop
2019-08-05 13:27:47 -07:00
if ([pendingPut isKindOfClass:[FPendingPut class]]) {
FPendingPut *put = pendingPut;
id<FNode> newNode =
[FSnapshotUtilities nodeFrom:put.data
priority:put.priority];
[self saveUserOverwrite:newNode
atPath:put.path
writeId:[key integerValue]];
numberOfWritesRestored++;
} else if ([pendingPut
isKindOfClass:[FPendingPutPriority class]]) {
// This is for backwards compatibility. Older clients will
// save FPendingPutPriority. New ones will need to read it and
// translate.
FPendingPutPriority *putPriority = pendingPut;
FPath *priorityPath =
[putPriority.path childFromString:@".priority"];
id<FNode> newNode =
[FSnapshotUtilities nodeFrom:putPriority.priority
priority:nil];
[self saveUserOverwrite:newNode
atPath:priorityPath
writeId:[key integerValue]];
numberOfWritesRestored++;
} else if ([pendingPut isKindOfClass:[FPendingUpdate class]]) {
FPendingUpdate *update = pendingPut;
FCompoundWrite *merge = [FCompoundWrite
compoundWriteWithValueDictionary:update.data];
[self saveUserMerge:merge
atPath:update.path
writeId:[key integerValue]];
numberOfWritesRestored++;
} else {
FFWarn(@"I-RDB076003",
@"Failed to migrate legacy write, meh!");
}
2017-05-15 12:27:07 -07:00
}];
2019-08-05 13:27:47 -07:00
FFWarn(@"I-RDB076004", @"Migrated %lu writes",
(unsigned long)numberOfWritesRestored);
2017-05-15 12:27:07 -07:00
[writes close];
FFWarn(@"I-RDB076005", @"Deleting legacy database...");
2019-08-05 13:27:47 -07:00
BOOL success =
[[NSFileManager defaultManager] removeItemAtPath:legacyBaseDir
error:&error];
2017-05-15 12:27:07 -07:00
if (!success) {
2019-08-05 13:27:47 -07:00
FFWarn(@"I-RDB076006", @"Failed to delete legacy database: %@",
error);
2017-05-15 12:27:07 -07:00
} else {
FFWarn(@"I-RDB076007", @"Finished migrating legacy database.");
}
} else {
2019-08-05 13:27:47 -07:00
FFWarn(@"I-RDB076008", @"Failed to migrate old database: %@",
error);
2017-05-15 12:27:07 -07:00
}
}
}
- (void)openDatabases {
self.serverCacheDB = [self createDB:kFServerDBPath];
self.writesDB = [self createDB:kFWritesDBPath];
}
2019-08-05 13:27:47 -07:00
- (void)purgeDatabase:(NSString *)dbPath {
NSString *path = [self.basePath stringByAppendingPathComponent:dbPath];
NSError *error;
FFWarn(@"I-RDB076009", @"Deleting database at path %@", path);
2019-08-05 13:27:47 -07:00
BOOL success = [[NSFileManager defaultManager] removeItemAtPath:path
error:&error];
if (!success) {
2019-08-05 13:27:47 -07:00
[NSException raise:NSInternalInconsistencyException
format:@"Failed to delete database files: %@", error];
}
}
2017-05-15 12:27:07 -07:00
- (void)purgeEverything {
[self close];
2019-08-05 13:27:47 -07:00
[@[ kFServerDBPath, kFWritesDBPath ]
enumerateObjectsUsingBlock:^(NSString *dbPath, NSUInteger idx,
BOOL *stop) {
[self purgeDatabase:dbPath];
}];
2017-05-15 12:27:07 -07:00
[self openDatabases];
}
- (void)close {
// autoreleasepool will cause deallocation which will close the DB
@autoreleasepool {
[self.serverCacheDB close];
self.serverCacheDB = nil;
[self.writesDB close];
self.writesDB = nil;
}
}
2019-08-05 13:27:47 -07:00
+ (NSString *)firebaseDir {
2021-03-12 14:19:16 -05:00
#if TARGET_OS_IOS || TARGET_OS_WATCH
2019-08-05 13:27:47 -07:00
NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory, NSUserDomainMask, YES);
2017-05-15 12:27:07 -07:00
NSString *documentsDir = [dirPaths objectAtIndex:0];
return [documentsDir stringByAppendingPathComponent:@"firebase"];
2021-03-12 14:19:16 -05:00
#elif TARGET_OS_TV
NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(
NSCachesDirectory, NSUserDomainMask, YES);
NSString *cachesDir = [dirPaths objectAtIndex:0];
return [cachesDir stringByAppendingPathComponent:@"firebase"];
2019-08-05 13:27:47 -07:00
#elif TARGET_OS_OSX
2017-05-15 12:27:07 -07:00
return [NSHomeDirectory() stringByAppendingPathComponent:@".firebase"];
2019-08-05 13:27:47 -07:00
#endif
2017-05-15 12:27:07 -07:00
}
- (APLevelDB *)createDB:(NSString *)dbName {
2017-05-15 12:27:07 -07:00
NSError *err = nil;
NSString *path = [self.basePath stringByAppendingPathComponent:dbName];
2017-05-15 12:27:07 -07:00
APLevelDB *db = [APLevelDB levelDBWithPath:path error:&err];
if (err) {
2019-08-05 13:27:47 -07:00
FFWarn(@"I-RDB076036",
@"Failed to read database persistence file '%@': %@", dbName,
[err localizedDescription]);
err = nil;
// Delete the database and try again.
[self purgeDatabase:dbName];
db = [APLevelDB levelDBWithPath:path error:&err];
if (err) {
2019-08-05 13:27:47 -07:00
NSString *reason = [NSString
stringWithFormat:@"Error initializing persistence: %@",
[err description]];
@throw [NSException
exceptionWithName:@"FirebaseDatabasePersistenceFailure"
reason:reason
userInfo:nil];
}
2017-05-15 12:27:07 -07:00
}
2017-05-15 12:27:07 -07:00
return db;
}
2019-08-05 13:27:47 -07:00
- (void)saveUserOverwrite:(id<FNode>)node
atPath:(FPath *)path
writeId:(NSUInteger)writeId {
NSDictionary *write = @{
kFUserWriteId : @(writeId),
kFUserWritePath : [path toStringWithTrailingSlash],
kFUserWriteOverwrite : [node valForExport:YES]
};
2017-05-15 12:27:07 -07:00
NSError *error = nil;
2019-08-05 13:27:47 -07:00
NSData *data = [NSJSONSerialization dataWithJSONObject:write
options:0
error:&error];
NSAssert(data, @"Failed to serialize user overwrite: %@, (Error: %@)",
write, error);
2017-05-15 12:27:07 -07:00
[self.writesDB setData:data forKey:writeRecordKey(writeId)];
}
2019-08-05 13:27:47 -07:00
- (void)saveUserMerge:(FCompoundWrite *)merge
atPath:(FPath *)path
writeId:(NSUInteger)writeId {
NSDictionary *write = @{
kFUserWriteId : @(writeId),
kFUserWritePath : [path toStringWithTrailingSlash],
kFUserWriteMerge : [merge valForExport:YES]
};
2017-05-15 12:27:07 -07:00
NSError *error = nil;
2019-08-05 13:27:47 -07:00
NSData *data = [NSJSONSerialization dataWithJSONObject:write
options:0
error:&error];
NSAssert(data, @"Failed to serialize user merge: %@ (Error: %@)", write,
error);
2017-05-15 12:27:07 -07:00
[self.writesDB setData:data forKey:writeRecordKey(writeId)];
}
- (void)removeUserWrite:(NSUInteger)writeId {
[self.writesDB removeKey:writeRecordKey(writeId)];
}
- (void)removeAllUserWrites {
__block NSUInteger count = 0;
NSDate *start = [NSDate date];
id<APLevelDBWriteBatch> batch = [self.writesDB beginWriteBatch];
[self.writesDB enumerateKeys:^(NSString *key, BOOL *stop) {
2019-08-05 13:27:47 -07:00
[batch removeKey:key];
count++;
2017-05-15 12:27:07 -07:00
}];
BOOL success = [batch commit];
if (!success) {
FFWarn(@"I-RDB076010", @"Failed to remove all users writes on disk!");
} else {
2019-08-05 13:27:47 -07:00
FFDebug(@"I-RDB076011", @"Removed %lu writes in %fms",
(unsigned long)count, [start timeIntervalSinceNow] * -1000);
2017-05-15 12:27:07 -07:00
}
}
- (NSArray *)userWrites {
NSDate *date = [NSDate date];
NSMutableArray *writes = [NSMutableArray array];
2019-08-05 13:27:47 -07:00
[self.writesDB enumerateKeysAndValuesAsData:^(NSString *key, NSData *data,
BOOL *stop) {
NSError *error = nil;
NSDictionary *writeJSON = [NSJSONSerialization JSONObjectWithData:data
options:0
error:&error];
if (writeJSON == nil) {
if (error.code == kFNanFailureCode) {
FFWarn(@"I-RDB076012",
@"Failed to deserialize write (%@), likely because of out "
@"of range doubles (Error: %@)",
[[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding],
error);
FFWarn(@"I-RDB076013", @"Removing failed write with key %@", key);
[self.writesDB removeKey:key];
} else {
[NSException raise:NSInternalInconsistencyException
format:@"Failed to deserialize write: %@", error];
}
} else {
NSInteger writeId =
((NSNumber *)writeJSON[kFUserWriteId]).integerValue;
FPath *path = [FPath pathWithString:writeJSON[kFUserWritePath]];
FWriteRecord *writeRecord;
if (writeJSON[kFUserWriteMerge] != nil) {
// It's a merge
FCompoundWrite *merge = [FCompoundWrite
compoundWriteWithValueDictionary:writeJSON[kFUserWriteMerge]];
writeRecord = [[FWriteRecord alloc] initWithPath:path
merge:merge
writeId:writeId];
} else {
// It's an overwrite
NSAssert(writeJSON[kFUserWriteOverwrite] != nil,
@"Persisted write did not contain merge or overwrite!");
id<FNode> node =
[FSnapshotUtilities nodeFrom:writeJSON[kFUserWriteOverwrite]];
writeRecord = [[FWriteRecord alloc] initWithPath:path
overwrite:node
writeId:writeId
visible:YES];
}
[writes addObject:writeRecord];
}
2017-05-15 12:27:07 -07:00
}];
// Make sure writes are sorted
2019-08-05 13:27:47 -07:00
[writes sortUsingComparator:^NSComparisonResult(FWriteRecord *one,
FWriteRecord *two) {
if (one.writeId < two.writeId) {
return NSOrderedAscending;
} else if (one.writeId > two.writeId) {
return NSOrderedDescending;
} else {
return NSOrderedSame;
}
2017-05-15 12:27:07 -07:00
}];
2019-08-05 13:27:47 -07:00
FFDebug(@"I-RDB076014", @"Loaded %lu writes in %fms",
(unsigned long)writes.count, [date timeIntervalSinceNow] * -1000);
2017-05-15 12:27:07 -07:00
return writes;
}
- (id<FNode>)serverCacheAtPath:(FPath *)path {
NSDate *start = [NSDate date];
id data = [self internalNestedDataForPath:path];
id<FNode> node = [FSnapshotUtilities nodeFrom:data];
2019-08-05 13:27:47 -07:00
FFDebug(@"I-RDB076015", @"Loaded node with %d children at %@ in %fms",
[node numChildren], path, [start timeIntervalSinceNow] * -1000);
2017-05-15 12:27:07 -07:00
return node;
}
- (id<FNode>)serverCacheForKeys:(NSSet *)keys atPath:(FPath *)path {
NSDate *start = [NSDate date];
__block id<FNode> node = [FEmptyNode emptyNode];
[keys enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) {
2019-08-05 13:27:47 -07:00
id data = [self internalNestedDataForPath:[path childFromString:key]];
node = [node updateImmediateChild:key
withNewChild:[FSnapshotUtilities nodeFrom:data]];
2017-05-15 12:27:07 -07:00
}];
2019-08-05 13:27:47 -07:00
FFDebug(@"I-RDB076016",
@"Loaded node with %d children for %lu keys at %@ in %fms",
[node numChildren], (unsigned long)keys.count, path,
[start timeIntervalSinceNow] * -1000);
2017-05-15 12:27:07 -07:00
return node;
}
2019-08-05 13:27:47 -07:00
- (void)updateServerCache:(id<FNode>)node
atPath:(FPath *)path
merge:(BOOL)merge {
2017-05-15 12:27:07 -07:00
NSDate *start = [NSDate date];
id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
// Remove any leaf nodes that might be higher up
[self removeAllLeafNodesOnPath:path batch:batch];
__block NSUInteger counter = 0;
if (merge) {
// remove any children that exist
2019-08-05 13:27:47 -07:00
[node enumerateChildrenUsingBlock:^(NSString *childKey,
id<FNode> childNode, BOOL *stop) {
FPath *childPath = [path childFromString:childKey];
[self removeAllWithPrefix:serverCacheKey(childPath)
batch:batch
database:self.serverCacheDB];
[self saveNodeInternal:childNode
atPath:childPath
batch:batch
counter:&counter];
2017-05-15 12:27:07 -07:00
}];
} else {
// remove everything
2019-08-05 13:27:47 -07:00
[self removeAllWithPrefix:serverCacheKey(path)
batch:batch
database:self.serverCacheDB];
2017-05-15 12:27:07 -07:00
[self saveNodeInternal:node atPath:path batch:batch counter:&counter];
}
BOOL success = [batch commit];
if (!success) {
FFWarn(@"I-RDB076017", @"Failed to update server cache on disk!");
} else {
2019-08-05 13:27:47 -07:00
FFDebug(@"I-RDB076018", @"Saved %lu leaf nodes for overwrite in %fms",
(unsigned long)counter, [start timeIntervalSinceNow] * -1000);
2017-05-15 12:27:07 -07:00
}
}
2019-08-05 13:27:47 -07:00
- (void)updateServerCacheWithMerge:(FCompoundWrite *)merge
atPath:(FPath *)path {
2017-05-15 12:27:07 -07:00
NSDate *start = [NSDate date];
__block NSUInteger counter = 0;
id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
// Remove any leaf nodes that might be higher up
[self removeAllLeafNodesOnPath:path batch:batch];
[merge enumerateWrites:^(FPath *relativePath, id<FNode> node, BOOL *stop) {
2019-08-05 13:27:47 -07:00
FPath *childPath = [path child:relativePath];
[self removeAllWithPrefix:serverCacheKey(childPath)
batch:batch
database:self.serverCacheDB];
[self saveNodeInternal:node
atPath:childPath
batch:batch
counter:&counter];
2017-05-15 12:27:07 -07:00
}];
BOOL success = [batch commit];
if (!success) {
FFWarn(@"I-RDB076019", @"Failed to update server cache on disk!");
} else {
2019-08-05 13:27:47 -07:00
FFDebug(@"I-RDB076020", @"Saved %lu leaf nodes for merge in %fms",
(unsigned long)counter, [start timeIntervalSinceNow] * -1000);
2017-05-15 12:27:07 -07:00
}
}
2019-08-05 13:27:47 -07:00
- (void)saveNodeInternal:(id<FNode>)node
atPath:(FPath *)path
batch:(id<APLevelDBWriteBatch>)batch
counter:(NSUInteger *)counter {
2017-05-15 12:27:07 -07:00
id data = [node valForExport:YES];
2019-08-05 13:27:47 -07:00
if (data != nil && ![data isKindOfClass:[NSNull class]]) {
[self internalSetNestedData:data
forKey:serverCacheKey(path)
withBatch:batch
counter:counter];
2017-05-15 12:27:07 -07:00
}
}
- (NSUInteger)serverCacheEstimatedSizeInBytes {
2019-08-05 13:27:47 -07:00
// Use the exact size, because for pruning the approximate size can lead to
// weird situations where we prune everything because no compaction is ever
// run
return [self.serverCacheDB exactSizeFrom:kFServerCachePrefix
to:kFServerCacheRangeEnd];
2017-05-15 12:27:07 -07:00
}
- (void)pruneCache:(FPruneForest *)pruneForest atPath:(FPath *)path {
// TODO: be more intelligent, don't scan entire database...
__block NSUInteger pruned = 0;
__block NSUInteger kept = 0;
NSDate *start = [NSDate date];
NSString *prefix = serverCacheKey(path);
id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
2019-08-05 13:27:47 -07:00
[self.serverCacheDB
enumerateKeysWithPrefix:prefix
usingBlock:^(NSString *dbKey, BOOL *stop) {
NSString *pathStr =
[dbKey substringFromIndex:prefix.length];
FPath *relativePath = [[FPath alloc] initWith:pathStr];
if ([pruneForest shouldPruneUnkeptDescendantsAtPath:
relativePath]) {
pruned++;
[batch removeKey:dbKey];
} else {
kept++;
}
}];
2017-05-15 12:27:07 -07:00
BOOL success = [batch commit];
if (!success) {
FFWarn(@"I-RDB076021", @"Failed to prune cache on disk!");
} else {
2019-08-05 13:27:47 -07:00
FFDebug(@"I-RDB076022", @"Pruned %lu paths, kept %lu paths in %fms",
(unsigned long)pruned, (unsigned long)kept,
[start timeIntervalSinceNow] * -1000);
2017-05-15 12:27:07 -07:00
}
}
#pragma mark - Tracked Queries
- (NSArray *)loadTrackedQueries {
NSDate *date = [NSDate date];
NSMutableArray *trackedQueries = [NSMutableArray array];
2019-08-05 13:27:47 -07:00
[self.serverCacheDB
enumerateKeysWithPrefix:kFTrackedQueriesPrefix
asData:^(NSString *key, NSData *data, BOOL *stop) {
NSError *error = nil;
NSDictionary *queryJSON =
[NSJSONSerialization JSONObjectWithData:data
options:0
error:&error];
if (queryJSON == nil) {
if (error.code == kFNanFailureCode) {
FFWarn(
@"I-RDB076023",
@"Failed to deserialize tracked query "
@"(%@), likely because of out of range "
@"doubles (Error: %@)",
[[NSString alloc]
initWithData:data
encoding:NSUTF8StringEncoding],
error);
FFWarn(@"I-RDB076024",
@"Removing failed tracked query with "
@"key %@",
key);
[self.serverCacheDB removeKey:key];
} else {
[NSException
raise:NSInternalInconsistencyException
format:@"Failed to deserialize tracked "
@"query: %@",
error];
}
} else {
NSUInteger queryId =
((NSNumber *)queryJSON[kFTrackedQueryId])
.unsignedIntegerValue;
FPath *path =
[FPath pathWithString:
queryJSON[kFTrackedQueryPath]];
FQueryParams *params = [FQueryParams
fromQueryObject:queryJSON
[kFTrackedQueryParams]];
FQuerySpec *query =
[[FQuerySpec alloc] initWithPath:path
params:params];
BOOL isComplete =
[queryJSON[kFTrackedQueryIsComplete]
boolValue];
BOOL isActive =
[queryJSON[kFTrackedQueryIsActive]
boolValue];
NSTimeInterval lastUse =
[queryJSON[kFTrackedQueryLastUse]
doubleValue];
2017-05-15 12:27:07 -07:00
2019-08-05 13:27:47 -07:00
FTrackedQuery *trackedQuery =
[[FTrackedQuery alloc]
initWithId:queryId
query:query
lastUse:lastUse
isActive:isActive
isComplete:isComplete];
2017-05-15 12:27:07 -07:00
2019-08-05 13:27:47 -07:00
[trackedQueries addObject:trackedQuery];
}
}];
FFDebug(@"I-RDB076025", @"Loaded %lu tracked queries in %fms",
(unsigned long)trackedQueries.count,
[date timeIntervalSinceNow] * -1000);
2017-05-15 12:27:07 -07:00
return trackedQueries;
}
- (void)removeTrackedQuery:(NSUInteger)queryId {
NSDate *start = [NSDate date];
id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
[batch removeKey:trackedQueryKey(queryId)];
__block NSUInteger keyCount = 0;
2019-08-05 13:27:47 -07:00
[self.serverCacheDB
enumerateKeysWithPrefix:trackedQueryKeysKeyPrefix(queryId)
usingBlock:^(NSString *key, BOOL *stop) {
[batch removeKey:key];
keyCount++;
}];
2017-05-15 12:27:07 -07:00
BOOL success = [batch commit];
if (!success) {
FFWarn(@"I-RDB076026", @"Failed to remove tracked query on disk!");
} else {
2019-08-05 13:27:47 -07:00
FFDebug(@"I-RDB076027",
@"Removed query with id %lu (and removed %lu keys) in %fms",
(unsigned long)queryId, (unsigned long)keyCount,
[start timeIntervalSinceNow] * -1000);
2017-05-15 12:27:07 -07:00
}
}
- (void)saveTrackedQuery:(FTrackedQuery *)query {
NSDate *start = [NSDate date];
2019-08-05 13:27:47 -07:00
NSDictionary *trackedQuery = @{
kFTrackedQueryId : @(query.queryId),
kFTrackedQueryPath : [query.query.path toStringWithTrailingSlash],
kFTrackedQueryParams : [query.query.params wireProtocolParams],
kFTrackedQueryLastUse : @(query.lastUse),
kFTrackedQueryIsComplete : @(query.isComplete),
kFTrackedQueryIsActive : @(query.isActive)
2017-05-15 12:27:07 -07:00
};
NSError *error = nil;
2019-08-05 13:27:47 -07:00
NSData *data = [NSJSONSerialization dataWithJSONObject:trackedQuery
options:0
error:&error];
2017-05-15 12:27:07 -07:00
NSAssert(data, @"Failed to serialize tracked query (Error: %@)", error);
[self.serverCacheDB setData:data forKey:trackedQueryKey(query.queryId)];
2019-08-05 13:27:47 -07:00
FFDebug(@"I-RDB076028", @"Saved tracked query %lu in %fms",
(unsigned long)query.queryId, [start timeIntervalSinceNow] * -1000);
2017-05-15 12:27:07 -07:00
}
- (void)setTrackedQueryKeys:(NSSet *)keys forQueryId:(NSUInteger)queryId {
NSDate *start = [NSDate date];
__block NSUInteger removed = 0;
__block NSUInteger added = 0;
id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
NSMutableSet *seenKeys = [NSMutableSet set];
2019-08-05 13:27:47 -07:00
// First, delete any keys that might be stored and are not part of the
// current keys
[self.serverCacheDB
enumerateKeysWithPrefix:trackedQueryKeysKeyPrefix(queryId)
asStrings:^(NSString *dbKey, NSString *actualKey,
BOOL *stop) {
if ([keys containsObject:actualKey]) {
// Already in DB
[seenKeys addObject:actualKey];
} else {
// Not part of set, delete key
[batch removeKey:dbKey];
removed++;
}
}];
2017-05-15 12:27:07 -07:00
// Next add any keys that are missing in the database
[keys enumerateObjectsUsingBlock:^(NSString *childKey, BOOL *stop) {
2019-08-05 13:27:47 -07:00
if (![seenKeys containsObject:childKey]) {
[batch setString:childKey
forKey:trackedQueryKeysKey(queryId, childKey)];
added++;
}
2017-05-15 12:27:07 -07:00
}];
BOOL success = [batch commit];
if (!success) {
FFWarn(@"I-RDB076029", @"Failed to set tracked queries on disk!");
} else {
2019-08-05 13:27:47 -07:00
FFDebug(@"I-RDB076030",
@"Set %lu tracked keys (%lu added, %lu removed) for query %lu "
@"in %fms",
(unsigned long)keys.count, (unsigned long)added,
(unsigned long)removed, (unsigned long)queryId,
[start timeIntervalSinceNow] * -1000);
2017-05-15 12:27:07 -07:00
}
}
2019-08-05 13:27:47 -07:00
- (void)updateTrackedQueryKeysWithAddedKeys:(NSSet *)added
removedKeys:(NSSet *)removed
forQueryId:(NSUInteger)queryId {
2017-05-15 12:27:07 -07:00
NSDate *start = [NSDate date];
id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
[removed enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) {
2019-08-05 13:27:47 -07:00
[batch removeKey:trackedQueryKeysKey(queryId, key)];
2017-05-15 12:27:07 -07:00
}];
[added enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) {
2019-08-05 13:27:47 -07:00
[batch setString:key forKey:trackedQueryKeysKey(queryId, key)];
2017-05-15 12:27:07 -07:00
}];
BOOL success = [batch commit];
if (!success) {
FFWarn(@"I-RDB076031", @"Failed to update tracked queries on disk!");
} else {
2019-08-05 13:27:47 -07:00
FFDebug(@"I-RDB076032",
@"Added %lu tracked keys, removed %lu for query %lu in %fms",
(unsigned long)added.count, (unsigned long)removed.count,
(unsigned long)queryId, [start timeIntervalSinceNow] * -1000);
2017-05-15 12:27:07 -07:00
}
}
- (NSSet *)trackedQueryKeysForQuery:(NSUInteger)queryId {
NSDate *start = [NSDate date];
NSMutableSet *set = [NSMutableSet set];
2019-08-05 13:27:47 -07:00
[self.serverCacheDB
enumerateKeysWithPrefix:trackedQueryKeysKeyPrefix(queryId)
asStrings:^(NSString *dbKey, NSString *actualKey,
BOOL *stop) {
[set addObject:actualKey];
}];
FFDebug(@"I-RDB076033", @"Loaded %lu tracked keys for query %lu in %fms",
(unsigned long)set.count, (unsigned long)queryId,
[start timeIntervalSinceNow] * -1000);
2017-05-15 12:27:07 -07:00
return set;
}
#pragma mark - Internal methods
2019-08-05 13:27:47 -07:00
- (void)removeAllLeafNodesOnPath:(FPath *)path
batch:(id<APLevelDBWriteBatch>)batch {
2017-05-15 12:27:07 -07:00
while (!path.isEmpty) {
[batch removeKey:serverCacheKey(path)];
path = [path parent];
}
// Make sure to delete any nodes at the root
[batch removeKey:serverCacheKey([FPath empty])];
}
2019-08-05 13:27:47 -07:00
- (void)removeAllWithPrefix:(NSString *)prefix
batch:(id<APLevelDBWriteBatch>)batch
database:(APLevelDB *)database {
2017-05-15 12:27:07 -07:00
assert(prefix != nil);
2019-08-05 13:27:47 -07:00
[database enumerateKeysWithPrefix:prefix
usingBlock:^(NSString *key, BOOL *stop) {
[batch removeKey:key];
}];
2017-05-15 12:27:07 -07:00
}
#pragma mark - Internal helper methods
2019-08-05 13:27:47 -07:00
- (void)internalSetNestedData:(id)value
forKey:(NSString *)key
withBatch:(id<APLevelDBWriteBatch>)batch
counter:(NSUInteger *)counter {
if ([value isKindOfClass:[NSDictionary class]]) {
NSDictionary *dictionary = value;
[dictionary enumerateKeysAndObjectsUsingBlock:^(id childKey, id obj,
BOOL *stop) {
assert(obj != nil);
NSString *childPath =
[NSString stringWithFormat:@"%@%@/", key, childKey];
[self internalSetNestedData:obj
forKey:childPath
withBatch:batch
counter:counter];
2017-05-15 12:27:07 -07:00
}];
2019-08-05 13:27:47 -07:00
} else {
2017-05-15 12:27:07 -07:00
NSData *data = [self serializePrimitive:value];
[batch setData:data forKey:key];
(*counter)++;
}
}
- (id)internalNestedDataForPath:(FPath *)path {
NSAssert(path != nil, @"Path was nil!");
NSString *baseKey = serverCacheKey(path);
2019-08-05 13:27:47 -07:00
// HACK to make sure iter is freed now to avoid race conditions (if self.db
// is deleted before iter, you get an access violation).
2017-05-15 12:27:07 -07:00
@autoreleasepool {
2019-08-05 13:27:47 -07:00
APLevelDBIterator *iter =
[APLevelDBIterator iteratorWithLevelDB:self.serverCacheDB];
2017-05-15 12:27:07 -07:00
[iter seekToKey:baseKey];
if (iter.key == nil || ![iter.key hasPrefix:baseKey]) {
// No data.
return nil;
} else {
2019-08-05 13:27:47 -07:00
return [self internalNestedDataFromIterator:iter
andKeyPrefix:baseKey];
2017-05-15 12:27:07 -07:00
}
}
}
2019-08-05 13:27:47 -07:00
- (id)internalNestedDataFromIterator:(APLevelDBIterator *)iterator
andKeyPrefix:(NSString *)prefix {
NSString *key = iterator.key;
2017-05-15 12:27:07 -07:00
if ([key isEqualToString:prefix]) {
id result = [self deserializePrimitive:iterator.valueAsData];
[iterator nextKey];
return result;
} else {
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
while (key != nil && [key hasPrefix:prefix]) {
NSString *relativePath = [key substringFromIndex:prefix.length];
2019-08-05 13:27:47 -07:00
NSArray *pathPieces =
[relativePath componentsSeparatedByString:@"/"];
2017-05-15 12:27:07 -07:00
assert(pathPieces.count > 0);
NSString *childName = pathPieces[0];
2019-08-05 13:27:47 -07:00
NSString *childPath =
[NSString stringWithFormat:@"%@%@/", prefix, childName];
id childValue = [self internalNestedDataFromIterator:iterator
andKeyPrefix:childPath];
2017-05-15 12:27:07 -07:00
[dict setValue:childValue forKey:childName];
key = iterator.key;
}
return dict;
}
}
2019-08-05 13:27:47 -07:00
- (NSData *)serializePrimitive:(id)value {
// HACK: The built-in serialization only works on dicts and arrays. So we
// create an array and then strip off the leading / trailing byte (the [ and
// ]).
2017-05-15 12:27:07 -07:00
NSError *error = nil;
2019-08-05 13:27:47 -07:00
NSData *data = [NSJSONSerialization dataWithJSONObject:@[ value ]
options:0
error:&error];
2017-05-15 12:27:07 -07:00
NSAssert(data, @"Failed to serialize primitive: %@", error);
return [data subdataWithRange:NSMakeRange(1, data.length - 2)];
}
2019-08-05 13:27:47 -07:00
- (id)fixDoubleParsing:(id)value
__attribute__((no_sanitize("float-cast-overflow"))) {
if ([value isKindOfClass:[NSDecimalNumber class]]) {
// In case the value is an NSDecimalNumber, we may be dealing with
// precisions that are higher than what can be represented in a double.
// In this case it does not suffice to check for integral numbers by
// casting the [value doubleValue] to an int64_t, because this will
// cause the compared values to be rounded to double precision.
// Coupled with a bug in [NSDecimalNumber longLongValue] that triggers
// when converting values with high precision, this would cause
// values of high precision, but with an integral 'doubleValue'
// representation to be converted to bogus values.
// A radar for the NSDecimalNumber issue can be found here:
// http://www.openradar.me/radar?id=5007005597040640
// Consider the NSDecimalNumber value: 999.9999999999999487
// This number has a 'doubleValue' of 1000. Using the previous version
// of this method would cause the value to be interpreted to be integral
// and then the resulting value would be based on the longLongValue
// which due to the NSDecimalNumber issue would turn out as -844.
// By using NSDecimal logic to test for integral values,
// 999.9999999999999487 will not be considered integral, and instead
// of triggering the 'longLongValue' issue, it will be returned as
// the 'doubleValue' representation (1000).
// Please note, that even without the NSDecimalNumber issue, the
// 'correct' longLongValue of 999.9999999999999487 is 999 and not 1000,
// so the previous code would cause issues even without the bug
// referenced in the radar.
NSDecimal original = [(NSDecimalNumber *)value decimalValue];
NSDecimal rounded;
NSDecimalRound(&rounded, &original, 0, NSRoundPlain);
if (NSDecimalCompare(&original, &rounded) != NSOrderedSame) {
NSString *doubleString = [value stringValue];
return [NSNumber numberWithDouble:[doubleString doubleValue]];
} else {
return [NSNumber numberWithLongLong:[value longLongValue]];
}
} else if ([value isKindOfClass:[NSNumber class]]) {
// The parser for double values in JSONSerialization at the root takes
// some short-cuts and delivers wrong results (wrong rounding) for some
// double values, including 2.47. Because we use the exact bytes for
// hashing on the server this will lead to hash mismatches. The parser
// of NSNumber seems to be more in line with what the server expects, so
// we use that here
2017-05-15 12:27:07 -07:00
CFNumberType type = CFNumberGetType((CFNumberRef)value);
if (type == kCFNumberDoubleType || type == kCFNumberFloatType) {
2019-08-05 13:27:47 -07:00
// The NSJSON parser returns all numbers as double values, even
// those that contain no exponent. To make sure that the String
// conversion below doesn't unexpectedly reduce precision, we make
// sure that our number is indeed not an integer.
if ((double)(int64_t)[value doubleValue] != [value doubleValue]) {
2017-05-15 12:27:07 -07:00
NSString *doubleString = [value stringValue];
return [NSNumber numberWithDouble:[doubleString doubleValue]];
2017-06-27 10:04:47 -07:00
} else {
return [NSNumber numberWithLongLong:[value longLongValue]];
2017-05-15 12:27:07 -07:00
}
}
}
return value;
}
2019-08-05 13:27:47 -07:00
- (id)deserializePrimitive:(NSData *)data {
2017-05-15 12:27:07 -07:00
NSError *error = nil;
2019-08-05 13:27:47 -07:00
id result =
[NSJSONSerialization JSONObjectWithData:data
options:NSJSONReadingAllowFragments
error:&error];
2017-05-15 12:27:07 -07:00
if (result != nil) {
return [self fixDoubleParsing:result];
} else {
if (error.code == kFNanFailureCode) {
2019-08-05 13:27:47 -07:00
FFWarn(@"I-RDB076034",
@"Failed to load primitive %@, likely because doubles where "
@"out of range (Error: %@)",
[[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding],
error);
2017-05-15 12:27:07 -07:00
return [NSNull null];
} else {
2019-08-05 13:27:47 -07:00
[NSException raise:NSInternalInconsistencyException
format:@"Failed to deserialiaze primitive: %@", error];
2017-05-15 12:27:07 -07:00
return nil;
}
}
}
2019-08-05 13:27:47 -07:00
+ (void)ensureDir:(NSString *)path markAsDoNotBackup:(BOOL)markAsDoNotBackup {
NSError *error;
BOOL success =
[[NSFileManager defaultManager] createDirectoryAtPath:path
withIntermediateDirectories:YES
attributes:nil
error:&error];
2017-05-15 12:27:07 -07:00
if (!success) {
2019-08-05 13:27:47 -07:00
@throw [NSException
exceptionWithName:@"FailedToCreatePersistenceDir"
reason:@"Failed to create persistence directory."
userInfo:@{@"path" : path}];
2017-05-15 12:27:07 -07:00
}
if (markAsDoNotBackup) {
NSURL *firebaseDirURL = [NSURL fileURLWithPath:path];
success = [firebaseDirURL setResourceValue:@YES
forKey:NSURLIsExcludedFromBackupKey
error:&error];
if (!success) {
2019-08-05 13:27:47 -07:00
FFWarn(
@"I-RDB076035",
@"Failed to mark firebase database folder as do not backup: %@",
error);
[NSException raise:@"Error marking as do not backup"
format:@"Failed to mark folder %@ as do not backup",
firebaseDirURL];
2017-05-15 12:27:07 -07:00
}
}
}
@end