Skip to content

Commit bb1a448

Browse files
authored
Implement global resume token (#1696)
1 parent bad531d commit bb1a448

File tree

5 files changed

+97
-9
lines changed

5 files changed

+97
-9
lines changed

Firestore/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Unreleased
22
- [fixed] Fixed an issue where changes to custom authentication claims did not
33
take effect until you did a full sign-out and sign-in. (#1499)
4+
- [changed] Improved how Firestore handles idle queries to reduce the cost of
5+
re-listening within 30 minutes.
46

57
# v0.13.1
68
- [fixed] Fixed an issue where `get(source:.Cache)` could throw an

Firestore/Example/Tests/SpecTests/FSTSpecTests.mm

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@
7878

7979
NSString *const kNoLRUTag = @"no-lru";
8080

81+
static NSString *Describe(NSData *data) {
82+
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
83+
}
84+
8185
@interface FSTSpecTests ()
8286
@property(nonatomic, strong) FSTSyncEngineTestDriver *driver;
8387

@@ -660,7 +664,7 @@ - (void)validateActiveTargets {
660664
XCTAssertEqualObjects(actual.query, queryData.query);
661665
XCTAssertEqual(actual.targetID, queryData.targetID);
662666
XCTAssertEqual(actual.snapshotVersion, queryData.snapshotVersion);
663-
XCTAssertEqualObjects(actual.resumeToken, queryData.resumeToken);
667+
XCTAssertEqualObjects(Describe(actual.resumeToken), Describe(queryData.resumeToken));
664668
}
665669

666670
[actualTargets removeObjectForKey:targetID];

Firestore/Example/Tests/SpecTests/json/listen_spec_test.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4060,7 +4060,7 @@
40604060
"Persists global resume tokens on unlisten": {
40614061
"describeName": "Listens:",
40624062
"itName": "Persists global resume tokens on unlisten",
4063-
"tags": ["no-ios"],
4063+
"tags": [],
40644064
"config": {
40654065
"useGarbageCollection": false,
40664066
"numClients": 1
@@ -4248,7 +4248,7 @@
42484248
"Omits global resume tokens for a short while": {
42494249
"describeName": "Listens:",
42504250
"itName": "Omits global resume tokens for a short while",
4251-
"tags": ["no-ios"],
4251+
"tags": [],
42524252
"config": {
42534253
"useGarbageCollection": false,
42544254
"numClients": 1
@@ -4423,7 +4423,7 @@
44234423
"Persists global resume tokens if the snapshot is old enough": {
44244424
"describeName": "Listens:",
44254425
"itName": "Persists global resume tokens if the snapshot is old enough",
4426-
"tags": ["no-ios"],
4426+
"tags": [],
44274427
"config": {
44284428
"useGarbageCollection": false,
44294429
"numClients": 1

Firestore/Source/Local/FSTLocalStore.mm

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@
5353

5454
NS_ASSUME_NONNULL_BEGIN
5555

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+
5664
@interface FSTLocalStore ()
5765

5866
/** Manages our in-memory or durable persistence. */
@@ -277,11 +285,15 @@ - (FSTMaybeDocumentDictionary *)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent {
277285
// than documents that were previously removed from this target.
278286
NSData *resumeToken = change.resumeToken;
279287
if (resumeToken.length > 0) {
288+
FSTQueryData *oldQueryData = queryData;
280289
queryData = [queryData queryDataByReplacingSnapshotVersion:remoteEvent.snapshotVersion
281290
resumeToken:resumeToken
282291
sequenceNumber:sequenceNumber];
283292
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+
}
285297
}
286298
}
287299

@@ -338,6 +350,43 @@ - (FSTMaybeDocumentDictionary *)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent {
338350
});
339351
}
340352

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+
341390
- (void)notifyLocalViewChanges:(NSArray<FSTLocalViewChanges *> *)viewChanges {
342391
self.persistence.run("NotifyLocalViewChanges", [&]() {
343392
FSTReferenceSet *localViewReferences = self.localViewReferences;
@@ -391,9 +440,21 @@ - (void)releaseQuery:(FSTQuery *)query {
391440
FSTQueryData *queryData = [self.queryCache queryDataForQuery:query];
392441
HARD_ASSERT(queryData, "Tried to release nonexistent query: %s", query);
393442

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];
395457
[self.persistence.referenceDelegate removeTarget:queryData];
396-
[self.targetIDs removeObjectForKey:@(queryData.targetID)];
397458

398459
// If this was the last watch target, then we won't get any more watch snapshots, so we should
399460
// release any held batch results.

Firestore/Source/Remote/FSTRemoteEvent.mm

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <set>
2121
#include <unordered_map>
2222
#include <utility>
23+
#include <vector>
2324

2425
#import "Firestore/Source/Core/FSTQuery.h"
2526
#import "Firestore/Source/Core/FSTViewSnapshot.h"
@@ -339,8 +340,7 @@ - (void)handleDocumentChange:(FSTDocumentWatchChange *)documentChange {
339340
}
340341

341342
- (void)handleTargetChange:(FSTWatchTargetChange *)targetChange {
342-
for (FSTBoxedTargetID *boxedTargetID in targetChange.targetIDs) {
343-
int targetID = boxedTargetID.intValue;
343+
for (TargetId targetID : [self targetIdsForChange:targetChange]) {
344344
FSTTargetState *targetState = [self ensureTargetStateForTarget:targetID];
345345
switch (targetChange.state) {
346346
case FSTWatchTargetChangeStateNoChange:
@@ -388,6 +388,27 @@ - (void)handleTargetChange:(FSTWatchTargetChange *)targetChange {
388388
}
389389
}
390390

391+
/**
392+
* Returns all targetIds that the watch change applies to: either the targetIds explicitly listed
393+
* in the change or the targetIds of all currently active targets.
394+
*/
395+
- (std::vector<TargetId>)targetIdsForChange:(FSTWatchTargetChange *)targetChange {
396+
NSArray<NSNumber *> *targetIDs = targetChange.targetIDs;
397+
std::vector<TargetId> result;
398+
if (targetIDs.count > 0) {
399+
result.reserve(targetIDs.count);
400+
for (NSNumber *targetID in targetIDs) {
401+
result.push_back(targetID.intValue);
402+
}
403+
} else {
404+
result.reserve(_targetStates.size());
405+
for (const auto &entry : _targetStates) {
406+
result.push_back(entry.first);
407+
}
408+
}
409+
return result;
410+
}
411+
391412
- (void)removeTarget:(TargetId)targetID {
392413
_targetStates.erase(targetID);
393414
}

0 commit comments

Comments
 (0)