FLevelDBStorageEngine.m 33 KB

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