FLevelDBStorageEngine.m 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011
  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 "FirebaseDatabase/Sources/Persistence/FLevelDBStorageEngine.h"
  18. #import "FirebaseCore/Extension/FirebaseCoreInternal.h"
  19. #import "FirebaseDatabase/Sources/Core/FQueryParams.h"
  20. #import "FirebaseDatabase/Sources/Core/FWriteRecord.h"
  21. #import "FirebaseDatabase/Sources/Persistence/FPendingPut.h"
  22. #import "FirebaseDatabase/Sources/Persistence/FPruneForest.h"
  23. #import "FirebaseDatabase/Sources/Persistence/FTrackedQuery.h"
  24. #import "FirebaseDatabase/Sources/Snapshot/FEmptyNode.h"
  25. #import "FirebaseDatabase/Sources/Snapshot/FSnapshotUtilities.h"
  26. #import "FirebaseDatabase/Sources/Utilities/FUtilities.h"
  27. #import "FirebaseDatabase/Sources/third_party/Wrap-leveldb/APLevelDB.h"
  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
  54. // deserializing
  55. static const NSInteger kFNanFailureCode = 3840;
  56. static NSString *writeRecordKey(NSUInteger writeId) {
  57. return [NSString stringWithFormat:@"%lu", (unsigned long)(writeId)];
  58. }
  59. static NSString *serverCacheKey(FPath *path) {
  60. return [NSString stringWithFormat:@"%@%@", kFServerCachePrefix,
  61. ([path toStringWithTrailingSlash])];
  62. }
  63. static NSString *trackedQueryKey(NSUInteger trackedQueryId) {
  64. return [NSString stringWithFormat:@"%@%lu", kFTrackedQueriesPrefix,
  65. (unsigned long)trackedQueryId];
  66. }
  67. static NSString *trackedQueryKeysKeyPrefix(NSUInteger trackedQueryId) {
  68. return [NSString stringWithFormat:@"%@%lu/", kFTrackedQueryKeysPrefix,
  69. (unsigned long)trackedQueryId];
  70. }
  71. static NSString *trackedQueryKeysKey(NSUInteger trackedQueryId, NSString *key) {
  72. return [NSString stringWithFormat:@"%@%lu/%@", kFTrackedQueryKeysPrefix,
  73. (unsigned long)trackedQueryId, key];
  74. }
  75. @implementation FLevelDBStorageEngine
  76. #pragma mark - Constructors
  77. - (id)initWithPath:(NSString *)dbPath {
  78. self = [super init];
  79. if (self) {
  80. self.basePath = [[FLevelDBStorageEngine firebaseDir]
  81. stringByAppendingPathComponent:dbPath];
  82. /* For reference:
  83. serverDataDB = [aPersistence createDbByName:@"server_data"];
  84. FPangolinDB *completenessDb = [aPersistence
  85. createDbByName:@"server_complete"];
  86. */
  87. [FLevelDBStorageEngine ensureDir:self.basePath markAsDoNotBackup:YES];
  88. [self runMigration];
  89. [self openDatabases];
  90. }
  91. return self;
  92. }
  93. - (void)runMigration {
  94. // Currently we're at version 1, so all we need to do is write that to a
  95. // file
  96. NSString *versionFile =
  97. [self.basePath stringByAppendingPathComponent:@"version"];
  98. NSError *error;
  99. NSString *oldVersion =
  100. [NSString stringWithContentsOfFile:versionFile
  101. encoding:NSUTF8StringEncoding
  102. error:&error];
  103. if (!oldVersion) {
  104. // This is probably fine, we don't have a version file yet
  105. BOOL success = [kFPersistenceVersion writeToFile:versionFile
  106. atomically:NO
  107. encoding:NSUTF8StringEncoding
  108. error:&error];
  109. if (!success) {
  110. FFWarn(@"I-RDB076001", @"Failed to write version for database: %@",
  111. error);
  112. }
  113. } else if ([oldVersion isEqualToString:kFPersistenceVersion]) {
  114. // Everythings fine no need for migration
  115. } else if ([oldVersion length] == 0) {
  116. FFWarn(@"I-RDB076036",
  117. @"Version file empty. Assuming database version 1.");
  118. } else {
  119. // If we add more versions in the future, we need to run migration here
  120. [NSException raise:NSInternalInconsistencyException
  121. format:@"Unrecognized database version: %@", oldVersion];
  122. }
  123. }
  124. - (void)runLegacyMigration:(FRepoInfo *)info {
  125. NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(
  126. NSDocumentDirectory, NSUserDomainMask, YES);
  127. NSString *documentsDir = [dirPaths objectAtIndex:0];
  128. NSString *firebaseDir =
  129. [documentsDir stringByAppendingPathComponent:@"firebase"];
  130. NSString *repoHashString =
  131. [NSString stringWithFormat:@"%@_%@", info.host, info.namespace];
  132. NSString *legacyBaseDir =
  133. [NSString stringWithFormat:@"%@/1/%@/v1", firebaseDir, repoHashString];
  134. if ([[NSFileManager defaultManager] fileExistsAtPath:legacyBaseDir]) {
  135. FFWarn(@"I-RDB076002", @"Legacy database found, migrating...");
  136. // We only need to migrate writes
  137. NSError *error = nil;
  138. APLevelDB *writes = [APLevelDB
  139. levelDBWithPath:[legacyBaseDir stringByAppendingPathComponent:
  140. @"outstanding_puts"]
  141. error:&error];
  142. if (writes != nil) {
  143. __block NSUInteger numberOfWritesRestored = 0;
  144. // Maybe we could use write batches, but what the heck, I'm sure
  145. // it'll go fine :P
  146. [writes enumerateKeysAndValuesAsData:^(NSString *key, NSData *data,
  147. BOOL *stop) {
  148. NSError *error;
  149. id pendingPut = [NSKeyedUnarchiver
  150. unarchivedObjectOfClasses:
  151. [NSSet setWithObjects:[FPendingPut class],
  152. [FPendingPutPriority class],
  153. [FPendingUpdate class], nil]
  154. fromData:data
  155. error:&error];
  156. if (error) {
  157. FFWarn(@"I-RDB076003", @"Failed to migrate legacy write: %@",
  158. error);
  159. } else if ([pendingPut isKindOfClass:[FPendingPut class]]) {
  160. FPendingPut *put = pendingPut;
  161. id<FNode> newNode =
  162. [FSnapshotUtilities nodeFrom:put.data
  163. priority:put.priority];
  164. [self saveUserOverwrite:newNode
  165. atPath:put.path
  166. writeId:[key integerValue]];
  167. numberOfWritesRestored++;
  168. } else if ([pendingPut
  169. isKindOfClass:[FPendingPutPriority class]]) {
  170. // This is for backwards compatibility. Older clients will
  171. // save FPendingPutPriority. New ones will need to read it and
  172. // translate.
  173. FPendingPutPriority *putPriority = pendingPut;
  174. FPath *priorityPath =
  175. [putPriority.path childFromString:@".priority"];
  176. id<FNode> newNode =
  177. [FSnapshotUtilities nodeFrom:putPriority.priority
  178. priority:nil];
  179. [self saveUserOverwrite:newNode
  180. atPath:priorityPath
  181. writeId:[key integerValue]];
  182. numberOfWritesRestored++;
  183. } else if ([pendingPut isKindOfClass:[FPendingUpdate class]]) {
  184. FPendingUpdate *update = pendingPut;
  185. FCompoundWrite *merge = [FCompoundWrite
  186. compoundWriteWithValueDictionary:update.data];
  187. [self saveUserMerge:merge
  188. atPath:update.path
  189. writeId:[key integerValue]];
  190. numberOfWritesRestored++;
  191. } else {
  192. FFWarn(@"I-RDB076003",
  193. @"Failed to migrate legacy write: unrecognized class "
  194. @"\"%@\"",
  195. [pendingPut class]);
  196. }
  197. }];
  198. FFWarn(@"I-RDB076004", @"Migrated %lu writes",
  199. (unsigned long)numberOfWritesRestored);
  200. [writes close];
  201. FFWarn(@"I-RDB076005", @"Deleting legacy database...");
  202. BOOL success =
  203. [[NSFileManager defaultManager] removeItemAtPath:legacyBaseDir
  204. error:&error];
  205. if (!success) {
  206. FFWarn(@"I-RDB076006", @"Failed to delete legacy database: %@",
  207. error);
  208. } else {
  209. FFWarn(@"I-RDB076007", @"Finished migrating legacy database.");
  210. }
  211. } else {
  212. FFWarn(@"I-RDB076008", @"Failed to migrate old database: %@",
  213. error);
  214. }
  215. }
  216. }
  217. - (void)openDatabases {
  218. self.serverCacheDB = [self createDB:kFServerDBPath];
  219. self.writesDB = [self createDB:kFWritesDBPath];
  220. }
  221. - (void)purgeDatabase:(NSString *)dbPath {
  222. NSString *path = [self.basePath stringByAppendingPathComponent:dbPath];
  223. NSError *error;
  224. FFWarn(@"I-RDB076009", @"Deleting database at path %@", path);
  225. BOOL success = [[NSFileManager defaultManager] removeItemAtPath:path
  226. error:&error];
  227. if (!success) {
  228. [NSException raise:NSInternalInconsistencyException
  229. format:@"Failed to delete database files: %@", error];
  230. }
  231. }
  232. - (void)purgeEverything {
  233. [self close];
  234. [@[ kFServerDBPath, kFWritesDBPath ]
  235. enumerateObjectsUsingBlock:^(NSString *dbPath, NSUInteger idx,
  236. BOOL *stop) {
  237. [self purgeDatabase:dbPath];
  238. }];
  239. [self openDatabases];
  240. }
  241. - (void)close {
  242. // autoreleasepool will cause deallocation which will close the DB
  243. @autoreleasepool {
  244. [self.serverCacheDB close];
  245. self.serverCacheDB = nil;
  246. [self.writesDB close];
  247. self.writesDB = nil;
  248. }
  249. }
  250. + (NSString *)firebaseDir {
  251. #if TARGET_OS_IOS || TARGET_OS_WATCH || \
  252. (defined(TARGET_OS_VISION) && TARGET_OS_VISION)
  253. NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(
  254. NSDocumentDirectory, NSUserDomainMask, YES);
  255. NSString *documentsDir = [dirPaths objectAtIndex:0];
  256. return [documentsDir stringByAppendingPathComponent:@"firebase"];
  257. #elif TARGET_OS_TV
  258. NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(
  259. NSCachesDirectory, NSUserDomainMask, YES);
  260. NSString *cachesDir = [dirPaths objectAtIndex:0];
  261. return [cachesDir stringByAppendingPathComponent:@"firebase"];
  262. #elif TARGET_OS_OSX
  263. return [NSHomeDirectory() stringByAppendingPathComponent:@".firebase"];
  264. #endif
  265. }
  266. - (APLevelDB *)createDB:(NSString *)dbName {
  267. NSError *err = nil;
  268. NSString *path = [self.basePath stringByAppendingPathComponent:dbName];
  269. APLevelDB *db = [APLevelDB levelDBWithPath:path error:&err];
  270. if (err) {
  271. FFWarn(@"I-RDB076036",
  272. @"Failed to read database persistence file '%@': %@", dbName,
  273. [err localizedDescription]);
  274. err = nil;
  275. // Delete the database and try again.
  276. [self purgeDatabase:dbName];
  277. db = [APLevelDB levelDBWithPath:path error:&err];
  278. if (err) {
  279. NSString *reason = [NSString
  280. stringWithFormat:@"Error initializing persistence: %@",
  281. [err description]];
  282. @throw [NSException
  283. exceptionWithName:@"FirebaseDatabasePersistenceFailure"
  284. reason:reason
  285. userInfo:nil];
  286. }
  287. }
  288. return db;
  289. }
  290. - (void)saveUserOverwrite:(id<FNode>)node
  291. atPath:(FPath *)path
  292. writeId:(NSUInteger)writeId {
  293. NSDictionary *write = @{
  294. kFUserWriteId : @(writeId),
  295. kFUserWritePath : [path toStringWithTrailingSlash],
  296. kFUserWriteOverwrite : [node valForExport:YES]
  297. };
  298. NSError *error = nil;
  299. NSData *data = [NSJSONSerialization dataWithJSONObject:write
  300. options:0
  301. error:&error];
  302. NSAssert(data, @"Failed to serialize user overwrite: %@, (Error: %@)",
  303. write, error);
  304. [self.writesDB setData:data forKey:writeRecordKey(writeId)];
  305. }
  306. - (void)saveUserMerge:(FCompoundWrite *)merge
  307. atPath:(FPath *)path
  308. writeId:(NSUInteger)writeId {
  309. NSDictionary *write = @{
  310. kFUserWriteId : @(writeId),
  311. kFUserWritePath : [path toStringWithTrailingSlash],
  312. kFUserWriteMerge : [merge valForExport:YES]
  313. };
  314. NSError *error = nil;
  315. NSData *data = [NSJSONSerialization dataWithJSONObject:write
  316. options:0
  317. error:&error];
  318. NSAssert(data, @"Failed to serialize user merge: %@ (Error: %@)", write,
  319. error);
  320. [self.writesDB setData:data forKey:writeRecordKey(writeId)];
  321. }
  322. - (void)removeUserWrite:(NSUInteger)writeId {
  323. [self.writesDB removeKey:writeRecordKey(writeId)];
  324. }
  325. - (void)removeAllUserWrites {
  326. __block NSUInteger count = 0;
  327. NSDate *start = [NSDate date];
  328. id<APLevelDBWriteBatch> batch = [self.writesDB beginWriteBatch];
  329. [self.writesDB enumerateKeys:^(NSString *key, BOOL *stop) {
  330. [batch removeKey:key];
  331. count++;
  332. }];
  333. BOOL success = [batch commit];
  334. if (!success) {
  335. FFWarn(@"I-RDB076010", @"Failed to remove all users writes on disk!");
  336. } else {
  337. FFDebug(@"I-RDB076011", @"Removed %lu writes in %fms",
  338. (unsigned long)count, [start timeIntervalSinceNow] * -1000);
  339. }
  340. }
  341. - (NSArray *)userWrites {
  342. NSDate *date = [NSDate date];
  343. NSMutableArray *writes = [NSMutableArray array];
  344. [self.writesDB enumerateKeysAndValuesAsData:^(NSString *key, NSData *data,
  345. BOOL *stop) {
  346. NSError *error = nil;
  347. NSDictionary *writeJSON = [NSJSONSerialization JSONObjectWithData:data
  348. options:0
  349. error:&error];
  350. if (writeJSON == nil) {
  351. if (error.code == kFNanFailureCode) {
  352. FFWarn(@"I-RDB076012",
  353. @"Failed to deserialize write (%@), likely because of out "
  354. @"of range doubles (Error: %@)",
  355. [[NSString alloc] initWithData:data
  356. encoding:NSUTF8StringEncoding],
  357. error);
  358. FFWarn(@"I-RDB076013", @"Removing failed write with key %@", key);
  359. [self.writesDB removeKey:key];
  360. } else {
  361. [NSException raise:NSInternalInconsistencyException
  362. format:@"Failed to deserialize write: %@", error];
  363. }
  364. } else {
  365. NSInteger writeId =
  366. ((NSNumber *)writeJSON[kFUserWriteId]).integerValue;
  367. FPath *path = [FPath pathWithString:writeJSON[kFUserWritePath]];
  368. FWriteRecord *writeRecord;
  369. if (writeJSON[kFUserWriteMerge] != nil) {
  370. // It's a merge
  371. FCompoundWrite *merge = [FCompoundWrite
  372. compoundWriteWithValueDictionary:writeJSON[kFUserWriteMerge]];
  373. writeRecord = [[FWriteRecord alloc] initWithPath:path
  374. merge:merge
  375. writeId:writeId];
  376. } else {
  377. // It's an overwrite
  378. NSAssert(writeJSON[kFUserWriteOverwrite] != nil,
  379. @"Persisted write did not contain merge or overwrite!");
  380. id<FNode> node =
  381. [FSnapshotUtilities nodeFrom:writeJSON[kFUserWriteOverwrite]];
  382. writeRecord = [[FWriteRecord alloc] initWithPath:path
  383. overwrite:node
  384. writeId:writeId
  385. visible:YES];
  386. }
  387. [writes addObject:writeRecord];
  388. }
  389. }];
  390. // Make sure writes are sorted
  391. [writes sortUsingComparator:^NSComparisonResult(FWriteRecord *one,
  392. FWriteRecord *two) {
  393. if (one.writeId < two.writeId) {
  394. return NSOrderedAscending;
  395. } else if (one.writeId > two.writeId) {
  396. return NSOrderedDescending;
  397. } else {
  398. return NSOrderedSame;
  399. }
  400. }];
  401. FFDebug(@"I-RDB076014", @"Loaded %lu writes in %fms",
  402. (unsigned long)writes.count, [date timeIntervalSinceNow] * -1000);
  403. return writes;
  404. }
  405. - (id<FNode>)serverCacheAtPath:(FPath *)path {
  406. NSDate *start = [NSDate date];
  407. id data = [self internalNestedDataForPath:path];
  408. id<FNode> node = [FSnapshotUtilities nodeFrom:data];
  409. FFDebug(@"I-RDB076015", @"Loaded node with %d children at %@ in %fms",
  410. [node numChildren], path, [start timeIntervalSinceNow] * -1000);
  411. return node;
  412. }
  413. - (id<FNode>)serverCacheForKeys:(NSSet *)keys atPath:(FPath *)path {
  414. NSDate *start = [NSDate date];
  415. __block id<FNode> node = [FEmptyNode emptyNode];
  416. [keys enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) {
  417. id data = [self internalNestedDataForPath:[path childFromString:key]];
  418. node = [node updateImmediateChild:key
  419. withNewChild:[FSnapshotUtilities nodeFrom:data]];
  420. }];
  421. FFDebug(@"I-RDB076016",
  422. @"Loaded node with %d children for %lu keys at %@ in %fms",
  423. [node numChildren], (unsigned long)keys.count, path,
  424. [start timeIntervalSinceNow] * -1000);
  425. return node;
  426. }
  427. - (void)updateServerCache:(id<FNode>)node
  428. atPath:(FPath *)path
  429. merge:(BOOL)merge {
  430. NSDate *start = [NSDate date];
  431. id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
  432. // Remove any leaf nodes that might be higher up
  433. [self removeAllLeafNodesOnPath:path batch:batch];
  434. __block NSUInteger counter = 0;
  435. if (merge) {
  436. // remove any children that exist
  437. [node enumerateChildrenUsingBlock:^(NSString *childKey,
  438. id<FNode> childNode, BOOL *stop) {
  439. FPath *childPath = [path childFromString:childKey];
  440. [self removeAllWithPrefix:serverCacheKey(childPath)
  441. batch:batch
  442. database:self.serverCacheDB];
  443. [self saveNodeInternal:childNode
  444. atPath:childPath
  445. batch:batch
  446. counter:&counter];
  447. }];
  448. } else {
  449. // remove everything
  450. [self removeAllWithPrefix:serverCacheKey(path)
  451. batch:batch
  452. database:self.serverCacheDB];
  453. [self saveNodeInternal:node atPath:path batch:batch counter:&counter];
  454. }
  455. BOOL success = [batch commit];
  456. if (!success) {
  457. FFWarn(@"I-RDB076017", @"Failed to update server cache on disk!");
  458. } else {
  459. FFDebug(@"I-RDB076018", @"Saved %lu leaf nodes for overwrite in %fms",
  460. (unsigned long)counter, [start timeIntervalSinceNow] * -1000);
  461. }
  462. }
  463. - (void)updateServerCacheWithMerge:(FCompoundWrite *)merge
  464. atPath:(FPath *)path {
  465. NSDate *start = [NSDate date];
  466. __block NSUInteger counter = 0;
  467. id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
  468. // Remove any leaf nodes that might be higher up
  469. [self removeAllLeafNodesOnPath:path batch:batch];
  470. [merge enumerateWrites:^(FPath *relativePath, id<FNode> node, BOOL *stop) {
  471. FPath *childPath = [path child:relativePath];
  472. [self removeAllWithPrefix:serverCacheKey(childPath)
  473. batch:batch
  474. database:self.serverCacheDB];
  475. [self saveNodeInternal:node
  476. atPath:childPath
  477. batch:batch
  478. counter:&counter];
  479. }];
  480. BOOL success = [batch commit];
  481. if (!success) {
  482. FFWarn(@"I-RDB076019", @"Failed to update server cache on disk!");
  483. } else {
  484. FFDebug(@"I-RDB076020", @"Saved %lu leaf nodes for merge in %fms",
  485. (unsigned long)counter, [start timeIntervalSinceNow] * -1000);
  486. }
  487. }
  488. - (void)saveNodeInternal:(id<FNode>)node
  489. atPath:(FPath *)path
  490. batch:(id<APLevelDBWriteBatch>)batch
  491. counter:(NSUInteger *)counter {
  492. id data = [node valForExport:YES];
  493. if (data != nil && ![data isKindOfClass:[NSNull class]]) {
  494. [self internalSetNestedData:data
  495. forKey:serverCacheKey(path)
  496. withBatch:batch
  497. counter:counter];
  498. }
  499. }
  500. - (NSUInteger)serverCacheEstimatedSizeInBytes {
  501. // Use the exact size, because for pruning the approximate size can lead to
  502. // weird situations where we prune everything because no compaction is ever
  503. // run
  504. return [self.serverCacheDB exactSizeFrom:kFServerCachePrefix
  505. to:kFServerCacheRangeEnd];
  506. }
  507. - (void)pruneCache:(FPruneForest *)pruneForest atPath:(FPath *)path {
  508. // TODO: be more intelligent, don't scan entire database...
  509. __block NSUInteger pruned = 0;
  510. __block NSUInteger kept = 0;
  511. NSDate *start = [NSDate date];
  512. NSString *prefix = serverCacheKey(path);
  513. id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
  514. [self.serverCacheDB
  515. enumerateKeysWithPrefix:prefix
  516. usingBlock:^(NSString *dbKey, BOOL *stop) {
  517. NSString *pathStr =
  518. [dbKey substringFromIndex:prefix.length];
  519. FPath *relativePath = [[FPath alloc] initWith:pathStr];
  520. if ([pruneForest shouldPruneUnkeptDescendantsAtPath:
  521. relativePath]) {
  522. pruned++;
  523. [batch removeKey:dbKey];
  524. } else {
  525. kept++;
  526. }
  527. }];
  528. BOOL success = [batch commit];
  529. if (!success) {
  530. FFWarn(@"I-RDB076021", @"Failed to prune cache on disk!");
  531. } else {
  532. FFDebug(@"I-RDB076022", @"Pruned %lu paths, kept %lu paths in %fms",
  533. (unsigned long)pruned, (unsigned long)kept,
  534. [start timeIntervalSinceNow] * -1000);
  535. }
  536. }
  537. #pragma mark - Tracked Queries
  538. - (NSArray *)loadTrackedQueries {
  539. NSDate *date = [NSDate date];
  540. NSMutableArray *trackedQueries = [NSMutableArray array];
  541. [self.serverCacheDB
  542. enumerateKeysWithPrefix:kFTrackedQueriesPrefix
  543. asData:^(NSString *key, NSData *data, BOOL *stop) {
  544. NSError *error = nil;
  545. NSDictionary *queryJSON =
  546. [NSJSONSerialization JSONObjectWithData:data
  547. options:0
  548. error:&error];
  549. if (queryJSON == nil) {
  550. if (error.code == kFNanFailureCode) {
  551. FFWarn(
  552. @"I-RDB076023",
  553. @"Failed to deserialize tracked query "
  554. @"(%@), likely because of out of range "
  555. @"doubles (Error: %@)",
  556. [[NSString alloc]
  557. initWithData:data
  558. encoding:NSUTF8StringEncoding],
  559. error);
  560. FFWarn(@"I-RDB076024",
  561. @"Removing failed tracked query with "
  562. @"key %@",
  563. key);
  564. [self.serverCacheDB removeKey:key];
  565. } else {
  566. [NSException
  567. raise:NSInternalInconsistencyException
  568. format:@"Failed to deserialize tracked "
  569. @"query: %@",
  570. error];
  571. }
  572. } else {
  573. NSUInteger queryId =
  574. ((NSNumber *)queryJSON[kFTrackedQueryId])
  575. .unsignedIntegerValue;
  576. FPath *path =
  577. [FPath pathWithString:
  578. queryJSON[kFTrackedQueryPath]];
  579. FQueryParams *params = [FQueryParams
  580. fromQueryObject:queryJSON
  581. [kFTrackedQueryParams]];
  582. FQuerySpec *query =
  583. [[FQuerySpec alloc] initWithPath:path
  584. params:params];
  585. BOOL isComplete =
  586. [queryJSON[kFTrackedQueryIsComplete]
  587. boolValue];
  588. BOOL isActive =
  589. [queryJSON[kFTrackedQueryIsActive]
  590. boolValue];
  591. NSTimeInterval lastUse =
  592. [queryJSON[kFTrackedQueryLastUse]
  593. doubleValue];
  594. FTrackedQuery *trackedQuery =
  595. [[FTrackedQuery alloc]
  596. initWithId:queryId
  597. query:query
  598. lastUse:lastUse
  599. isActive:isActive
  600. isComplete:isComplete];
  601. [trackedQueries addObject:trackedQuery];
  602. }
  603. }];
  604. FFDebug(@"I-RDB076025", @"Loaded %lu tracked queries in %fms",
  605. (unsigned long)trackedQueries.count,
  606. [date timeIntervalSinceNow] * -1000);
  607. return trackedQueries;
  608. }
  609. - (void)removeTrackedQuery:(NSUInteger)queryId {
  610. NSDate *start = [NSDate date];
  611. id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
  612. [batch removeKey:trackedQueryKey(queryId)];
  613. __block NSUInteger keyCount = 0;
  614. [self.serverCacheDB
  615. enumerateKeysWithPrefix:trackedQueryKeysKeyPrefix(queryId)
  616. usingBlock:^(NSString *key, BOOL *stop) {
  617. [batch removeKey:key];
  618. keyCount++;
  619. }];
  620. BOOL success = [batch commit];
  621. if (!success) {
  622. FFWarn(@"I-RDB076026", @"Failed to remove tracked query on disk!");
  623. } else {
  624. FFDebug(@"I-RDB076027",
  625. @"Removed query with id %lu (and removed %lu keys) in %fms",
  626. (unsigned long)queryId, (unsigned long)keyCount,
  627. [start timeIntervalSinceNow] * -1000);
  628. }
  629. }
  630. - (void)saveTrackedQuery:(FTrackedQuery *)query {
  631. NSDate *start = [NSDate date];
  632. NSDictionary *trackedQuery = @{
  633. kFTrackedQueryId : @(query.queryId),
  634. kFTrackedQueryPath : [query.query.path toStringWithTrailingSlash],
  635. kFTrackedQueryParams : [query.query.params wireProtocolParams],
  636. kFTrackedQueryLastUse : @(query.lastUse),
  637. kFTrackedQueryIsComplete : @(query.isComplete),
  638. kFTrackedQueryIsActive : @(query.isActive)
  639. };
  640. NSError *error = nil;
  641. NSData *data = [NSJSONSerialization dataWithJSONObject:trackedQuery
  642. options:0
  643. error:&error];
  644. NSAssert(data, @"Failed to serialize tracked query (Error: %@)", error);
  645. [self.serverCacheDB setData:data forKey:trackedQueryKey(query.queryId)];
  646. FFDebug(@"I-RDB076028", @"Saved tracked query %lu in %fms",
  647. (unsigned long)query.queryId, [start timeIntervalSinceNow] * -1000);
  648. }
  649. - (void)setTrackedQueryKeys:(NSSet *)keys forQueryId:(NSUInteger)queryId {
  650. NSDate *start = [NSDate date];
  651. __block NSUInteger removed = 0;
  652. __block NSUInteger added = 0;
  653. id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
  654. NSMutableSet *seenKeys = [NSMutableSet set];
  655. // First, delete any keys that might be stored and are not part of the
  656. // current keys
  657. [self.serverCacheDB
  658. enumerateKeysWithPrefix:trackedQueryKeysKeyPrefix(queryId)
  659. asStrings:^(NSString *dbKey, NSString *actualKey,
  660. BOOL *stop) {
  661. if ([keys containsObject:actualKey]) {
  662. // Already in DB
  663. [seenKeys addObject:actualKey];
  664. } else {
  665. // Not part of set, delete key
  666. [batch removeKey:dbKey];
  667. removed++;
  668. }
  669. }];
  670. // Next add any keys that are missing in the database
  671. [keys enumerateObjectsUsingBlock:^(NSString *childKey, BOOL *stop) {
  672. if (![seenKeys containsObject:childKey]) {
  673. [batch setString:childKey
  674. forKey:trackedQueryKeysKey(queryId, childKey)];
  675. added++;
  676. }
  677. }];
  678. BOOL success = [batch commit];
  679. if (!success) {
  680. FFWarn(@"I-RDB076029", @"Failed to set tracked queries on disk!");
  681. } else {
  682. FFDebug(@"I-RDB076030",
  683. @"Set %lu tracked keys (%lu added, %lu removed) for query %lu "
  684. @"in %fms",
  685. (unsigned long)keys.count, (unsigned long)added,
  686. (unsigned long)removed, (unsigned long)queryId,
  687. [start timeIntervalSinceNow] * -1000);
  688. }
  689. }
  690. - (void)updateTrackedQueryKeysWithAddedKeys:(NSSet *)added
  691. removedKeys:(NSSet *)removed
  692. forQueryId:(NSUInteger)queryId {
  693. NSDate *start = [NSDate date];
  694. id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
  695. [removed enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) {
  696. [batch removeKey:trackedQueryKeysKey(queryId, key)];
  697. }];
  698. [added enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) {
  699. [batch setString:key forKey:trackedQueryKeysKey(queryId, key)];
  700. }];
  701. BOOL success = [batch commit];
  702. if (!success) {
  703. FFWarn(@"I-RDB076031", @"Failed to update tracked queries on disk!");
  704. } else {
  705. FFDebug(@"I-RDB076032",
  706. @"Added %lu tracked keys, removed %lu for query %lu in %fms",
  707. (unsigned long)added.count, (unsigned long)removed.count,
  708. (unsigned long)queryId, [start timeIntervalSinceNow] * -1000);
  709. }
  710. }
  711. - (NSSet *)trackedQueryKeysForQuery:(NSUInteger)queryId {
  712. NSDate *start = [NSDate date];
  713. NSMutableSet *set = [NSMutableSet set];
  714. [self.serverCacheDB
  715. enumerateKeysWithPrefix:trackedQueryKeysKeyPrefix(queryId)
  716. asStrings:^(NSString *dbKey, NSString *actualKey,
  717. BOOL *stop) {
  718. [set addObject:actualKey];
  719. }];
  720. FFDebug(@"I-RDB076033", @"Loaded %lu tracked keys for query %lu in %fms",
  721. (unsigned long)set.count, (unsigned long)queryId,
  722. [start timeIntervalSinceNow] * -1000);
  723. return set;
  724. }
  725. #pragma mark - Internal methods
  726. - (void)removeAllLeafNodesOnPath:(FPath *)path
  727. batch:(id<APLevelDBWriteBatch>)batch {
  728. while (!path.isEmpty) {
  729. [batch removeKey:serverCacheKey(path)];
  730. path = [path parent];
  731. }
  732. // Make sure to delete any nodes at the root
  733. [batch removeKey:serverCacheKey([FPath empty])];
  734. }
  735. - (void)removeAllWithPrefix:(NSString *)prefix
  736. batch:(id<APLevelDBWriteBatch>)batch
  737. database:(APLevelDB *)database {
  738. assert(prefix != nil);
  739. [database enumerateKeysWithPrefix:prefix
  740. usingBlock:^(NSString *key, BOOL *stop) {
  741. [batch removeKey:key];
  742. }];
  743. }
  744. #pragma mark - Internal helper methods
  745. - (void)internalSetNestedData:(id)value
  746. forKey:(NSString *)key
  747. withBatch:(id<APLevelDBWriteBatch>)batch
  748. counter:(NSUInteger *)counter {
  749. if ([value isKindOfClass:[NSDictionary class]]) {
  750. NSDictionary *dictionary = value;
  751. [dictionary enumerateKeysAndObjectsUsingBlock:^(id childKey, id obj,
  752. BOOL *stop) {
  753. assert(obj != nil);
  754. NSString *childPath =
  755. [NSString stringWithFormat:@"%@%@/", key, childKey];
  756. [self internalSetNestedData:obj
  757. forKey:childPath
  758. withBatch:batch
  759. counter:counter];
  760. }];
  761. } else {
  762. NSData *data = [self serializePrimitive:value];
  763. [batch setData:data forKey:key];
  764. (*counter)++;
  765. }
  766. }
  767. - (id)internalNestedDataForPath:(FPath *)path {
  768. NSAssert(path != nil, @"Path was nil!");
  769. NSString *baseKey = serverCacheKey(path);
  770. // HACK to make sure iter is freed now to avoid race conditions (if self.db
  771. // is deleted before iter, you get an access violation).
  772. @autoreleasepool {
  773. APLevelDBIterator *iter =
  774. [APLevelDBIterator iteratorWithLevelDB:self.serverCacheDB];
  775. [iter seekToKey:baseKey];
  776. if (iter.key == nil || ![iter.key hasPrefix:baseKey]) {
  777. // No data.
  778. return nil;
  779. } else {
  780. return [self internalNestedDataFromIterator:iter
  781. andKeyPrefix:baseKey];
  782. }
  783. }
  784. }
  785. - (id)internalNestedDataFromIterator:(APLevelDBIterator *)iterator
  786. andKeyPrefix:(NSString *)prefix {
  787. NSString *key = iterator.key;
  788. if ([key isEqualToString:prefix]) {
  789. id result = [self deserializePrimitive:iterator.valueAsData];
  790. [iterator nextKey];
  791. return result;
  792. } else {
  793. NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
  794. while (key != nil && [key hasPrefix:prefix]) {
  795. NSString *relativePath = [key substringFromIndex:prefix.length];
  796. NSArray *pathPieces =
  797. [relativePath componentsSeparatedByString:@"/"];
  798. assert(pathPieces.count > 0);
  799. NSString *childName = pathPieces[0];
  800. NSString *childPath =
  801. [NSString stringWithFormat:@"%@%@/", prefix, childName];
  802. id childValue = [self internalNestedDataFromIterator:iterator
  803. andKeyPrefix:childPath];
  804. [dict setValue:childValue forKey:childName];
  805. key = iterator.key;
  806. }
  807. return dict;
  808. }
  809. }
  810. - (NSData *)serializePrimitive:(id)value {
  811. // HACK: The built-in serialization only works on dicts and arrays. So we
  812. // create an array and then strip off the leading / trailing byte (the [ and
  813. // ]).
  814. NSError *error = nil;
  815. NSData *data = [NSJSONSerialization dataWithJSONObject:@[ value ]
  816. options:0
  817. error:&error];
  818. NSAssert(data, @"Failed to serialize primitive: %@", error);
  819. return [data subdataWithRange:NSMakeRange(1, data.length - 2)];
  820. }
  821. - (id)fixDoubleParsing:(id)value
  822. __attribute__((no_sanitize("float-cast-overflow"))) {
  823. if ([value isKindOfClass:[NSDecimalNumber class]]) {
  824. // In case the value is an NSDecimalNumber, we may be dealing with
  825. // precisions that are higher than what can be represented in a double.
  826. // In this case it does not suffice to check for integral numbers by
  827. // casting the [value doubleValue] to an int64_t, because this will
  828. // cause the compared values to be rounded to double precision.
  829. // Coupled with a bug in [NSDecimalNumber longLongValue] that triggers
  830. // when converting values with high precision, this would cause
  831. // values of high precision, but with an integral 'doubleValue'
  832. // representation to be converted to bogus values.
  833. // A radar for the NSDecimalNumber issue can be found here:
  834. // http://www.openradar.me/radar?id=5007005597040640
  835. // Consider the NSDecimalNumber value: 999.9999999999999487
  836. // This number has a 'doubleValue' of 1000. Using the previous version
  837. // of this method would cause the value to be interpreted to be integral
  838. // and then the resulting value would be based on the longLongValue
  839. // which due to the NSDecimalNumber issue would turn out as -844.
  840. // By using NSDecimal logic to test for integral values,
  841. // 999.9999999999999487 will not be considered integral, and instead
  842. // of triggering the 'longLongValue' issue, it will be returned as
  843. // the 'doubleValue' representation (1000).
  844. // Please note, that even without the NSDecimalNumber issue, the
  845. // 'correct' longLongValue of 999.9999999999999487 is 999 and not 1000,
  846. // so the previous code would cause issues even without the bug
  847. // referenced in the radar.
  848. NSDecimal original = [(NSDecimalNumber *)value decimalValue];
  849. NSDecimal rounded;
  850. NSDecimalRound(&rounded, &original, 0, NSRoundPlain);
  851. if (NSDecimalCompare(&original, &rounded) != NSOrderedSame) {
  852. NSString *doubleString = [value stringValue];
  853. return [NSNumber numberWithDouble:[doubleString doubleValue]];
  854. } else {
  855. return [NSNumber numberWithLongLong:[value longLongValue]];
  856. }
  857. } else if ([value isKindOfClass:[NSNumber class]]) {
  858. // The parser for double values in JSONSerialization at the root takes
  859. // some short-cuts and delivers wrong results (wrong rounding) for some
  860. // double values, including 2.47. Because we use the exact bytes for
  861. // hashing on the server this will lead to hash mismatches. The parser
  862. // of NSNumber seems to be more in line with what the server expects, so
  863. // we use that here
  864. CFNumberType type = CFNumberGetType((CFNumberRef)value);
  865. if (type == kCFNumberDoubleType || type == kCFNumberFloatType) {
  866. // The NSJSON parser returns all numbers as double values, even
  867. // those that contain no exponent. To make sure that the String
  868. // conversion below doesn't unexpectedly reduce precision, we make
  869. // sure that our number is indeed not an integer.
  870. if ((double)(int64_t)[value doubleValue] != [value doubleValue]) {
  871. NSString *doubleString = [value stringValue];
  872. return [NSNumber numberWithDouble:[doubleString doubleValue]];
  873. } else {
  874. return [NSNumber numberWithLongLong:[value longLongValue]];
  875. }
  876. }
  877. }
  878. return value;
  879. }
  880. - (id)deserializePrimitive:(NSData *)data {
  881. NSError *error = nil;
  882. id result =
  883. [NSJSONSerialization JSONObjectWithData:data
  884. options:NSJSONReadingAllowFragments
  885. error:&error];
  886. if (result != nil) {
  887. return [self fixDoubleParsing:result];
  888. } else {
  889. if (error.code == kFNanFailureCode) {
  890. FFWarn(@"I-RDB076034",
  891. @"Failed to load primitive %@, likely because doubles where "
  892. @"out of range (Error: %@)",
  893. [[NSString alloc] initWithData:data
  894. encoding:NSUTF8StringEncoding],
  895. error);
  896. return [NSNull null];
  897. } else {
  898. [NSException raise:NSInternalInconsistencyException
  899. format:@"Failed to deserialiaze primitive: %@", error];
  900. return nil;
  901. }
  902. }
  903. }
  904. + (void)ensureDir:(NSString *)path markAsDoNotBackup:(BOOL)markAsDoNotBackup {
  905. NSError *error;
  906. BOOL success =
  907. [[NSFileManager defaultManager] createDirectoryAtPath:path
  908. withIntermediateDirectories:YES
  909. attributes:nil
  910. error:&error];
  911. if (!success) {
  912. @throw [NSException
  913. exceptionWithName:@"FailedToCreatePersistenceDir"
  914. reason:@"Failed to create persistence directory."
  915. userInfo:@{@"path" : path}];
  916. }
  917. if (markAsDoNotBackup) {
  918. NSURL *firebaseDirURL = [NSURL fileURLWithPath:path];
  919. success = [firebaseDirURL setResourceValue:@YES
  920. forKey:NSURLIsExcludedFromBackupKey
  921. error:&error];
  922. if (!success) {
  923. FFWarn(
  924. @"I-RDB076035",
  925. @"Failed to mark firebase database folder as do not backup: %@",
  926. error);
  927. [NSException raise:@"Error marking as do not backup"
  928. format:@"Failed to mark folder %@ as do not backup",
  929. firebaseDirURL];
  930. }
  931. }
  932. }
  933. @end