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.
|
|
|
|
|
*/
|
|
|
|
|
|
2017-06-06 17:00:20 -07:00
|
|
|
#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
|
|
|
|
2022-03-22 13:36:33 -04:00
|
|
|
#import "FirebaseCore/Extension/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
|
2020-06-02 10:30:47 -07:00
|
|
|
} 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) {
|
2023-06-26 13:57:47 -04:00
|
|
|
NSError *error;
|
|
|
|
|
id pendingPut = [NSKeyedUnarchiver
|
|
|
|
|
unarchivedObjectOfClasses:
|
|
|
|
|
[NSSet setWithObjects:[FPendingPut class],
|
|
|
|
|
[FPendingPutPriority class],
|
|
|
|
|
[FPendingUpdate class], nil]
|
|
|
|
|
fromData:data
|
|
|
|
|
error:&error];
|
|
|
|
|
if (error) {
|
|
|
|
|
FFWarn(@"I-RDB076003", @"Failed to migrate legacy write: %@",
|
|
|
|
|
error);
|
|
|
|
|
} else if ([pendingPut isKindOfClass:[FPendingPut class]]) {
|
2019-08-05 13:27:47 -07:00
|
|
|
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",
|
2023-06-26 13:57:47 -04:00
|
|
|
@"Failed to migrate legacy write: unrecognized class "
|
|
|
|
|
@"\"%@\"",
|
|
|
|
|
[pendingPut class]);
|
2019-08-05 13:27:47 -07:00
|
|
|
}
|
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 {
|
2017-07-26 13:50:34 -07:00
|
|
|
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];
|
2017-07-26 13:50:34 -07:00
|
|
|
if (!success) {
|
2019-08-05 13:27:47 -07:00
|
|
|
[NSException raise:NSInternalInconsistencyException
|
|
|
|
|
format:@"Failed to delete database files: %@", error];
|
2017-07-26 13:50:34 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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 {
|
2024-05-10 16:18:00 -04:00
|
|
|
#if TARGET_OS_IOS || TARGET_OS_WATCH || TARGET_OS_VISION
|
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
|
|
|
}
|
|
|
|
|
|
2017-07-26 13:50:34 -07:00
|
|
|
- (APLevelDB *)createDB:(NSString *)dbName {
|
2017-05-15 12:27:07 -07:00
|
|
|
NSError *err = nil;
|
2017-07-26 13:50:34 -07:00
|
|
|
NSString *path = [self.basePath stringByAppendingPathComponent:dbName];
|
2017-05-15 12:27:07 -07:00
|
|
|
APLevelDB *db = [APLevelDB levelDBWithPath:path error:&err];
|
2017-07-26 13:50:34 -07:00
|
|
|
|
|
|
|
|
if (err) {
|
2019-08-05 13:27:47 -07:00
|
|
|
FFWarn(@"I-RDB076036",
|
|
|
|
|
@"Failed to read database persistence file '%@': %@", dbName,
|
|
|
|
|
[err localizedDescription]);
|
2017-07-26 13:50:34 -07:00
|
|
|
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-07-26 13:50:34 -07:00
|
|
|
}
|
2017-05-15 12:27:07 -07:00
|
|
|
}
|
2017-07-26 13:50:34 -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"))) {
|
2019-10-18 19:56:10 +02:00
|
|
|
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.
|
2018-06-25 10:49:10 -07:00
|
|
|
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 {
|
2018-02-13 14:04:25 -05:00
|
|
|
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
|