|
53 | 53 |
|
54 | 54 | NS_ASSUME_NONNULL_BEGIN
|
55 | 55 |
|
| 56 | +/** |
| 57 | + * The maximum time to leave a resume token buffered without writing it out. This value is |
| 58 | + * arbitrary: it's long enough to avoid several writes (possibly indefinitely if updates come more |
| 59 | + * frequently than this) but short enough that restarting after crashing will still have a pretty |
| 60 | + * recent resume token. |
| 61 | + */ |
| 62 | +static const int64_t kResumeTokenMaxAgeSeconds = 5 * 60; // 5 minutes |
| 63 | + |
56 | 64 | @interface FSTLocalStore ()
|
57 | 65 |
|
58 | 66 | /** Manages our in-memory or durable persistence. */
|
@@ -277,11 +285,15 @@ - (FSTMaybeDocumentDictionary *)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent {
|
277 | 285 | // than documents that were previously removed from this target.
|
278 | 286 | NSData *resumeToken = change.resumeToken;
|
279 | 287 | if (resumeToken.length > 0) {
|
| 288 | + FSTQueryData *oldQueryData = queryData; |
280 | 289 | queryData = [queryData queryDataByReplacingSnapshotVersion:remoteEvent.snapshotVersion
|
281 | 290 | resumeToken:resumeToken
|
282 | 291 | sequenceNumber:sequenceNumber];
|
283 | 292 | self.targetIDs[boxedTargetID] = queryData;
|
284 |
| - [self.queryCache updateQueryData:queryData]; |
| 293 | + |
| 294 | + if ([self shouldPersistQueryData:queryData oldQueryData:oldQueryData change:change]) { |
| 295 | + [self.queryCache updateQueryData:queryData]; |
| 296 | + } |
285 | 297 | }
|
286 | 298 | }
|
287 | 299 |
|
@@ -338,6 +350,43 @@ - (FSTMaybeDocumentDictionary *)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent {
|
338 | 350 | });
|
339 | 351 | }
|
340 | 352 |
|
| 353 | +/** |
| 354 | + * Returns YES if the newQueryData should be persisted during an update of an active target. |
| 355 | + * QueryData should always be persisted when a target is being released and should not call this |
| 356 | + * function. |
| 357 | + * |
| 358 | + * While the target is active, QueryData updates can be omitted when nothing about the target has |
| 359 | + * changed except metadata like the resume token or snapshot version. Occasionally it's worth the |
| 360 | + * extra write to prevent these values from getting too stale after a crash, but this doesn't have |
| 361 | + * to be too frequent. |
| 362 | + */ |
| 363 | +- (BOOL)shouldPersistQueryData:(FSTQueryData *)newQueryData |
| 364 | + oldQueryData:(FSTQueryData *)oldQueryData |
| 365 | + change:(FSTTargetChange *)change { |
| 366 | + // Avoid clearing any existing value |
| 367 | + if (newQueryData.resumeToken.length == 0) return NO; |
| 368 | + |
| 369 | + // Any resume token is interesting if there isn't one already. |
| 370 | + if (oldQueryData.resumeToken.length == 0) return YES; |
| 371 | + |
| 372 | + // Don't allow resume token changes to be buffered indefinitely. This allows us to be reasonably |
| 373 | + // up-to-date after a crash and avoids needing to loop over all active queries on shutdown. |
| 374 | + // Especially in the browser we may not get time to do anything interesting while the current |
| 375 | + // tab is closing. |
| 376 | + int64_t newSeconds = newQueryData.snapshotVersion.timestamp().seconds(); |
| 377 | + int64_t oldSeconds = oldQueryData.snapshotVersion.timestamp().seconds(); |
| 378 | + int64_t timeDelta = newSeconds - oldSeconds; |
| 379 | + if (timeDelta >= kResumeTokenMaxAgeSeconds) return YES; |
| 380 | + |
| 381 | + // Otherwise if the only thing that has changed about a target is its resume token then it's not |
| 382 | + // worth persisting. Note that the RemoteStore keeps an in-memory view of the currently active |
| 383 | + // targets which includes the current resume token, so stream failure or user changes will still |
| 384 | + // use an up-to-date resume token regardless of what we do here. |
| 385 | + size_t changes = change.addedDocuments.size() + change.modifiedDocuments.size() + |
| 386 | + change.removedDocuments.size(); |
| 387 | + return changes > 0; |
| 388 | +} |
| 389 | + |
341 | 390 | - (void)notifyLocalViewChanges:(NSArray<FSTLocalViewChanges *> *)viewChanges {
|
342 | 391 | self.persistence.run("NotifyLocalViewChanges", [&]() {
|
343 | 392 | FSTReferenceSet *localViewReferences = self.localViewReferences;
|
@@ -391,9 +440,21 @@ - (void)releaseQuery:(FSTQuery *)query {
|
391 | 440 | FSTQueryData *queryData = [self.queryCache queryDataForQuery:query];
|
392 | 441 | HARD_ASSERT(queryData, "Tried to release nonexistent query: %s", query);
|
393 | 442 |
|
394 |
| - [self.localViewReferences removeReferencesForID:queryData.targetID]; |
| 443 | + TargetId targetID = queryData.targetID; |
| 444 | + FSTBoxedTargetID *boxedTargetID = @(targetID); |
| 445 | + |
| 446 | + FSTQueryData *cachedQueryData = self.targetIDs[boxedTargetID]; |
| 447 | + if (cachedQueryData.snapshotVersion > queryData.snapshotVersion) { |
| 448 | + // If we've been avoiding persisting the resumeToken (see shouldPersistQueryData for |
| 449 | + // conditions and rationale) we need to persist the token now because there will no |
| 450 | + // longer be an in-memory version to fall back on. |
| 451 | + queryData = cachedQueryData; |
| 452 | + [self.queryCache updateQueryData:queryData]; |
| 453 | + } |
| 454 | + |
| 455 | + [self.localViewReferences removeReferencesForID:targetID]; |
| 456 | + [self.targetIDs removeObjectForKey:boxedTargetID]; |
395 | 457 | [self.persistence.referenceDelegate removeTarget:queryData];
|
396 |
| - [self.targetIDs removeObjectForKey:@(queryData.targetID)]; |
397 | 458 |
|
398 | 459 | // If this was the last watch target, then we won't get any more watch snapshots, so we should
|
399 | 460 | // release any held batch results.
|
|
0 commit comments