FLevelDBStorageEngine.m 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010
  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. NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(
  253. NSDocumentDirectory, NSUserDomainMask, YES);
  254. NSString *documentsDir = [dirPaths objectAtIndex:0];
  255. return [documentsDir stringByAppendingPathComponent:@"firebase"];
  256. #elif TARGET_OS_TV
  257. NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(
  258. NSCachesDirectory, NSUserDomainMask, YES);
  259. NSString *cachesDir = [dirPaths objectAtIndex:0];
  260. return [cachesDir stringByAppendingPathComponent:@"firebase"];
  261. #elif TARGET_OS_OSX
  262. return [NSHomeDirectory() stringByAppendingPathComponent:@".firebase"];
  263. #endif
  264. }
  265. - (APLevelDB *)createDB:(NSString *)dbName {
  266. NSError *err = nil;
  267. NSString *path = [self.basePath stringByAppendingPathComponent:dbName];
  268. APLevelDB *db = [APLevelDB levelDBWithPath:path error:&err];
  269. if (err) {
  270. FFWarn(@"I-RDB076036",
  271. @"Failed to read database persistence file '%@': %@", dbName,
  272. [err localizedDescription]);
  273. err = nil;
  274. // Delete the database and try again.
  275. [self purgeDatabase:dbName];
  276. db = [APLevelDB levelDBWithPath:path error:&err];
  277. if (err) {
  278. NSString *reason = [NSString
  279. stringWithFormat:@"Error initializing persistence: %@",
  280. [err description]];
  281. @throw [NSException
  282. exceptionWithName:@"FirebaseDatabasePersistenceFailure"
  283. reason:reason
  284. userInfo:nil];
  285. }
  286. }
  287. return db;
  288. }
  289. - (void)saveUserOverwrite:(id<FNode>)node
  290. atPath:(FPath *)path
  291. writeId:(NSUInteger)writeId {
  292. NSDictionary *write = @{
  293. kFUserWriteId : @(writeId),
  294. kFUserWritePath : [path toStringWithTrailingSlash],
  295. kFUserWriteOverwrite : [node valForExport:YES]
  296. };
  297. NSError *error = nil;
  298. NSData *data = [NSJSONSerialization dataWithJSONObject:write
  299. options:0
  300. error:&error];
  301. NSAssert(data, @"Failed to serialize user overwrite: %@, (Error: %@)",
  302. write, error);
  303. [self.writesDB setData:data forKey:writeRecordKey(writeId)];
  304. }
  305. - (void)saveUserMerge:(FCompoundWrite *)merge
  306. atPath:(FPath *)path
  307. writeId:(NSUInteger)writeId {
  308. NSDictionary *write = @{
  309. kFUserWriteId : @(writeId),
  310. kFUserWritePath : [path toStringWithTrailingSlash],
  311. kFUserWriteMerge : [merge valForExport:YES]
  312. };
  313. NSError *error = nil;
  314. NSData *data = [NSJSONSerialization dataWithJSONObject:write
  315. options:0
  316. error:&error];
  317. NSAssert(data, @"Failed to serialize user merge: %@ (Error: %@)", write,
  318. error);
  319. [self.writesDB setData:data forKey:writeRecordKey(writeId)];
  320. }
  321. - (void)removeUserWrite:(NSUInteger)writeId {
  322. [self.writesDB removeKey:writeRecordKey(writeId)];
  323. }
  324. - (void)removeAllUserWrites {
  325. __block NSUInteger count = 0;
  326. NSDate *start = [NSDate date];
  327. id<APLevelDBWriteBatch> batch = [self.writesDB beginWriteBatch];
  328. [self.writesDB enumerateKeys:^(NSString *key, BOOL *stop) {
  329. [batch removeKey:key];
  330. count++;
  331. }];
  332. BOOL success = [batch commit];
  333. if (!success) {
  334. FFWarn(@"I-RDB076010", @"Failed to remove all users writes on disk!");
  335. } else {
  336. FFDebug(@"I-RDB076011", @"Removed %lu writes in %fms",
  337. (unsigned long)count, [start timeIntervalSinceNow] * -1000);
  338. }
  339. }
  340. - (NSArray *)userWrites {
  341. NSDate *date = [NSDate date];
  342. NSMutableArray *writes = [NSMutableArray array];
  343. [self.writesDB enumerateKeysAndValuesAsData:^(NSString *key, NSData *data,
  344. BOOL *stop) {
  345. NSError *error = nil;
  346. NSDictionary *writeJSON = [NSJSONSerialization JSONObjectWithData:data
  347. options:0
  348. error:&error];
  349. if (writeJSON == nil) {
  350. if (error.code == kFNanFailureCode) {
  351. FFWarn(@"I-RDB076012",
  352. @"Failed to deserialize write (%@), likely because of out "
  353. @"of range doubles (Error: %@)",
  354. [[NSString alloc] initWithData:data
  355. encoding:NSUTF8StringEncoding],
  356. error);
  357. FFWarn(@"I-RDB076013", @"Removing failed write with key %@", key);
  358. [self.writesDB removeKey:key];
  359. } else {
  360. [NSException raise:NSInternalInconsistencyException
  361. format:@"Failed to deserialize write: %@", error];
  362. }
  363. } else {
  364. NSInteger writeId =
  365. ((NSNumber *)writeJSON[kFUserWriteId]).integerValue;
  366. FPath *path = [FPath pathWithString:writeJSON[kFUserWritePath]];
  367. FWriteRecord *writeRecord;
  368. if (writeJSON[kFUserWriteMerge] != nil) {
  369. // It's a merge
  370. FCompoundWrite *merge = [FCompoundWrite
  371. compoundWriteWithValueDictionary:writeJSON[kFUserWriteMerge]];
  372. writeRecord = [[FWriteRecord alloc] initWithPath:path
  373. merge:merge
  374. writeId:writeId];
  375. } else {
  376. // It's an overwrite
  377. NSAssert(writeJSON[kFUserWriteOverwrite] != nil,
  378. @"Persisted write did not contain merge or overwrite!");
  379. id<FNode> node =
  380. [FSnapshotUtilities nodeFrom:writeJSON[kFUserWriteOverwrite]];
  381. writeRecord = [[FWriteRecord alloc] initWithPath:path
  382. overwrite:node
  383. writeId:writeId
  384. visible:YES];
  385. }
  386. [writes addObject:writeRecord];
  387. }
  388. }];
  389. // Make sure writes are sorted
  390. [writes sortUsingComparator:^NSComparisonResult(FWriteRecord *one,
  391. FWriteRecord *two) {
  392. if (one.writeId < two.writeId) {
  393. return NSOrderedAscending;
  394. } else if (one.writeId > two.writeId) {
  395. return NSOrderedDescending;
  396. } else {
  397. return NSOrderedSame;
  398. }
  399. }];
  400. FFDebug(@"I-RDB076014", @"Loaded %lu writes in %fms",
  401. (unsigned long)writes.count, [date timeIntervalSinceNow] * -1000);
  402. return writes;
  403. }
  404. - (id<FNode>)serverCacheAtPath:(FPath *)path {
  405. NSDate *start = [NSDate date];
  406. id data = [self internalNestedDataForPath:path];
  407. id<FNode> node = [FSnapshotUtilities nodeFrom:data];
  408. FFDebug(@"I-RDB076015", @"Loaded node with %d children at %@ in %fms",
  409. [node numChildren], path, [start timeIntervalSinceNow] * -1000);
  410. return node;
  411. }
  412. - (id<FNode>)serverCacheForKeys:(NSSet *)keys atPath:(FPath *)path {
  413. NSDate *start = [NSDate date];
  414. __block id<FNode> node = [FEmptyNode emptyNode];
  415. [keys enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) {
  416. id data = [self internalNestedDataForPath:[path childFromString:key]];
  417. node = [node updateImmediateChild:key
  418. withNewChild:[FSnapshotUtilities nodeFrom:data]];
  419. }];
  420. FFDebug(@"I-RDB076016",
  421. @"Loaded node with %d children for %lu keys at %@ in %fms",
  422. [node numChildren], (unsigned long)keys.count, path,
  423. [start timeIntervalSinceNow] * -1000);
  424. return node;
  425. }
  426. - (void)updateServerCache:(id<FNode>)node
  427. atPath:(FPath *)path
  428. merge:(BOOL)merge {
  429. NSDate *start = [NSDate date];
  430. id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
  431. // Remove any leaf nodes that might be higher up
  432. [self removeAllLeafNodesOnPath:path batch:batch];
  433. __block NSUInteger counter = 0;
  434. if (merge) {
  435. // remove any children that exist
  436. [node enumerateChildrenUsingBlock:^(NSString *childKey,
  437. id<FNode> childNode, BOOL *stop) {
  438. FPath *childPath = [path childFromString:childKey];
  439. [self removeAllWithPrefix:serverCacheKey(childPath)
  440. batch:batch
  441. database:self.serverCacheDB];
  442. [self saveNodeInternal:childNode
  443. atPath:childPath
  444. batch:batch
  445. counter:&counter];
  446. }];
  447. } else {
  448. // remove everything
  449. [self removeAllWithPrefix:serverCacheKey(path)
  450. batch:batch
  451. database:self.serverCacheDB];
  452. [self saveNodeInternal:node atPath:path batch:batch counter:&counter];
  453. }
  454. BOOL success = [batch commit];
  455. if (!success) {
  456. FFWarn(@"I-RDB076017", @"Failed to update server cache on disk!");
  457. } else {
  458. FFDebug(@"I-RDB076018", @"Saved %lu leaf nodes for overwrite in %fms",
  459. (unsigned long)counter, [start timeIntervalSinceNow] * -1000);
  460. }
  461. }
  462. - (void)updateServerCacheWithMerge:(FCompoundWrite *)merge
  463. atPath:(FPath *)path {
  464. NSDate *start = [NSDate date];
  465. __block NSUInteger counter = 0;
  466. id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
  467. // Remove any leaf nodes that might be higher up
  468. [self removeAllLeafNodesOnPath:path batch:batch];
  469. [merge enumerateWrites:^(FPath *relativePath, id<FNode> node, BOOL *stop) {
  470. FPath *childPath = [path child:relativePath];
  471. [self removeAllWithPrefix:serverCacheKey(childPath)
  472. batch:batch
  473. database:self.serverCacheDB];
  474. [self saveNodeInternal:node
  475. atPath:childPath
  476. batch:batch
  477. counter:&counter];
  478. }];
  479. BOOL success = [batch commit];
  480. if (!success) {
  481. FFWarn(@"I-RDB076019", @"Failed to update server cache on disk!");
  482. } else {
  483. FFDebug(@"I-RDB076020", @"Saved %lu leaf nodes for merge in %fms",
  484. (unsigned long)counter, [start timeIntervalSinceNow] * -1000);
  485. }
  486. }
  487. - (void)saveNodeInternal:(id<FNode>)node
  488. atPath:(FPath *)path
  489. batch:(id<APLevelDBWriteBatch>)batch
  490. counter:(NSUInteger *)counter {
  491. id data = [node valForExport:YES];
  492. if (data != nil && ![data isKindOfClass:[NSNull class]]) {
  493. [self internalSetNestedData:data
  494. forKey:serverCacheKey(path)
  495. withBatch:batch
  496. counter:counter];
  497. }
  498. }
  499. - (NSUInteger)serverCacheEstimatedSizeInBytes {
  500. // Use the exact size, because for pruning the approximate size can lead to
  501. // weird situations where we prune everything because no compaction is ever
  502. // run
  503. return [self.serverCacheDB exactSizeFrom:kFServerCachePrefix
  504. to:kFServerCacheRangeEnd];
  505. }
  506. - (void)pruneCache:(FPruneForest *)pruneForest atPath:(FPath *)path {
  507. // TODO: be more intelligent, don't scan entire database...
  508. __block NSUInteger pruned = 0;
  509. __block NSUInteger kept = 0;
  510. NSDate *start = [NSDate date];
  511. NSString *prefix = serverCacheKey(path);
  512. id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
  513. [self.serverCacheDB
  514. enumerateKeysWithPrefix:prefix
  515. usingBlock:^(NSString *dbKey, BOOL *stop) {
  516. NSString *pathStr =
  517. [dbKey substringFromIndex:prefix.length];
  518. FPath *relativePath = [[FPath alloc] initWith:pathStr];
  519. if ([pruneForest shouldPruneUnkeptDescendantsAtPath:
  520. relativePath]) {
  521. pruned++;
  522. [batch removeKey:dbKey];
  523. } else {
  524. kept++;
  525. }
  526. }];
  527. BOOL success = [batch commit];
  528. if (!success) {
  529. FFWarn(@"I-RDB076021", @"Failed to prune cache on disk!");
  530. } else {
  531. FFDebug(@"I-RDB076022", @"Pruned %lu paths, kept %lu paths in %fms",
  532. (unsigned long)pruned, (unsigned long)kept,
  533. [start timeIntervalSinceNow] * -1000);
  534. }
  535. }
  536. #pragma mark - Tracked Queries
  537. - (NSArray *)loadTrackedQueries {
  538. NSDate *date = [NSDate date];
  539. NSMutableArray *trackedQueries = [NSMutableArray array];
  540. [self.serverCacheDB
  541. enumerateKeysWithPrefix:kFTrackedQueriesPrefix
  542. asData:^(NSString *key, NSData *data, BOOL *stop) {
  543. NSError *error = nil;
  544. NSDictionary *queryJSON =
  545. [NSJSONSerialization JSONObjectWithData:data
  546. options:0
  547. error:&error];
  548. if (queryJSON == nil) {
  549. if (error.code == kFNanFailureCode) {
  550. FFWarn(
  551. @"I-RDB076023",
  552. @"Failed to deserialize tracked query "
  553. @"(%@), likely because of out of range "
  554. @"doubles (Error: %@)",
  555. [[NSString alloc]
  556. initWithData:data
  557. encoding:NSUTF8StringEncoding],
  558. error);
  559. FFWarn(@"I-RDB076024",
  560. @"Removing failed tracked query with "
  561. @"key %@",
  562. key);
  563. [self.serverCacheDB removeKey:key];
  564. } else {
  565. [NSException
  566. raise:NSInternalInconsistencyException
  567. format:@"Failed to deserialize tracked "
  568. @"query: %@",
  569. error];
  570. }
  571. } else {
  572. NSUInteger queryId =
  573. ((NSNumber *)queryJSON[kFTrackedQueryId])
  574. .unsignedIntegerValue;
  575. FPath *path =
  576. [FPath pathWithString:
  577. queryJSON[kFTrackedQueryPath]];
  578. FQueryParams *params = [FQueryParams
  579. fromQueryObject:queryJSON
  580. [kFTrackedQueryParams]];
  581. FQuerySpec *query =
  582. [[FQuerySpec alloc] initWithPath:path
  583. params:params];
  584. BOOL isComplete =
  585. [queryJSON[kFTrackedQueryIsComplete]
  586. boolValue];
  587. BOOL isActive =
  588. [queryJSON[kFTrackedQueryIsActive]
  589. boolValue];
  590. NSTimeInterval lastUse =
  591. [queryJSON[kFTrackedQueryLastUse]
  592. doubleValue];
  593. FTrackedQuery *trackedQuery =
  594. [[FTrackedQuery alloc]
  595. initWithId:queryId
  596. query:query
  597. lastUse:lastUse
  598. isActive:isActive
  599. isComplete:isComplete];
  600. [trackedQueries addObject:trackedQuery];
  601. }
  602. }];
  603. FFDebug(@"I-RDB076025", @"Loaded %lu tracked queries in %fms",
  604. (unsigned long)trackedQueries.count,
  605. [date timeIntervalSinceNow] * -1000);
  606. return trackedQueries;
  607. }
  608. - (void)removeTrackedQuery:(NSUInteger)queryId {
  609. NSDate *start = [NSDate date];
  610. id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
  611. [batch removeKey:trackedQueryKey(queryId)];
  612. __block NSUInteger keyCount = 0;
  613. [self.serverCacheDB
  614. enumerateKeysWithPrefix:trackedQueryKeysKeyPrefix(queryId)
  615. usingBlock:^(NSString *key, BOOL *stop) {
  616. [batch removeKey:key];
  617. keyCount++;
  618. }];
  619. BOOL success = [batch commit];
  620. if (!success) {
  621. FFWarn(@"I-RDB076026", @"Failed to remove tracked query on disk!");
  622. } else {
  623. FFDebug(@"I-RDB076027",
  624. @"Removed query with id %lu (and removed %lu keys) in %fms",
  625. (unsigned long)queryId, (unsigned long)keyCount,
  626. [start timeIntervalSinceNow] * -1000);
  627. }
  628. }
  629. - (void)saveTrackedQuery:(FTrackedQuery *)query {
  630. NSDate *start = [NSDate date];
  631. NSDictionary *trackedQuery = @{
  632. kFTrackedQueryId : @(query.queryId),
  633. kFTrackedQueryPath : [query.query.path toStringWithTrailingSlash],
  634. kFTrackedQueryParams : [query.query.params wireProtocolParams],
  635. kFTrackedQueryLastUse : @(query.lastUse),
  636. kFTrackedQueryIsComplete : @(query.isComplete),
  637. kFTrackedQueryIsActive : @(query.isActive)
  638. };
  639. NSError *error = nil;
  640. NSData *data = [NSJSONSerialization dataWithJSONObject:trackedQuery
  641. options:0
  642. error:&error];
  643. NSAssert(data, @"Failed to serialize tracked query (Error: %@)", error);
  644. [self.serverCacheDB setData:data forKey:trackedQueryKey(query.queryId)];
  645. FFDebug(@"I-RDB076028", @"Saved tracked query %lu in %fms",
  646. (unsigned long)query.queryId, [start timeIntervalSinceNow] * -1000);
  647. }
  648. - (void)setTrackedQueryKeys:(NSSet *)keys forQueryId:(NSUInteger)queryId {
  649. NSDate *start = [NSDate date];
  650. __block NSUInteger removed = 0;
  651. __block NSUInteger added = 0;
  652. id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
  653. NSMutableSet *seenKeys = [NSMutableSet set];
  654. // First, delete any keys that might be stored and are not part of the
  655. // current keys
  656. [self.serverCacheDB
  657. enumerateKeysWithPrefix:trackedQueryKeysKeyPrefix(queryId)
  658. asStrings:^(NSString *dbKey, NSString *actualKey,
  659. BOOL *stop) {
  660. if ([keys containsObject:actualKey]) {
  661. // Already in DB
  662. [seenKeys addObject:actualKey];
  663. } else {
  664. // Not part of set, delete key
  665. [batch removeKey:dbKey];
  666. removed++;
  667. }
  668. }];
  669. // Next add any keys that are missing in the database
  670. [keys enumerateObjectsUsingBlock:^(NSString *childKey, BOOL *stop) {
  671. if (![seenKeys containsObject:childKey]) {
  672. [batch setString:childKey
  673. forKey:trackedQueryKeysKey(queryId, childKey)];
  674. added++;
  675. }
  676. }];
  677. BOOL success = [batch commit];
  678. if (!success) {
  679. FFWarn(@"I-RDB076029", @"Failed to set tracked queries on disk!");
  680. } else {
  681. FFDebug(@"I-RDB076030",
  682. @"Set %lu tracked keys (%lu added, %lu removed) for query %lu "
  683. @"in %fms",
  684. (unsigned long)keys.count, (unsigned long)added,
  685. (unsigned long)removed, (unsigned long)queryId,
  686. [start timeIntervalSinceNow] * -1000);
  687. }
  688. }
  689. - (void)updateTrackedQueryKeysWithAddedKeys:(NSSet *)added
  690. removedKeys:(NSSet *)removed
  691. forQueryId:(NSUInteger)queryId {
  692. NSDate *start = [NSDate date];
  693. id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
  694. [removed enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) {
  695. [batch removeKey:trackedQueryKeysKey(queryId, key)];
  696. }];
  697. [added enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) {
  698. [batch setString:key forKey:trackedQueryKeysKey(queryId, key)];
  699. }];
  700. BOOL success = [batch commit];
  701. if (!success) {
  702. FFWarn(@"I-RDB076031", @"Failed to update tracked queries on disk!");
  703. } else {
  704. FFDebug(@"I-RDB076032",
  705. @"Added %lu tracked keys, removed %lu for query %lu in %fms",
  706. (unsigned long)added.count, (unsigned long)removed.count,
  707. (unsigned long)queryId, [start timeIntervalSinceNow] * -1000);
  708. }
  709. }
  710. - (NSSet *)trackedQueryKeysForQuery:(NSUInteger)queryId {
  711. NSDate *start = [NSDate date];
  712. NSMutableSet *set = [NSMutableSet set];
  713. [self.serverCacheDB
  714. enumerateKeysWithPrefix:trackedQueryKeysKeyPrefix(queryId)
  715. asStrings:^(NSString *dbKey, NSString *actualKey,
  716. BOOL *stop) {
  717. [set addObject:actualKey];
  718. }];
  719. FFDebug(@"I-RDB076033", @"Loaded %lu tracked keys for query %lu in %fms",
  720. (unsigned long)set.count, (unsigned long)queryId,
  721. [start timeIntervalSinceNow] * -1000);
  722. return set;
  723. }
  724. #pragma mark - Internal methods
  725. - (void)removeAllLeafNodesOnPath:(FPath *)path
  726. batch:(id<APLevelDBWriteBatch>)batch {
  727. while (!path.isEmpty) {
  728. [batch removeKey:serverCacheKey(path)];
  729. path = [path parent];
  730. }
  731. // Make sure to delete any nodes at the root
  732. [batch removeKey:serverCacheKey([FPath empty])];
  733. }
  734. - (void)removeAllWithPrefix:(NSString *)prefix
  735. batch:(id<APLevelDBWriteBatch>)batch
  736. database:(APLevelDB *)database {
  737. assert(prefix != nil);
  738. [database enumerateKeysWithPrefix:prefix
  739. usingBlock:^(NSString *key, BOOL *stop) {
  740. [batch removeKey:key];
  741. }];
  742. }
  743. #pragma mark - Internal helper methods
  744. - (void)internalSetNestedData:(id)value
  745. forKey:(NSString *)key
  746. withBatch:(id<APLevelDBWriteBatch>)batch
  747. counter:(NSUInteger *)counter {
  748. if ([value isKindOfClass:[NSDictionary class]]) {
  749. NSDictionary *dictionary = value;
  750. [dictionary enumerateKeysAndObjectsUsingBlock:^(id childKey, id obj,
  751. BOOL *stop) {
  752. assert(obj != nil);
  753. NSString *childPath =
  754. [NSString stringWithFormat:@"%@%@/", key, childKey];
  755. [self internalSetNestedData:obj
  756. forKey:childPath
  757. withBatch:batch
  758. counter:counter];
  759. }];
  760. } else {
  761. NSData *data = [self serializePrimitive:value];
  762. [batch setData:data forKey:key];
  763. (*counter)++;
  764. }
  765. }
  766. - (id)internalNestedDataForPath:(FPath *)path {
  767. NSAssert(path != nil, @"Path was nil!");
  768. NSString *baseKey = serverCacheKey(path);
  769. // HACK to make sure iter is freed now to avoid race conditions (if self.db
  770. // is deleted before iter, you get an access violation).
  771. @autoreleasepool {
  772. APLevelDBIterator *iter =
  773. [APLevelDBIterator iteratorWithLevelDB:self.serverCacheDB];
  774. [iter seekToKey:baseKey];
  775. if (iter.key == nil || ![iter.key hasPrefix:baseKey]) {
  776. // No data.
  777. return nil;
  778. } else {
  779. return [self internalNestedDataFromIterator:iter
  780. andKeyPrefix:baseKey];
  781. }
  782. }
  783. }
  784. - (id)internalNestedDataFromIterator:(APLevelDBIterator *)iterator
  785. andKeyPrefix:(NSString *)prefix {
  786. NSString *key = iterator.key;
  787. if ([key isEqualToString:prefix]) {
  788. id result = [self deserializePrimitive:iterator.valueAsData];
  789. [iterator nextKey];
  790. return result;
  791. } else {
  792. NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
  793. while (key != nil && [key hasPrefix:prefix]) {
  794. NSString *relativePath = [key substringFromIndex:prefix.length];
  795. NSArray *pathPieces =
  796. [relativePath componentsSeparatedByString:@"/"];
  797. assert(pathPieces.count > 0);
  798. NSString *childName = pathPieces[0];
  799. NSString *childPath =
  800. [NSString stringWithFormat:@"%@%@/", prefix, childName];
  801. id childValue = [self internalNestedDataFromIterator:iterator
  802. andKeyPrefix:childPath];
  803. [dict setValue:childValue forKey:childName];
  804. key = iterator.key;
  805. }
  806. return dict;
  807. }
  808. }
  809. - (NSData *)serializePrimitive:(id)value {
  810. // HACK: The built-in serialization only works on dicts and arrays. So we
  811. // create an array and then strip off the leading / trailing byte (the [ and
  812. // ]).
  813. NSError *error = nil;
  814. NSData *data = [NSJSONSerialization dataWithJSONObject:@[ value ]
  815. options:0
  816. error:&error];
  817. NSAssert(data, @"Failed to serialize primitive: %@", error);
  818. return [data subdataWithRange:NSMakeRange(1, data.length - 2)];
  819. }
  820. - (id)fixDoubleParsing:(id)value
  821. __attribute__((no_sanitize("float-cast-overflow"))) {
  822. if ([value isKindOfClass:[NSDecimalNumber class]]) {
  823. // In case the value is an NSDecimalNumber, we may be dealing with
  824. // precisions that are higher than what can be represented in a double.
  825. // In this case it does not suffice to check for integral numbers by
  826. // casting the [value doubleValue] to an int64_t, because this will
  827. // cause the compared values to be rounded to double precision.
  828. // Coupled with a bug in [NSDecimalNumber longLongValue] that triggers
  829. // when converting values with high precision, this would cause
  830. // values of high precision, but with an integral 'doubleValue'
  831. // representation to be converted to bogus values.
  832. // A radar for the NSDecimalNumber issue can be found here:
  833. // http://www.openradar.me/radar?id=5007005597040640
  834. // Consider the NSDecimalNumber value: 999.9999999999999487
  835. // This number has a 'doubleValue' of 1000. Using the previous version
  836. // of this method would cause the value to be interpreted to be integral
  837. // and then the resulting value would be based on the longLongValue
  838. // which due to the NSDecimalNumber issue would turn out as -844.
  839. // By using NSDecimal logic to test for integral values,
  840. // 999.9999999999999487 will not be considered integral, and instead
  841. // of triggering the 'longLongValue' issue, it will be returned as
  842. // the 'doubleValue' representation (1000).
  843. // Please note, that even without the NSDecimalNumber issue, the
  844. // 'correct' longLongValue of 999.9999999999999487 is 999 and not 1000,
  845. // so the previous code would cause issues even without the bug
  846. // referenced in the radar.
  847. NSDecimal original = [(NSDecimalNumber *)value decimalValue];
  848. NSDecimal rounded;
  849. NSDecimalRound(&rounded, &original, 0, NSRoundPlain);
  850. if (NSDecimalCompare(&original, &rounded) != NSOrderedSame) {
  851. NSString *doubleString = [value stringValue];
  852. return [NSNumber numberWithDouble:[doubleString doubleValue]];
  853. } else {
  854. return [NSNumber numberWithLongLong:[value longLongValue]];
  855. }
  856. } else if ([value isKindOfClass:[NSNumber class]]) {
  857. // The parser for double values in JSONSerialization at the root takes
  858. // some short-cuts and delivers wrong results (wrong rounding) for some
  859. // double values, including 2.47. Because we use the exact bytes for
  860. // hashing on the server this will lead to hash mismatches. The parser
  861. // of NSNumber seems to be more in line with what the server expects, so
  862. // we use that here
  863. CFNumberType type = CFNumberGetType((CFNumberRef)value);
  864. if (type == kCFNumberDoubleType || type == kCFNumberFloatType) {
  865. // The NSJSON parser returns all numbers as double values, even
  866. // those that contain no exponent. To make sure that the String
  867. // conversion below doesn't unexpectedly reduce precision, we make
  868. // sure that our number is indeed not an integer.
  869. if ((double)(int64_t)[value doubleValue] != [value doubleValue]) {
  870. NSString *doubleString = [value stringValue];
  871. return [NSNumber numberWithDouble:[doubleString doubleValue]];
  872. } else {
  873. return [NSNumber numberWithLongLong:[value longLongValue]];
  874. }
  875. }
  876. }
  877. return value;
  878. }
  879. - (id)deserializePrimitive:(NSData *)data {
  880. NSError *error = nil;
  881. id result =
  882. [NSJSONSerialization JSONObjectWithData:data
  883. options:NSJSONReadingAllowFragments
  884. error:&error];
  885. if (result != nil) {
  886. return [self fixDoubleParsing:result];
  887. } else {
  888. if (error.code == kFNanFailureCode) {
  889. FFWarn(@"I-RDB076034",
  890. @"Failed to load primitive %@, likely because doubles where "
  891. @"out of range (Error: %@)",
  892. [[NSString alloc] initWithData:data
  893. encoding:NSUTF8StringEncoding],
  894. error);
  895. return [NSNull null];
  896. } else {
  897. [NSException raise:NSInternalInconsistencyException
  898. format:@"Failed to deserialiaze primitive: %@", error];
  899. return nil;
  900. }
  901. }
  902. }
  903. + (void)ensureDir:(NSString *)path markAsDoNotBackup:(BOOL)markAsDoNotBackup {
  904. NSError *error;
  905. BOOL success =
  906. [[NSFileManager defaultManager] createDirectoryAtPath:path
  907. withIntermediateDirectories:YES
  908. attributes:nil
  909. error:&error];
  910. if (!success) {
  911. @throw [NSException
  912. exceptionWithName:@"FailedToCreatePersistenceDir"
  913. reason:@"Failed to create persistence directory."
  914. userInfo:@{@"path" : path}];
  915. }
  916. if (markAsDoNotBackup) {
  917. NSURL *firebaseDirURL = [NSURL fileURLWithPath:path];
  918. success = [firebaseDirURL setResourceValue:@YES
  919. forKey:NSURLIsExcludedFromBackupKey
  920. error:&error];
  921. if (!success) {
  922. FFWarn(
  923. @"I-RDB076035",
  924. @"Failed to mark firebase database folder as do not backup: %@",
  925. error);
  926. [NSException raise:@"Error marking as do not backup"
  927. format:@"Failed to mark folder %@ as do not backup",
  928. firebaseDirURL];
  929. }
  930. }
  931. }
  932. @end