FLevelDBStorageEngine.m 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717
  1. /*
  2. * Copyright 2017 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import "FLevelDBStorageEngine.h"
  17. #import "APLevelDB.h"
  18. #import "FSnapshotUtilities.h"
  19. #import "FWriteRecord.h"
  20. #import "FTrackedQuery.h"
  21. #import "FQueryParams.h"
  22. #import "FEmptyNode.h"
  23. #import "FPruneForest.h"
  24. #import "FUtilities.h"
  25. #import "FPendingPut.h" // For legacy migration
  26. @interface FLevelDBStorageEngine ()
  27. @property (nonatomic, strong) NSString *basePath;
  28. @property (nonatomic, strong) APLevelDB *writesDB;
  29. @property (nonatomic, strong) APLevelDB *serverCacheDB;
  30. @end
  31. // WARNING: If you change this, you need to write a migration script
  32. static NSString * const kFPersistenceVersion = @"1";
  33. static NSString * const kFServerDBPath = @"server_data";
  34. static NSString * const kFWritesDBPath = @"writes";
  35. static NSString * const kFUserWriteId = @"id";
  36. static NSString * const kFUserWritePath = @"path";
  37. static NSString * const kFUserWriteOverwrite = @"o";
  38. static NSString * const kFUserWriteMerge = @"m";
  39. static NSString * const kFTrackedQueryId = @"id";
  40. static NSString * const kFTrackedQueryPath = @"path";
  41. static NSString * const kFTrackedQueryParams = @"p";
  42. static NSString * const kFTrackedQueryLastUse = @"lu";
  43. static NSString * const kFTrackedQueryIsComplete = @"c";
  44. static NSString * const kFTrackedQueryIsActive = @"a";
  45. static NSString * const kFServerCachePrefix = @"/server_cache/";
  46. // '~' is the last non-control character in the ASCII table until 127
  47. // We wan't the entire range of thing stored in the DB
  48. static NSString * const kFServerCacheRangeEnd = @"/server_cache~";
  49. static NSString * const kFTrackedQueriesPrefix = @"/tracked_queries/";
  50. static NSString * const kFTrackedQueryKeysPrefix = @"/tracked_query_keys/";
  51. // Failed to load JSON because a valid JSON turns out to be NaN while deserializing
  52. static const NSInteger kFNanFailureCode = 3840;
  53. static NSString* writeRecordKey(NSUInteger writeId) {
  54. return [NSString stringWithFormat:@"%lu", (unsigned long)(writeId)];
  55. }
  56. static NSString* serverCacheKey(FPath *path) {
  57. return [NSString stringWithFormat:@"%@%@", kFServerCachePrefix, ([path toStringWithTrailingSlash])];
  58. }
  59. static NSString* trackedQueryKey(NSUInteger trackedQueryId) {
  60. return [NSString stringWithFormat:@"%@%lu", kFTrackedQueriesPrefix, (unsigned long)trackedQueryId];
  61. }
  62. static NSString* trackedQueryKeysKeyPrefix(NSUInteger trackedQueryId) {
  63. return [NSString stringWithFormat:@"%@%lu/", kFTrackedQueryKeysPrefix, (unsigned long)trackedQueryId];
  64. }
  65. static NSString* trackedQueryKeysKey(NSUInteger trackedQueryId, NSString *key) {
  66. return [NSString stringWithFormat:@"%@%lu/%@", kFTrackedQueryKeysPrefix, (unsigned long)trackedQueryId, key];
  67. }
  68. @implementation FLevelDBStorageEngine
  69. #pragma mark - Constructors
  70. - (id)initWithPath:(NSString*)dbPath
  71. {
  72. self = [super init];
  73. if (self) {
  74. self.basePath = [[FLevelDBStorageEngine firebaseDir] stringByAppendingPathComponent:dbPath];
  75. /* For reference:
  76. serverDataDB = [aPersistence createDbByName:@"server_data"];
  77. FPangolinDB *completenessDb = [aPersistence createDbByName:@"server_complete"];
  78. */
  79. [FLevelDBStorageEngine ensureDir:self.basePath markAsDoNotBackup:YES];
  80. [self runMigration];
  81. [self openDatabases];
  82. }
  83. return self;
  84. }
  85. - (void)runMigration {
  86. // Currently we're at version 1, so all we need to do is write that to a file
  87. NSString *versionFile = [self.basePath stringByAppendingPathComponent:@"version"];
  88. NSError *error;
  89. NSString *oldVersion = [NSString stringWithContentsOfFile:versionFile encoding:NSUTF8StringEncoding error:&error];
  90. if (!oldVersion) {
  91. // This is probably fine, we don't have a version file yet
  92. BOOL success = [kFPersistenceVersion writeToFile:versionFile atomically:NO encoding:NSUTF8StringEncoding error:&error];
  93. if (!success) {
  94. FFWarn(@"I-RDB076001", @"Failed to write version for database: %@", error);
  95. }
  96. } else if ([oldVersion isEqualToString:kFPersistenceVersion]) {
  97. // Everythings fine no need for migration
  98. } else {
  99. // If we add more versions in the future, we need to run migration here
  100. [NSException raise:NSInternalInconsistencyException format:@"Unrecognized database version: %@", oldVersion];
  101. }
  102. }
  103. - (void)runLegacyMigration:(FRepoInfo *)info {
  104. NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
  105. NSString *documentsDir = [dirPaths objectAtIndex:0];
  106. NSString *firebaseDir = [documentsDir stringByAppendingPathComponent:@"firebase"];
  107. NSString* repoHashString = [NSString stringWithFormat:@"%@_%@", info.host, info.namespace];
  108. NSString *legacyBaseDir = [NSString stringWithFormat:@"%@/1/%@/v1", firebaseDir, repoHashString];
  109. if ([[NSFileManager defaultManager] fileExistsAtPath:legacyBaseDir]) {
  110. FFWarn(@"I-RDB076002", @"Legacy database found, migrating...");
  111. // We only need to migrate writes
  112. NSError *error = nil;
  113. APLevelDB *writes = [APLevelDB levelDBWithPath:[legacyBaseDir stringByAppendingPathComponent:@"outstanding_puts"] error:&error];
  114. if (writes != nil) {
  115. __block NSUInteger numberOfWritesRestored = 0;
  116. // Maybe we could use write batches, but what the heck, I'm sure it'll go fine :P
  117. [writes enumerateKeysAndValuesAsData:^(NSString *key, NSData *data, BOOL *stop) {
  118. id pendingPut = [NSKeyedUnarchiver unarchiveObjectWithData:data];
  119. if ([pendingPut isKindOfClass:[FPendingPut class]]) {
  120. FPendingPut *put = pendingPut;
  121. id<FNode> newNode = [FSnapshotUtilities nodeFrom:put.data priority:put.priority];
  122. [self saveUserOverwrite:newNode atPath:put.path writeId:[key integerValue]];
  123. numberOfWritesRestored++;
  124. } else if ([pendingPut isKindOfClass:[FPendingPutPriority class]]) {
  125. // This is for backwards compatibility. Older clients will save FPendingPutPriority. New ones will need to read it and translate.
  126. FPendingPutPriority *putPriority = pendingPut;
  127. FPath *priorityPath = [putPriority.path childFromString:@".priority"];
  128. id<FNode> newNode = [FSnapshotUtilities nodeFrom:putPriority.priority priority:nil];
  129. [self saveUserOverwrite:newNode atPath:priorityPath writeId:[key integerValue]];
  130. numberOfWritesRestored++;
  131. } else if ([pendingPut isKindOfClass:[FPendingUpdate class]]) {
  132. FPendingUpdate *update = pendingPut;
  133. FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:update.data];
  134. [self saveUserMerge:merge atPath:update.path writeId:[key integerValue]];
  135. numberOfWritesRestored++;
  136. } else {
  137. FFWarn(@"I-RDB076003", @"Failed to migrate legacy write, meh!");
  138. }
  139. }];
  140. FFWarn(@"I-RDB076004", @"Migrated %lu writes", (unsigned long)numberOfWritesRestored);
  141. [writes close];
  142. FFWarn(@"I-RDB076005", @"Deleting legacy database...");
  143. BOOL success = [[NSFileManager defaultManager] removeItemAtPath:legacyBaseDir error:&error];
  144. if (!success) {
  145. FFWarn(@"I-RDB076006", @"Failed to delete legacy database: %@", error);
  146. } else {
  147. FFWarn(@"I-RDB076007", @"Finished migrating legacy database.");
  148. }
  149. } else {
  150. FFWarn(@"I-RDB076008", @"Failed to migrate old database: %@", error);
  151. }
  152. }
  153. }
  154. - (void)openDatabases {
  155. self.serverCacheDB = [self createDB:kFServerDBPath];
  156. self.writesDB = [self createDB:kFWritesDBPath];
  157. }
  158. - (void)purgeEverything {
  159. [self close];
  160. [@[kFServerDBPath, kFWritesDBPath]
  161. enumerateObjectsUsingBlock:^(NSString *dbPath, NSUInteger idx, BOOL *stop) {
  162. NSString *path = [self.basePath stringByAppendingPathComponent:dbPath];
  163. NSError *error;
  164. FFDebug(@"I-RDB076009", @"Deleting database at path %@", path);
  165. BOOL success = [[NSFileManager defaultManager] removeItemAtPath:path error:&error];
  166. if (!success) {
  167. [NSException raise:NSInternalInconsistencyException format:@"Failed to delete database files: %@", error];
  168. }
  169. }];
  170. [self openDatabases];
  171. }
  172. - (void)close {
  173. // autoreleasepool will cause deallocation which will close the DB
  174. @autoreleasepool {
  175. [self.serverCacheDB close];
  176. self.serverCacheDB = nil;
  177. [self.writesDB close];
  178. self.writesDB = nil;
  179. }
  180. }
  181. + (NSString *) firebaseDir {
  182. #if TARGET_OS_IPHONE
  183. NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
  184. NSString *documentsDir = [dirPaths objectAtIndex:0];
  185. return [documentsDir stringByAppendingPathComponent:@"firebase"];
  186. #else // this must be OSX then
  187. return [NSHomeDirectory() stringByAppendingPathComponent:@".firebase"];
  188. #endif
  189. }
  190. - (APLevelDB *)createDB:(NSString *)name {
  191. NSError *err = nil;
  192. NSString *path = [self.basePath stringByAppendingPathComponent:name];
  193. APLevelDB *db = [APLevelDB levelDBWithPath:path error:&err];
  194. if(err) {
  195. NSString *reason = [NSString stringWithFormat:@"Error initializing persistence: %@", [err description]];
  196. @throw [NSException exceptionWithName:@"FirebaseDatabasePersistenceFailure" reason:reason userInfo:nil];
  197. }
  198. return db;
  199. }
  200. - (void)saveUserOverwrite:(id<FNode>)node atPath:(FPath *)path writeId:(NSUInteger)writeId {
  201. NSDictionary *write =
  202. @{ kFUserWriteId: @(writeId),
  203. kFUserWritePath: [path toStringWithTrailingSlash],
  204. kFUserWriteOverwrite: [node valForExport:YES] };
  205. NSError *error = nil;
  206. NSData *data = [NSJSONSerialization dataWithJSONObject:write options:0 error:&error];
  207. NSAssert(data, @"Failed to serialize user overwrite: %@, (Error: %@)", write, error);
  208. [self.writesDB setData:data forKey:writeRecordKey(writeId)];
  209. }
  210. - (void)saveUserMerge:(FCompoundWrite *)merge atPath:(FPath *)path writeId:(NSUInteger)writeId {
  211. NSDictionary *write =
  212. @{ kFUserWriteId: @(writeId),
  213. kFUserWritePath: [path toStringWithTrailingSlash],
  214. kFUserWriteMerge: [merge valForExport:YES] };
  215. NSError *error = nil;
  216. NSData *data = [NSJSONSerialization dataWithJSONObject:write options:0 error:&error];
  217. NSAssert(data, @"Failed to serialize user merge: %@ (Error: %@)", write, error);
  218. [self.writesDB setData:data forKey:writeRecordKey(writeId)];
  219. }
  220. - (void)removeUserWrite:(NSUInteger)writeId {
  221. [self.writesDB removeKey:writeRecordKey(writeId)];
  222. }
  223. - (void)removeAllUserWrites {
  224. __block NSUInteger count = 0;
  225. NSDate *start = [NSDate date];
  226. id<APLevelDBWriteBatch> batch = [self.writesDB beginWriteBatch];
  227. [self.writesDB enumerateKeys:^(NSString *key, BOOL *stop) {
  228. [batch removeKey:key];
  229. count++;
  230. }];
  231. BOOL success = [batch commit];
  232. if (!success) {
  233. FFWarn(@"I-RDB076010", @"Failed to remove all users writes on disk!");
  234. } else {
  235. FFDebug(@"I-RDB076011", @"Removed %lu writes in %fms", (unsigned long)count, [start timeIntervalSinceNow]*-1000);
  236. }
  237. }
  238. - (NSArray *)userWrites {
  239. NSDate *date = [NSDate date];
  240. NSMutableArray *writes = [NSMutableArray array];
  241. [self.writesDB enumerateKeysAndValuesAsData:^(NSString *key, NSData *data, BOOL *stop) {
  242. NSError *error = nil;
  243. NSDictionary *writeJSON = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
  244. if (writeJSON == nil) {
  245. if (error.code == kFNanFailureCode) {
  246. FFWarn(@"I-RDB076012", @"Failed to deserialize write (%@), likely because of out of range doubles (Error: %@)",
  247. [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding],
  248. error);
  249. FFWarn(@"I-RDB076013", @"Removing failed write with key %@", key);
  250. [self.writesDB removeKey:key];
  251. } else {
  252. [NSException raise:NSInternalInconsistencyException format:@"Failed to deserialize write: %@", error];
  253. }
  254. } else {
  255. NSInteger writeId = ((NSNumber *)writeJSON[kFUserWriteId]).integerValue;
  256. FPath *path = [FPath pathWithString:writeJSON[kFUserWritePath]];
  257. FWriteRecord *writeRecord;
  258. if (writeJSON[kFUserWriteMerge] != nil) {
  259. // It's a merge
  260. FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:writeJSON[kFUserWriteMerge]];
  261. writeRecord = [[FWriteRecord alloc] initWithPath:path merge:merge writeId:writeId];
  262. } else {
  263. // It's an overwrite
  264. NSAssert(writeJSON[kFUserWriteOverwrite] != nil, @"Persisted write did not contain merge or overwrite!");
  265. id<FNode> node = [FSnapshotUtilities nodeFrom:writeJSON[kFUserWriteOverwrite]];
  266. writeRecord = [[FWriteRecord alloc] initWithPath:path overwrite:node writeId:writeId visible:YES];
  267. }
  268. [writes addObject:writeRecord];
  269. }
  270. }];
  271. // Make sure writes are sorted
  272. [writes sortUsingComparator:^NSComparisonResult(FWriteRecord *one, FWriteRecord *two) {
  273. if (one.writeId < two.writeId) {
  274. return NSOrderedAscending;
  275. } else if (one.writeId > two.writeId) {
  276. return NSOrderedDescending;
  277. } else {
  278. return NSOrderedSame;
  279. }
  280. }];
  281. FFDebug(@"I-RDB076014", @"Loaded %lu writes in %fms", (unsigned long)writes.count, [date timeIntervalSinceNow]*-1000);
  282. return writes;
  283. }
  284. - (id<FNode>)serverCacheAtPath:(FPath *)path {
  285. NSDate *start = [NSDate date];
  286. id data = [self internalNestedDataForPath:path];
  287. id<FNode> node = [FSnapshotUtilities nodeFrom:data];
  288. FFDebug(@"I-RDB076015", @"Loaded node with %d children at %@ in %fms", [node numChildren], path, [start timeIntervalSinceNow]*-1000);
  289. return node;
  290. }
  291. - (id<FNode>)serverCacheForKeys:(NSSet *)keys atPath:(FPath *)path {
  292. NSDate *start = [NSDate date];
  293. __block id<FNode> node = [FEmptyNode emptyNode];
  294. [keys enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) {
  295. id data = [self internalNestedDataForPath:[path childFromString:key]];
  296. node = [node updateImmediateChild:key withNewChild:[FSnapshotUtilities nodeFrom:data]];
  297. }];
  298. FFDebug(@"I-RDB076016", @"Loaded node with %d children for %lu keys at %@ in %fms", [node numChildren], (unsigned long)keys.count, path, [start timeIntervalSinceNow]*-1000);
  299. return node;
  300. }
  301. - (void)updateServerCache:(id<FNode>)node atPath:(FPath *)path merge:(BOOL)merge {
  302. NSDate *start = [NSDate date];
  303. id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
  304. // Remove any leaf nodes that might be higher up
  305. [self removeAllLeafNodesOnPath:path batch:batch];
  306. __block NSUInteger counter = 0;
  307. if (merge) {
  308. // remove any children that exist
  309. [node enumerateChildrenUsingBlock:^(NSString *childKey, id<FNode> childNode, BOOL *stop) {
  310. FPath *childPath = [path childFromString:childKey];
  311. [self removeAllWithPrefix:serverCacheKey(childPath) batch:batch database:self.serverCacheDB];
  312. [self saveNodeInternal:childNode atPath:childPath batch:batch counter:&counter];
  313. }];
  314. } else {
  315. // remove everything
  316. [self removeAllWithPrefix:serverCacheKey(path) batch:batch database:self.serverCacheDB];
  317. [self saveNodeInternal:node atPath:path batch:batch counter:&counter];
  318. }
  319. BOOL success = [batch commit];
  320. if (!success) {
  321. FFWarn(@"I-RDB076017", @"Failed to update server cache on disk!");
  322. } else {
  323. FFDebug(@"I-RDB076018", @"Saved %lu leaf nodes for overwrite in %fms", (unsigned long)counter, [start timeIntervalSinceNow]*-1000);
  324. }
  325. }
  326. - (void)updateServerCacheWithMerge:(FCompoundWrite *)merge atPath:(FPath *)path {
  327. NSDate *start = [NSDate date];
  328. __block NSUInteger counter = 0;
  329. id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
  330. // Remove any leaf nodes that might be higher up
  331. [self removeAllLeafNodesOnPath:path batch:batch];
  332. [merge enumerateWrites:^(FPath *relativePath, id<FNode> node, BOOL *stop) {
  333. FPath *childPath = [path child:relativePath];
  334. [self removeAllWithPrefix:serverCacheKey(childPath) batch:batch database:self.serverCacheDB];
  335. [self saveNodeInternal:node atPath:childPath batch:batch counter:&counter];
  336. }];
  337. BOOL success = [batch commit];
  338. if (!success) {
  339. FFWarn(@"I-RDB076019", @"Failed to update server cache on disk!");
  340. } else {
  341. FFDebug(@"I-RDB076020", @"Saved %lu leaf nodes for merge in %fms", (unsigned long)counter, [start timeIntervalSinceNow]*-1000);
  342. }
  343. }
  344. - (void)saveNodeInternal:(id<FNode>)node atPath:(FPath *)path batch:(id<APLevelDBWriteBatch>)batch counter:(NSUInteger *)counter {
  345. id data = [node valForExport:YES];
  346. if(data != nil && ![data isKindOfClass:[NSNull class]]) {
  347. [self internalSetNestedData:data forKey:serverCacheKey(path) withBatch:batch counter:counter];
  348. }
  349. }
  350. - (NSUInteger)serverCacheEstimatedSizeInBytes {
  351. // Use the exact size, because for pruning the approximate size can lead to weird situations where we prune everything
  352. // because no compaction is ever run
  353. return [self.serverCacheDB exactSizeFrom:kFServerCachePrefix to:kFServerCacheRangeEnd];
  354. }
  355. - (void)pruneCache:(FPruneForest *)pruneForest atPath:(FPath *)path {
  356. // TODO: be more intelligent, don't scan entire database...
  357. __block NSUInteger pruned = 0;
  358. __block NSUInteger kept = 0;
  359. NSDate *start = [NSDate date];
  360. NSString *prefix = serverCacheKey(path);
  361. id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
  362. [self.serverCacheDB enumerateKeysWithPrefix:prefix usingBlock:^(NSString *dbKey, BOOL *stop) {
  363. NSString *pathStr = [dbKey substringFromIndex:prefix.length];
  364. FPath *relativePath = [[FPath alloc] initWith:pathStr];
  365. if ([pruneForest shouldPruneUnkeptDescendantsAtPath:relativePath]) {
  366. pruned++;
  367. [batch removeKey:dbKey];
  368. } else {
  369. kept++;
  370. }
  371. }];
  372. BOOL success = [batch commit];
  373. if (!success) {
  374. FFWarn(@"I-RDB076021", @"Failed to prune cache on disk!");
  375. } else {
  376. FFDebug(@"I-RDB076022", @"Pruned %lu paths, kept %lu paths in %fms", (unsigned long)pruned, (unsigned long)kept, [start timeIntervalSinceNow]*-1000);
  377. }
  378. }
  379. #pragma mark - Tracked Queries
  380. - (NSArray *)loadTrackedQueries {
  381. NSDate *date = [NSDate date];
  382. NSMutableArray *trackedQueries = [NSMutableArray array];
  383. [self.serverCacheDB enumerateKeysWithPrefix:kFTrackedQueriesPrefix asData:^(NSString *key, NSData *data, BOOL *stop) {
  384. NSError *error = nil;
  385. NSDictionary *queryJSON = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
  386. if (queryJSON == nil) {
  387. if (error.code == kFNanFailureCode) {
  388. FFWarn(@"I-RDB076023", @"Failed to deserialize tracked query (%@), likely because of out of range doubles (Error: %@)",
  389. [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding],
  390. error);
  391. FFWarn(@"I-RDB076024", @"Removing failed tracked query with key %@", key);
  392. [self.serverCacheDB removeKey:key];
  393. } else {
  394. [NSException raise:NSInternalInconsistencyException format:@"Failed to deserialize tracked query: %@", error];
  395. }
  396. } else {
  397. NSUInteger queryId = ((NSNumber *)queryJSON[kFTrackedQueryId]).unsignedIntegerValue;
  398. FPath *path = [FPath pathWithString:queryJSON[kFTrackedQueryPath]];
  399. FQueryParams *params = [FQueryParams fromQueryObject:queryJSON[kFTrackedQueryParams]];
  400. FQuerySpec *query = [[FQuerySpec alloc] initWithPath:path params:params];
  401. BOOL isComplete = [queryJSON[kFTrackedQueryIsComplete] boolValue];
  402. BOOL isActive = [queryJSON[kFTrackedQueryIsActive] boolValue];
  403. NSTimeInterval lastUse = [queryJSON[kFTrackedQueryLastUse] doubleValue];
  404. FTrackedQuery *trackedQuery = [[FTrackedQuery alloc] initWithId:queryId
  405. query:query
  406. lastUse:lastUse
  407. isActive:isActive
  408. isComplete:isComplete];
  409. [trackedQueries addObject:trackedQuery];
  410. }
  411. }];
  412. FFDebug(@"I-RDB076025", @"Loaded %lu tracked queries in %fms", (unsigned long)trackedQueries.count, [date timeIntervalSinceNow]*-1000);
  413. return trackedQueries;
  414. }
  415. - (void)removeTrackedQuery:(NSUInteger)queryId {
  416. NSDate *start = [NSDate date];
  417. id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
  418. [batch removeKey:trackedQueryKey(queryId)];
  419. __block NSUInteger keyCount = 0;
  420. [self.serverCacheDB enumerateKeysWithPrefix:trackedQueryKeysKeyPrefix(queryId) usingBlock:^(NSString *key, BOOL *stop) {
  421. [batch removeKey:key];
  422. keyCount++;
  423. }];
  424. BOOL success = [batch commit];
  425. if (!success) {
  426. FFWarn(@"I-RDB076026", @"Failed to remove tracked query on disk!");
  427. } else {
  428. FFDebug(@"I-RDB076027", @"Removed query with id %lu (and removed %lu keys) in %fms",
  429. (unsigned long)queryId,
  430. (unsigned long)keyCount,
  431. [start timeIntervalSinceNow]*-1000);
  432. }
  433. }
  434. - (void)saveTrackedQuery:(FTrackedQuery *)query {
  435. NSDate *start = [NSDate date];
  436. NSDictionary *trackedQuery =
  437. @{
  438. kFTrackedQueryId: @(query.queryId),
  439. kFTrackedQueryPath: [query.query.path toStringWithTrailingSlash],
  440. kFTrackedQueryParams: [query.query.params wireProtocolParams],
  441. kFTrackedQueryLastUse: @(query.lastUse),
  442. kFTrackedQueryIsComplete: @(query.isComplete),
  443. kFTrackedQueryIsActive: @(query.isActive)
  444. };
  445. NSError *error = nil;
  446. NSData *data = [NSJSONSerialization dataWithJSONObject:trackedQuery options:0 error:&error];
  447. NSAssert(data, @"Failed to serialize tracked query (Error: %@)", error);
  448. [self.serverCacheDB setData:data forKey:trackedQueryKey(query.queryId)];
  449. FFDebug(@"I-RDB076028", @"Saved tracked query %lu in %fms", (unsigned long)query.queryId, [start timeIntervalSinceNow]*-1000);
  450. }
  451. - (void)setTrackedQueryKeys:(NSSet *)keys forQueryId:(NSUInteger)queryId {
  452. NSDate *start = [NSDate date];
  453. __block NSUInteger removed = 0;
  454. __block NSUInteger added = 0;
  455. id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
  456. NSMutableSet *seenKeys = [NSMutableSet set];
  457. // First, delete any keys that might be stored and are not part of the current keys
  458. [self.serverCacheDB enumerateKeysWithPrefix:trackedQueryKeysKeyPrefix(queryId) asStrings:^(NSString *dbKey, NSString *actualKey, BOOL *stop) {
  459. if ([keys containsObject:actualKey]) {
  460. // Already in DB
  461. [seenKeys addObject:actualKey];
  462. } else {
  463. // Not part of set, delete key
  464. [batch removeKey:dbKey];
  465. removed++;
  466. }
  467. }];
  468. // Next add any keys that are missing in the database
  469. [keys enumerateObjectsUsingBlock:^(NSString *childKey, BOOL *stop) {
  470. if (![seenKeys containsObject:childKey]) {
  471. [batch setString:childKey forKey:trackedQueryKeysKey(queryId, childKey)];
  472. added++;
  473. }
  474. }];
  475. BOOL success = [batch commit];
  476. if (!success) {
  477. FFWarn(@"I-RDB076029", @"Failed to set tracked queries on disk!");
  478. } else {
  479. FFDebug(@"I-RDB076030", @"Set %lu tracked keys (%lu added, %lu removed) for query %lu in %fms",
  480. (unsigned long)keys.count,
  481. (unsigned long)added,
  482. (unsigned long)removed,
  483. (unsigned long)queryId,
  484. [start timeIntervalSinceNow]*-1000);
  485. }
  486. }
  487. - (void)updateTrackedQueryKeysWithAddedKeys:(NSSet *)added removedKeys:(NSSet *)removed forQueryId:(NSUInteger)queryId {
  488. NSDate *start = [NSDate date];
  489. id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
  490. [removed enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) {
  491. [batch removeKey:trackedQueryKeysKey(queryId, key)];
  492. }];
  493. [added enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) {
  494. [batch setString:key forKey:trackedQueryKeysKey(queryId, key)];
  495. }];
  496. BOOL success = [batch commit];
  497. if (!success) {
  498. FFWarn(@"I-RDB076031", @"Failed to update tracked queries on disk!");
  499. } else {
  500. 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);
  501. }
  502. }
  503. - (NSSet *)trackedQueryKeysForQuery:(NSUInteger)queryId {
  504. NSDate *start = [NSDate date];
  505. NSMutableSet *set = [NSMutableSet set];
  506. [self.serverCacheDB enumerateKeysWithPrefix:trackedQueryKeysKeyPrefix(queryId) asStrings:^(NSString *dbKey, NSString *actualKey, BOOL *stop) {
  507. [set addObject:actualKey];
  508. }];
  509. FFDebug(@"I-RDB076033", @"Loaded %lu tracked keys for query %lu in %fms", (unsigned long)set.count, (unsigned long)queryId, [start timeIntervalSinceNow]*-1000);
  510. return set;
  511. }
  512. #pragma mark - Internal methods
  513. - (void)removeAllLeafNodesOnPath:(FPath *)path batch:(id<APLevelDBWriteBatch>)batch {
  514. while (!path.isEmpty) {
  515. [batch removeKey:serverCacheKey(path)];
  516. path = [path parent];
  517. }
  518. // Make sure to delete any nodes at the root
  519. [batch removeKey:serverCacheKey([FPath empty])];
  520. }
  521. - (void)removeAllWithPrefix:(NSString *)prefix batch:(id<APLevelDBWriteBatch>)batch database:(APLevelDB *)database {
  522. assert(prefix != nil);
  523. [database enumerateKeysWithPrefix:prefix usingBlock:^(NSString *key, BOOL *stop) {
  524. [batch removeKey:key];
  525. }];
  526. }
  527. #pragma mark - Internal helper methods
  528. - (void)internalSetNestedData:(id)value forKey:(NSString *)key withBatch:(id<APLevelDBWriteBatch>)batch counter:(NSUInteger *)counter {
  529. if([value isKindOfClass:[NSDictionary class]]) {
  530. NSDictionary* dictionary = value;
  531. [dictionary enumerateKeysAndObjectsUsingBlock:^(id childKey, id obj, BOOL *stop) {
  532. assert(obj != nil);
  533. NSString* childPath = [NSString stringWithFormat:@"%@%@/", key, childKey];
  534. [self internalSetNestedData:obj forKey:childPath withBatch:batch counter:counter];
  535. }];
  536. }
  537. else {
  538. NSData *data = [self serializePrimitive:value];
  539. [batch setData:data forKey:key];
  540. (*counter)++;
  541. }
  542. }
  543. - (id)internalNestedDataForPath:(FPath *)path {
  544. NSAssert(path != nil, @"Path was nil!");
  545. NSString *baseKey = serverCacheKey(path);
  546. // HACK to make sure iter is freed now to avoid race conditions (if self.db is deleted before iter, you get an access violation).
  547. @autoreleasepool {
  548. APLevelDBIterator* iter = [APLevelDBIterator iteratorWithLevelDB:self.serverCacheDB];
  549. [iter seekToKey:baseKey];
  550. if (iter.key == nil || ![iter.key hasPrefix:baseKey]) {
  551. // No data.
  552. return nil;
  553. } else {
  554. return [self internalNestedDataFromIterator:iter andKeyPrefix:baseKey];
  555. }
  556. }
  557. }
  558. - (id) internalNestedDataFromIterator:(APLevelDBIterator*)iterator andKeyPrefix:(NSString*)prefix {
  559. NSString* key = iterator.key;
  560. if ([key isEqualToString:prefix]) {
  561. id result = [self deserializePrimitive:iterator.valueAsData];
  562. [iterator nextKey];
  563. return result;
  564. } else {
  565. NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
  566. while (key != nil && [key hasPrefix:prefix]) {
  567. NSString *relativePath = [key substringFromIndex:prefix.length];
  568. NSArray* pathPieces = [relativePath componentsSeparatedByString:@"/"];
  569. assert(pathPieces.count > 0);
  570. NSString *childName = pathPieces[0];
  571. NSString *childPath = [NSString stringWithFormat:@"%@%@/", prefix, childName];
  572. id childValue = [self internalNestedDataFromIterator:iterator andKeyPrefix:childPath];
  573. [dict setValue:childValue forKey:childName];
  574. key = iterator.key;
  575. }
  576. return dict;
  577. }
  578. }
  579. - (NSData*) serializePrimitive:(id)value {
  580. // HACK: The built-in serialization only works on dicts and arrays. So we create an array and then strip off
  581. // the leading / trailing byte (the [ and ]).
  582. NSError *error = nil;
  583. NSData *data = [NSJSONSerialization dataWithJSONObject:@[value] options:0 error:&error];
  584. NSAssert(data, @"Failed to serialize primitive: %@", error);
  585. return [data subdataWithRange:NSMakeRange(1, data.length - 2)];
  586. }
  587. - (id)fixDoubleParsing:(id)value {
  588. // The parser for double values in JSONSerialization at the root takes some short-cuts and delivers wrong results
  589. // (wrong rounding) for some double values, including 2.47. Because we use the exact bytes for hashing on the server
  590. // this will lead to hash mismatches. The parser of NSNumber seems to be more in line with what the server expects,
  591. // so we use that here
  592. if ([value isKindOfClass:[NSNumber class]]) {
  593. CFNumberType type = CFNumberGetType((CFNumberRef)value);
  594. if (type == kCFNumberDoubleType || type == kCFNumberFloatType) {
  595. // The NSJSON parser returns all numbers as double values, even those that contain no exponent. To
  596. // make sure that the String conversion below doesn't unexpectedly reduce precision, we make sure that
  597. // our number is indeed not an integer.
  598. if ((double)(long long)[value doubleValue] != [value doubleValue]) {
  599. NSString *doubleString = [value stringValue];
  600. return [NSNumber numberWithDouble:[doubleString doubleValue]];
  601. }
  602. }
  603. }
  604. return value;
  605. }
  606. - (id) deserializePrimitive:(NSData*)data {
  607. NSError *error = nil;
  608. id result = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
  609. if (result != nil) {
  610. return [self fixDoubleParsing:result];
  611. } else {
  612. if (error.code == kFNanFailureCode) {
  613. FFWarn(@"I-RDB076034", @"Failed to load primitive %@, likely because doubles where out of range (Error: %@)",
  614. [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding], error);
  615. return [NSNull null];
  616. } else {
  617. [NSException raise:NSInternalInconsistencyException format:@"Failed to deserialiaze primitive: %@", error];
  618. return nil;
  619. }
  620. }
  621. }
  622. + (void)ensureDir:(NSString*)path markAsDoNotBackup:(BOOL)markAsDoNotBackup {
  623. NSError* error;
  624. BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:path
  625. withIntermediateDirectories:YES
  626. attributes:nil
  627. error:&error];
  628. if (!success) {
  629. @throw [NSException exceptionWithName:@"FailedToCreatePersistenceDir" reason:@"Failed to create persistence directory." userInfo:@{ @"path": path }];
  630. }
  631. if (markAsDoNotBackup) {
  632. NSURL *firebaseDirURL = [NSURL fileURLWithPath:path];
  633. success = [firebaseDirURL setResourceValue:@YES
  634. forKey:NSURLIsExcludedFromBackupKey
  635. error:&error];
  636. if (!success) {
  637. FFWarn(@"I-RDB076035", @"Failed to mark firebase database folder as do not backup: %@", error);
  638. [NSException raise:@"Error marking as do not backup" format:@"Failed to mark folder %@ as do not backup", firebaseDirURL];
  639. }
  640. }
  641. }
  642. @end