From c17064a6c832c0169194dddf5963be24705f1386 Mon Sep 17 00:00:00 2001 From: Michael Lehenbauer Date: Mon, 26 Feb 2018 08:17:58 -0800 Subject: [PATCH] Add 10 second timeout waiting for connection before client behaves as-if offline. [Port of https://github.com/firebase/firebase-js-sdk/commit/0fa319e5e019dd0d40ab441d2ff9f8f6d4724e43] * Refactored FSTOnlineState tracking out of FSTRemoteStore and into new FSTOnlineStateTracker component. * Added a 10 second timeout to transition from OnlineState.Unknown to OnlineState.Offline rather than waiting indefinitely for the stream to succeed or fail. * Removed hack to run SpecTests using an FSTDispatchQueue that wrapped dispatch_get_main_queue(). This was incompatible with [FSTDispatchQueue runDelayedCallbacksUntil:] since it queues work and blocks waiting for it to complete. Now spec tests create / use a proper FSTDispatchQueue. * Added a SpecTest to verify OnlineState timeout behavior. * Misc cleanup: * Renamed FSTOnlineState states: Failed => Offline, Healthy => Online * Renamed FSTTimerIds (ListenStreamConnection => ListenStreamConnectionBackoff) * Added ability to run timers from spec tests. --- Firestore/CHANGELOG.md | 4 + .../Tests/Core/FSTEventManagerTests.mm | 8 +- .../Tests/Core/FSTQueryListenerTests.mm | 18 +-- .../Tests/Integration/FSTDatastoreTests.mm | 4 +- .../Example/Tests/SpecTests/FSTSpecTests.mm | 25 ++- .../Tests/SpecTests/FSTSyncEngineTestDriver.h | 6 + .../SpecTests/FSTSyncEngineTestDriver.mm | 95 +++++++---- .../SpecTests/json/offline_spec_test.json | 152 ++++++++++++++++++ .../Tests/Util/FSTDispatchQueueTests.mm | 4 +- Firestore/Source/Core/FSTEventManager.mm | 8 +- Firestore/Source/Core/FSTFirestoreClient.mm | 4 +- Firestore/Source/Core/FSTTypes.h | 19 ++- Firestore/Source/Core/FSTView.mm | 2 +- .../Source/Remote/FSTOnlineStateTracker.h | 71 ++++++++ .../Source/Remote/FSTOnlineStateTracker.mm | 149 +++++++++++++++++ Firestore/Source/Remote/FSTRemoteStore.h | 8 +- Firestore/Source/Remote/FSTRemoteStore.mm | 117 +++----------- Firestore/Source/Remote/FSTStream.mm | 4 +- Firestore/Source/Util/FSTDispatchQueue.h | 26 ++- Firestore/Source/Util/FSTDispatchQueue.mm | 4 + 20 files changed, 569 insertions(+), 159 deletions(-) create mode 100644 Firestore/Source/Remote/FSTOnlineStateTracker.h create mode 100644 Firestore/Source/Remote/FSTOnlineStateTracker.mm diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index b472f0c328f..9b35ea36763 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -1,4 +1,8 @@ # Unreleased +- [changed] If the SDK's attempt to connect to the Cloud Firestore backend + neither succeeds nor fails within 10 seconds, the SDK will consider itself + "offline", causing getDocument() calls to resolve with cached results, rather + than continuing to wait. # v0.10.2 - [changed] When you delete a FirebaseApp, the associated Firestore instances diff --git a/Firestore/Example/Tests/Core/FSTEventManagerTests.mm b/Firestore/Example/Tests/Core/FSTEventManagerTests.mm index fcde17dcfe6..f5f7b5bf282 100644 --- a/Firestore/Example/Tests/Core/FSTEventManagerTests.mm +++ b/Firestore/Example/Tests/Core/FSTEventManagerTests.mm @@ -143,9 +143,9 @@ - (void)testWillForwardOnlineStateChanges { .andDo(^(NSInvocation *invocation) { [events addObject:@(FSTOnlineStateUnknown)]; }); - OCMStub([fakeListener applyChangedOnlineState:FSTOnlineStateHealthy]) + OCMStub([fakeListener applyChangedOnlineState:FSTOnlineStateOnline]) .andDo(^(NSInvocation *invocation) { - [events addObject:@(FSTOnlineStateHealthy)]; + [events addObject:@(FSTOnlineStateOnline)]; }); FSTSyncEngine *syncEngineMock = OCMClassMock([FSTSyncEngine class]); @@ -154,8 +154,8 @@ - (void)testWillForwardOnlineStateChanges { [eventManager addListener:fakeListener]; XCTAssertEqualObjects(events, @[ @(FSTOnlineStateUnknown) ]); - [eventManager applyChangedOnlineState:FSTOnlineStateHealthy]; - XCTAssertEqualObjects(events, (@[ @(FSTOnlineStateUnknown), @(FSTOnlineStateHealthy) ])); + [eventManager applyChangedOnlineState:FSTOnlineStateOnline]; + XCTAssertEqualObjects(events, (@[ @(FSTOnlineStateUnknown), @(FSTOnlineStateOnline) ])); } @end diff --git a/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm b/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm index 4856b5fa462..1b263605ab7 100644 --- a/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm +++ b/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm @@ -340,10 +340,10 @@ - (void)testWillWaitForSyncIfOnline { [FSTTargetChange changeWithDocuments:@[ doc1, doc2 ] currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); - [listener applyChangedOnlineState:FSTOnlineStateHealthy]; // no event + [listener applyChangedOnlineState:FSTOnlineStateOnline]; // no event [listener queryDidChangeViewSnapshot:snap1]; [listener applyChangedOnlineState:FSTOnlineStateUnknown]; - [listener applyChangedOnlineState:FSTOnlineStateHealthy]; + [listener applyChangedOnlineState:FSTOnlineStateOnline]; [listener queryDidChangeViewSnapshot:snap2]; [listener queryDidChangeViewSnapshot:snap3]; @@ -379,11 +379,11 @@ - (void)testWillRaiseInitialEventWhenGoingOffline { FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil); - [listener applyChangedOnlineState:FSTOnlineStateHealthy]; // no event + [listener applyChangedOnlineState:FSTOnlineStateOnline]; // no event [listener queryDidChangeViewSnapshot:snap1]; // no event - [listener applyChangedOnlineState:FSTOnlineStateFailed]; // event + [listener applyChangedOnlineState:FSTOnlineStateOffline]; // event [listener applyChangedOnlineState:FSTOnlineStateUnknown]; // no event - [listener applyChangedOnlineState:FSTOnlineStateFailed]; // no event + [listener applyChangedOnlineState:FSTOnlineStateOffline]; // no event [listener queryDidChangeViewSnapshot:snap2]; // another event FSTDocumentViewChange *change1 = @@ -419,9 +419,9 @@ - (void)testWillRaiseInitialEventWhenGoingOfflineAndThereAreNoDocs { FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); - [listener applyChangedOnlineState:FSTOnlineStateHealthy]; // no event + [listener applyChangedOnlineState:FSTOnlineStateOnline]; // no event [listener queryDidChangeViewSnapshot:snap1]; // no event - [listener applyChangedOnlineState:FSTOnlineStateFailed]; // event + [listener applyChangedOnlineState:FSTOnlineStateOffline]; // event FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc] initWithQuery:query @@ -445,8 +445,8 @@ - (void)testWillRaiseInitialEventWhenStartingOfflineAndThereAreNoDocs { FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); - [listener applyChangedOnlineState:FSTOnlineStateFailed]; // no event - [listener queryDidChangeViewSnapshot:snap1]; // event + [listener applyChangedOnlineState:FSTOnlineStateOffline]; // no event + [listener queryDidChangeViewSnapshot:snap1]; // event FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc] initWithQuery:query diff --git a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm index 4323ccdd02f..77581d41635 100644 --- a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm +++ b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm @@ -175,7 +175,9 @@ - (void)setUp { workerDispatchQueue:_testWorkerQueue credentials:&_credentials]; - _remoteStore = [FSTRemoteStore remoteStoreWithLocalStore:_localStore datastore:_datastore]; + _remoteStore = [[FSTRemoteStore alloc] initWithLocalStore:_localStore + datastore:_datastore + workerDispatchQueue:_testWorkerQueue]; [_testWorkerQueue dispatchAsync:^() { [_remoteStore start]; diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm index 43b2a5f1e5c..b00ea078c5c 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm +++ b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm @@ -22,7 +22,6 @@ #import "Firestore/Source/Core/FSTEventManager.h" #import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Core/FSTSnapshotVersion.h" -#import "Firestore/Source/Core/FSTViewSnapshot.h" #import "Firestore/Source/Local/FSTEagerGarbageCollector.h" #import "Firestore/Source/Local/FSTNoOpGarbageCollector.h" #import "Firestore/Source/Local/FSTPersistence.h" @@ -36,6 +35,7 @@ #import "Firestore/Source/Remote/FSTWatchChange.h" #import "Firestore/Source/Util/FSTAssert.h" #import "Firestore/Source/Util/FSTClasses.h" +#import "Firestore/Source/Util/FSTDispatchQueue.h" #import "Firestore/Source/Util/FSTLogger.h" #import "Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h" @@ -323,6 +323,27 @@ - (void)doFailWrite:(NSDictionary *)spec { } } +- (void)doRunTimer:(NSString *)timer { + FSTTimerID timerID; + if ([timer isEqualToString:@"all"]) { + timerID = FSTTimerIDAll; + } else if ([timer isEqualToString:@"listen_stream_idle"]) { + timerID = FSTTimerIDListenStreamIdle; + } else if ([timer isEqualToString:@"listen_stream_connection"]) { + timerID = FSTTimerIDListenStreamConnectionBackoff; + } else if ([timer isEqualToString:@"write_stream_idle"]) { + timerID = FSTTimerIDWriteStreamIdle; + } else if ([timer isEqualToString:@"write_stream_connection"]) { + timerID = FSTTimerIDWriteStreamConnectionBackoff; + } else if ([timer isEqualToString:@"online_state_timeout"]) { + timerID = FSTTimerIDOnlineStateTimeout; + } else { + FSTFail(@"runTimer spec step specified unknown timer: %@", timer); + } + + [self.driver runTimer:timerID]; +} + - (void)doDisableNetwork { [self.driver disableNetwork]; } @@ -391,6 +412,8 @@ - (void)doStep:(NSDictionary *)step { [self doWriteAck:step[@"writeAck"]]; } else if (step[@"failWrite"]) { [self doFailWrite:step[@"failWrite"]]; + } else if (step[@"runTimer"]) { + [self doRunTimer:step[@"runTimer"]]; } else if (step[@"enableNetwork"]) { if ([step[@"enableNetwork"] boolValue]) { [self doEnableNetwork]; diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h index 466a34710b0..f3d9a5d2d9b 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h @@ -20,6 +20,7 @@ #import "Firestore/Source/Core/FSTTypes.h" #import "Firestore/Source/Remote/FSTRemoteStore.h" +#import "Firestore/Source/Util/FSTDispatchQueue.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" @@ -223,6 +224,11 @@ typedef std::unordered_map)persistence _databaseInfo = {DatabaseId{"project", "database"}, "persistence", "host", false}; // Set up the sync engine and various stores. - dispatch_queue_t mainQueue = dispatch_get_main_queue(); - FSTDispatchQueue *dispatchQueue = [FSTDispatchQueue queueWith:mainQueue]; + dispatch_queue_t queue = + dispatch_queue_create("sync_engine_test_driver", DISPATCH_QUEUE_SERIAL); + _dispatchQueue = [FSTDispatchQueue queueWith:queue]; _localStore = [[FSTLocalStore alloc] initWithPersistence:persistence garbageCollector:garbageCollector initialUser:initialUser]; _datastore = [[FSTMockDatastore alloc] initWithDatabaseInfo:&_databaseInfo - workerDispatchQueue:dispatchQueue + workerDispatchQueue:_dispatchQueue credentials:&_credentialProvider]; - _remoteStore = [FSTRemoteStore remoteStoreWithLocalStore:_localStore datastore:_datastore]; + _remoteStore = [[FSTRemoteStore alloc] initWithLocalStore:_localStore + datastore:_datastore + workerDispatchQueue:_dispatchQueue]; _syncEngine = [[FSTSyncEngine alloc] initWithLocalStore:_localStore remoteStore:_remoteStore @@ -168,8 +171,10 @@ - (void)applyChangedOnlineState:(FSTOnlineState)onlineState { } - (void)start { - [self.localStore start]; - [self.remoteStore start]; + [self.dispatchQueue dispatchSync:^{ + [self.localStore start]; + [self.remoteStore start]; + }]; } - (void)validateUsage { @@ -180,8 +185,10 @@ - (void)validateUsage { } - (void)shutdown { - [self.remoteStore shutdown]; - [self.localStore shutdown]; + [self.dispatchQueue dispatchSync:^{ + [self.remoteStore shutdown]; + [self.localStore shutdown]; + }]; } - (void)validateNextWriteSent:(FSTMutation *)expectedWrite { @@ -208,19 +215,29 @@ - (int)watchStreamRequestCount { } - (void)disableNetwork { - // Make sure to execute all writes that are currently queued. This allows us - // to assert on the total number of requests sent before shutdown. - [self.remoteStore fillWritePipeline]; - [self.remoteStore disableNetwork]; + [self.dispatchQueue dispatchSync:^{ + // Make sure to execute all writes that are currently queued. This allows us + // to assert on the total number of requests sent before shutdown. + [self.remoteStore fillWritePipeline]; + [self.remoteStore disableNetwork]; + }]; } - (void)enableNetwork { - [self.remoteStore enableNetwork]; + [self.dispatchQueue dispatchSync:^{ + [self.remoteStore enableNetwork]; + }]; +} + +- (void)runTimer:(FSTTimerID)timerID { + [self.dispatchQueue runDelayedCallbacksUntil:timerID]; } - (void)changeUser:(const User &)user { _currentUser = user; - [self.syncEngine userDidChange:user]; + [self.dispatchQueue dispatchSync:^{ + [self.syncEngine userDidChange:user]; + }]; } - (FSTOutstandingWrite *)receiveWriteAckWithVersion:(FSTSnapshotVersion *)commitVersion @@ -230,7 +247,9 @@ - (FSTOutstandingWrite *)receiveWriteAckWithVersion:(FSTSnapshotVersion *)commit [[self currentOutstandingWrites] removeObjectAtIndex:0]; [self validateNextWriteSent:write.write]; - [self.datastore ackWriteWithVersion:commitVersion mutationResults:mutationResults]; + [self.dispatchQueue dispatchSync:^{ + [self.datastore ackWriteWithVersion:commitVersion mutationResults:mutationResults]; + }]; return write; } @@ -250,7 +269,9 @@ - (FSTOutstandingWrite *)receiveWriteError:(int)errorCode } FSTLog(@"Failing a write."); - [self.datastore failWriteWithError:error]; + [self.dispatchQueue dispatchSync:^{ + [self.datastore failWriteWithError:error]; + }]; return write; } @@ -278,13 +299,19 @@ - (FSTTargetID)addUserListenerWithQuery:(FSTQuery *)query { [self.events addObject:event]; }]; self.queryListeners[query] = listener; - return [self.eventManager addListener:listener]; + __block FSTTargetID targetID; + [self.dispatchQueue dispatchSync:^{ + targetID = [self.eventManager addListener:listener]; + }]; + return targetID; } - (void)removeUserListenerWithQuery:(FSTQuery *)query { FSTQueryListener *listener = self.queryListeners[query]; [self.queryListeners removeObjectForKey:query]; - [self.eventManager removeListener:listener]; + [self.dispatchQueue dispatchSync:^{ + [self.eventManager removeListener:listener]; + }]; } - (void)writeUserMutation:(FSTMutation *)mutation { @@ -292,28 +319,34 @@ - (void)writeUserMutation:(FSTMutation *)mutation { write.write = mutation; [[self currentOutstandingWrites] addObject:write]; FSTLog(@"sending a user write."); - [self.syncEngine writeMutations:@[ mutation ] - completion:^(NSError *_Nullable error) { - FSTLog(@"A callback was called with error: %@", error); - write.done = YES; - write.error = error; - }]; + [self.dispatchQueue dispatchSync:^{ + [self.syncEngine writeMutations:@[ mutation ] + completion:^(NSError *_Nullable error) { + FSTLog(@"A callback was called with error: %@", error); + write.done = YES; + write.error = error; + }]; + }]; } - (void)receiveWatchChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *_Nullable)snapshot { - [self.datastore writeWatchChange:change snapshotVersion:snapshot]; + [self.dispatchQueue dispatchSync:^{ + [self.datastore writeWatchChange:change snapshotVersion:snapshot]; + }]; } - (void)receiveWatchStreamError:(int)errorCode userInfo:(NSDictionary *)userInfo { NSError *error = [NSError errorWithDomain:FIRFirestoreErrorDomain code:errorCode userInfo:userInfo]; - [self.datastore failWatchStreamWithError:error]; - // Unlike web, stream should re-open synchronously (if we have any listeners) - if (self.queryListeners.count > 0) { - FSTAssert(self.datastore.isWatchStreamOpen, @"Watch stream is open"); - } + [self.dispatchQueue dispatchSync:^{ + [self.datastore failWatchStreamWithError:error]; + // Unlike web, stream should re-open synchronously (if we have any listeners) + if (self.queryListeners.count > 0) { + FSTAssert(self.datastore.isWatchStreamOpen, @"Watch stream is open"); + } + }]; } - (NSDictionary *)currentLimboDocuments { diff --git a/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json b/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json index 6cbbc80ee29..fc9f295e47e 100644 --- a/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json @@ -713,5 +713,157 @@ } } ] + }, + "OnlineState timeout triggers offline behavior": { + "describeName": "Offline:", + "itName": "OnlineState timeout triggers offline behavior", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "runTimer": "online_state_timeout", + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + } + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "runTimer": "all" + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + }, + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "resume-token-1000" + } + } + } + }, + { + "runTimer": "online_state_timeout", + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] } } diff --git a/Firestore/Example/Tests/Util/FSTDispatchQueueTests.mm b/Firestore/Example/Tests/Util/FSTDispatchQueueTests.mm index 9f5b52d45b9..5ef860ca4b9 100644 --- a/Firestore/Example/Tests/Util/FSTDispatchQueueTests.mm +++ b/Firestore/Example/Tests/Util/FSTDispatchQueueTests.mm @@ -21,9 +21,9 @@ #import "Firestore/Example/Tests/Util/XCTestCase+Await.h" // In these generic tests the specific TimerIDs don't matter. -static const FSTTimerID timerID1 = FSTTimerIDListenStreamConnection; +static const FSTTimerID timerID1 = FSTTimerIDListenStreamConnectionBackoff; static const FSTTimerID timerID2 = FSTTimerIDListenStreamIdle; -static const FSTTimerID timerID3 = FSTTimerIDWriteStreamConnection; +static const FSTTimerID timerID3 = FSTTimerIDWriteStreamConnectionBackoff; @interface FSTDispatchQueueTests : XCTestCase @end diff --git a/Firestore/Source/Core/FSTEventManager.mm b/Firestore/Source/Core/FSTEventManager.mm index bc204a02223..b02fc5f796c 100644 --- a/Firestore/Source/Core/FSTEventManager.mm +++ b/Firestore/Source/Core/FSTEventManager.mm @@ -169,9 +169,9 @@ - (BOOL)shouldRaiseInitialEventForSnapshot:(FSTViewSnapshot *)snapshot return YES; } - // NOTE: We consider OnlineState.Unknown as online (it should become Failed - // or Online if we wait long enough). - BOOL maybeOnline = onlineState != FSTOnlineStateFailed; + // NOTE: We consider OnlineState.Unknown as online (it should become Offline or Online if we + // wait long enough). + BOOL maybeOnline = onlineState != FSTOnlineStateOffline; // Don't raise the event if we're online, aren't synced yet (checked // above) and are waiting for a sync. if (self.options.waitForSyncWhenOnline && maybeOnline) { @@ -180,7 +180,7 @@ - (BOOL)shouldRaiseInitialEventForSnapshot:(FSTViewSnapshot *)snapshot } // Raise data from cache if we have any documents or we are offline - return !snapshot.documents.isEmpty || onlineState == FSTOnlineStateFailed; + return !snapshot.documents.isEmpty || onlineState == FSTOnlineStateOffline; } - (BOOL)shouldRaiseEventForSnapshot:(FSTViewSnapshot *)snapshot { diff --git a/Firestore/Source/Core/FSTFirestoreClient.mm b/Firestore/Source/Core/FSTFirestoreClient.mm index fb86e0b2e6a..288cbe2c157 100644 --- a/Firestore/Source/Core/FSTFirestoreClient.mm +++ b/Firestore/Source/Core/FSTFirestoreClient.mm @@ -181,7 +181,9 @@ - (void)initializeWithUser:(const User &)user usePersistence:(BOOL)usePersistenc workerDispatchQueue:self.workerDispatchQueue credentials:_credentialsProvider]; - _remoteStore = [FSTRemoteStore remoteStoreWithLocalStore:_localStore datastore:datastore]; + _remoteStore = [[FSTRemoteStore alloc] initWithLocalStore:_localStore + datastore:datastore + workerDispatchQueue:self.workerDispatchQueue]; _syncEngine = [[FSTSyncEngine alloc] initWithLocalStore:_localStore remoteStore:_remoteStore diff --git a/Firestore/Source/Core/FSTTypes.h b/Firestore/Source/Core/FSTTypes.h index 877ec944a72..688b2bf1d95 100644 --- a/Firestore/Source/Core/FSTTypes.h +++ b/Firestore/Source/Core/FSTTypes.h @@ -65,12 +65,18 @@ typedef void (^FSTVoidMaybeDocumentArrayErrorBlock)( typedef void (^FSTTransactionBlock)(FSTTransaction *transaction, void (^completion)(id _Nullable, NSError *_Nullable)); -/** Describes the online state of the Firestore client */ +/** + * Describes the online state of the Firestore client. Note that this does not indicate whether + * or not the remote store is trying to connect or not. This is primarily used by the View / + * EventManager code to change their behavior while offline (e.g. get() calls shouldn't wait for + * data from the server and snapshot events should set metadata.isFromCache=true). + */ typedef NS_ENUM(NSUInteger, FSTOnlineState) { /** * The Firestore client is in an unknown online state. This means the client is either not * actively trying to establish a connection or it is currently trying to establish a connection, - * but it has not succeeded or failed yet. + * but it has not succeeded or failed yet. Higher-level components should not operate in + * offline mode. */ FSTOnlineStateUnknown, @@ -79,13 +85,14 @@ typedef NS_ENUM(NSUInteger, FSTOnlineState) { * successful connection and there has been at least one successful message received from the * backends. */ - FSTOnlineStateHealthy, + FSTOnlineStateOnline, /** - * The client considers itself offline. It is either trying to establish a connection but - * failing, or it has been explicitly marked offline via a call to `disableNetwork`. + * The client is either trying to establish a connection but failing, or it has been explicitly + * marked offline via a call to `disableNetwork`. Higher-level components should operate in + * offline mode. */ - FSTOnlineStateFailed + FSTOnlineStateOffline }; NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTView.mm b/Firestore/Source/Core/FSTView.mm index d6b4558ac3d..b2a39cbb2a1 100644 --- a/Firestore/Source/Core/FSTView.mm +++ b/Firestore/Source/Core/FSTView.mm @@ -337,7 +337,7 @@ - (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges } - (FSTViewChange *)applyChangedOnlineState:(FSTOnlineState)onlineState { - if (self.isCurrent && onlineState == FSTOnlineStateFailed) { + if (self.isCurrent && onlineState == FSTOnlineStateOffline) { // If we're offline, set `current` to NO and then call applyChanges to refresh our syncState // and generate an FSTViewChange as appropriate. We are guaranteed to get a new FSTTargetChange // that sets `current` back to YES once the client is back online. diff --git a/Firestore/Source/Remote/FSTOnlineStateTracker.h b/Firestore/Source/Remote/FSTOnlineStateTracker.h new file mode 100644 index 00000000000..a414a184139 --- /dev/null +++ b/Firestore/Source/Remote/FSTOnlineStateTracker.h @@ -0,0 +1,71 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#import "Firestore/Source/Core/FSTTypes.h" + +@class FSTDispatchQueue; +@protocol FSTOnlineStateDelegate; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A component used by the FSTRemoteStore to track the FSTOnlineState (that is, whether or not the + * client as a whole should be considered to be online or offline), implementing the appropriate + * heuristics. + * + * In particular, when the client is trying to connect to the backend, we allow up to + * kMaxWatchStreamFailures within kOnlineStateTimeout for a connection to succeed. If we have too + * many failures or the timeout elapses, then we set the FSTOnlineState to Offline, and + * the client will behave as if it is offline (getDocument() calls will return cached data, etc.). + */ +@interface FSTOnlineStateTracker : NSObject + +- (instancetype)initWithWorkerDispatchQueue:(FSTDispatchQueue *)queue; + +- (instancetype)init NS_UNAVAILABLE; + +/** A delegate to be notified on FSTOnlineState changes. */ +@property(nonatomic, weak) id onlineStateDelegate; + +/** + * Called by FSTRemoteStore when a watch stream is started. + * + * It sets the FSTOnlineState to Unknown and starts the onlineStateTimer if necessary. + */ +- (void)handleWatchStreamStart; + +/** + * Called by FSTRemoteStore when a watch stream fails. + * + * Updates our FSTOnlineState as appropriate. The first failure moves us to FSTOnlineStateUnknown. + * We then may allow multiple failures (based on kMaxWatchStreamFailures) before we actually + * transition to FSTOnlineStateOffline. + */ +- (void)handleWatchStreamFailure; + +/** + * Explicitly sets the FSTOnlineState to the specified state. + * + * Note that this resets the timers / failure counters, etc. used by our Offline heuristics, so + * it must not be used in place of handleWatchStreamStart and handleWatchStreamFailure. + */ +- (void)updateState:(FSTOnlineState)newState; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTOnlineStateTracker.mm b/Firestore/Source/Remote/FSTOnlineStateTracker.mm new file mode 100644 index 00000000000..41650cd1667 --- /dev/null +++ b/Firestore/Source/Remote/FSTOnlineStateTracker.mm @@ -0,0 +1,149 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Firestore/Source/Remote/FSTOnlineStateTracker.h" +#import "Firestore/Source/Remote/FSTRemoteStore.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTDispatchQueue.h" +#import "Firestore/Source/Util/FSTLogger.h" + +NS_ASSUME_NONNULL_BEGIN + +// To deal with transient failures, we allow multiple stream attempts before giving up and +// transitioning from FSTOnlineState Unknown to Offline. +static const int kMaxWatchStreamFailures = 2; + +// To deal with stream attempts that don't succeed or fail in a timely manner, we have a +// timeout for FSTOnlineState to reach Online or Offline. If the timeout is reached, we transition +// to Offline rather than waiting indefinitely. +static const NSTimeInterval kOnlineStateTimeout = 10; + +@interface FSTOnlineStateTracker () + +/** The current FSTOnlineState. */ +@property(nonatomic, assign) FSTOnlineState state; + +/** + * A count of consecutive failures to open the stream. If it reaches the maximum defined by + * kMaxWatchStreamFailures, we'll revert to FSTOnlineStateOffline. + */ +@property(nonatomic, assign) int watchStreamFailures; + +/** + * A timer that elapses after kOnlineStateTimeout, at which point we transition from FSTOnlineState + * Unknown to Offline without waiting for the stream to actually fail (kMaxWatchStreamFailures + * times). + */ +@property(nonatomic, strong, nullable) FSTDelayedCallback *watchStreamTimer; + +/** + * Whether the client should log a warning message if it fails to connect to the backend + * (initially YES, cleared after a successful stream, or if we've logged the message already). + */ +@property(nonatomic, assign) BOOL shouldWarnClientIsOffline; + +/** The FSTDispatchQueue to use for running timers (and to call onlineStateDelegate). */ +@property(nonatomic, strong, readonly) FSTDispatchQueue *queue; + +@end + +@implementation FSTOnlineStateTracker +- (instancetype)initWithWorkerDispatchQueue:(FSTDispatchQueue *)queue { + if (self = [super init]) { + _queue = queue; + _state = FSTOnlineStateUnknown; + _shouldWarnClientIsOffline = YES; + } + return self; +} + +- (void)handleWatchStreamStart { + [self setAndBroadcastState:FSTOnlineStateUnknown]; + + if (self.watchStreamTimer == nil) { + self.watchStreamTimer = [self.queue + dispatchAfterDelay:kOnlineStateTimeout + timerID:FSTTimerIDOnlineStateTimeout + block:^{ + self.watchStreamTimer = nil; + FSTAssert( + self.state == FSTOnlineStateUnknown, + @"Timer should be canceled if we transitioned to a different state."); + FSTLog( + @"Watch stream didn't reach Online or Offline within %f seconds. " + @"Considering " + "client offline.", + kOnlineStateTimeout); + [self logClientOfflineWarningIfNecessary]; + [self setAndBroadcastState:FSTOnlineStateOffline]; + + // NOTE: handleWatchStreamFailure will continue to increment + // watchStreamFailures even though we are already marked Offline but this is + // non-harmful. + }]; + } +} + +- (void)handleWatchStreamFailure { + if (self.state == FSTOnlineStateOnline) { + [self setAndBroadcastState:FSTOnlineStateUnknown]; + } else { + self.watchStreamFailures++; + if (self.watchStreamFailures >= kMaxWatchStreamFailures) { + [self clearOnlineStateTimer]; + [self logClientOfflineWarningIfNecessary]; + [self setAndBroadcastState:FSTOnlineStateOffline]; + } + } +} + +- (void)updateState:(FSTOnlineState)newState { + [self clearOnlineStateTimer]; + self.watchStreamFailures = 0; + + if (newState == FSTOnlineStateOnline) { + // We've connected to watch at least once. Don't warn the developer about being offline going + // forward. + self.shouldWarnClientIsOffline = NO; + } + + [self setAndBroadcastState:newState]; +} + +- (void)setAndBroadcastState:(FSTOnlineState)newState { + if (newState != self.state) { + self.state = newState; + [self.onlineStateDelegate applyChangedOnlineState:newState]; + } +} + +- (void)logClientOfflineWarningIfNecessary { + if (self.shouldWarnClientIsOffline) { + FSTWarn(@"Could not reach Firestore backend."); + self.shouldWarnClientIsOffline = NO; + } +} + +- (void)clearOnlineStateTimer { + if (self.watchStreamTimer) { + [self.watchStreamTimer cancel]; + self.watchStreamTimer = nil; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTRemoteStore.h b/Firestore/Source/Remote/FSTRemoteStore.h index 4ea93793748..6f4d565cff3 100644 --- a/Firestore/Source/Remote/FSTRemoteStore.h +++ b/Firestore/Source/Remote/FSTRemoteStore.h @@ -30,6 +30,7 @@ @class FSTQueryData; @class FSTRemoteEvent; @class FSTTransaction; +@class FSTDispatchQueue; NS_ASSUME_NONNULL_BEGIN @@ -95,10 +96,11 @@ NS_ASSUME_NONNULL_BEGIN */ @interface FSTRemoteStore : NSObject -+ (instancetype)remoteStoreWithLocalStore:(FSTLocalStore *)localStore - datastore:(FSTDatastore *)datastore; +- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore + datastore:(FSTDatastore *)datastore + workerDispatchQueue:(FSTDispatchQueue *)queue; -- (instancetype)init __attribute__((unavailable("Use static constructor method."))); +- (instancetype)init NS_UNAVAILABLE; @property(nonatomic, weak) id syncEngine; diff --git a/Firestore/Source/Remote/FSTRemoteStore.mm b/Firestore/Source/Remote/FSTRemoteStore.mm index b2e401341a4..3a88e8f76db 100644 --- a/Firestore/Source/Remote/FSTRemoteStore.mm +++ b/Firestore/Source/Remote/FSTRemoteStore.mm @@ -29,6 +29,7 @@ #import "Firestore/Source/Model/FSTMutationBatch.h" #import "Firestore/Source/Remote/FSTDatastore.h" #import "Firestore/Source/Remote/FSTExistenceFilter.h" +#import "Firestore/Source/Remote/FSTOnlineStateTracker.h" #import "Firestore/Source/Remote/FSTRemoteEvent.h" #import "Firestore/Source/Remote/FSTStream.h" #import "Firestore/Source/Remote/FSTWatchChange.h" @@ -49,21 +50,10 @@ */ static const int kMaxPendingWrites = 10; -/** - * The FSTRemoteStore notifies an onlineStateDelegate with FSTOnlineStateFailed if we fail to - * connect to the backend. This subsequently triggers get() requests to fail or use cached data, - * etc. Unfortunately, our connections have historically been subject to various transient failures. - * So we wait for multiple failures before notifying the onlineStateDelegate. - */ -static const int kOnlineAttemptsBeforeFailure = 2; - #pragma mark - FSTRemoteStore @interface FSTRemoteStore () -- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore - datastore:(FSTDatastore *)datastore NS_DESIGNATED_INITIALIZER; - /** * The local store, used to fill the write pipeline with outbound mutations and resolve existence * filter mismatches. Immutable after initialization. @@ -109,29 +99,13 @@ - (instancetype)initWithLocalStore:(FSTLocalStore *)localStore @property(nonatomic, strong) NSMutableArray *accumulatedChanges; @property(nonatomic, assign) FSTBatchID lastBatchSeen; -/** - * The online state of the watch stream. The state is set to healthy if and only if there are - * messages received by the backend. - */ -@property(nonatomic, assign) FSTOnlineState watchStreamOnlineState; - -/** A count of consecutive failures to open the stream. */ -@property(nonatomic, assign) int watchStreamFailures; - -/** Whether the client should fire offline warning. */ -@property(nonatomic, assign) BOOL shouldWarnOffline; +@property(nonatomic, strong, readonly) FSTOnlineStateTracker *onlineStateTracker; #pragma mark Write Stream // The writeStream is null when the network is disabled. The non-null check is performed by // isNetworkEnabled. @property(nonatomic, strong, nullable) FSTWriteStream *writeStream; -/** - * The approximate time the StreamingWrite stream was opened. Used to estimate if stream was - * closed due to an auth expiration (a recoverable error) or some other more permanent error. - */ -@property(nonatomic, strong, nullable) NSDate *writeStreamOpenTime; - /** * A FIFO queue of in-flight writes. This is in-flight from the point of view of the caller of * writeMutations, not from the point of view from the Datastore itself. In particular, these @@ -142,12 +116,9 @@ - (instancetype)initWithLocalStore:(FSTLocalStore *)localStore @implementation FSTRemoteStore -+ (instancetype)remoteStoreWithLocalStore:(FSTLocalStore *)localStore - datastore:(FSTDatastore *)datastore { - return [[FSTRemoteStore alloc] initWithLocalStore:localStore datastore:datastore]; -} - -- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore datastore:(FSTDatastore *)datastore { +- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore + datastore:(FSTDatastore *)datastore + workerDispatchQueue:(FSTDispatchQueue *)queue { if (self = [super init]) { _localStore = localStore; _datastore = datastore; @@ -156,9 +127,8 @@ - (instancetype)initWithLocalStore:(FSTLocalStore *)localStore datastore:(FSTDat _accumulatedChanges = [NSMutableArray array]; _lastBatchSeen = kFSTBatchIDUnknown; - _watchStreamOnlineState = FSTOnlineStateUnknown; - _shouldWarnOffline = YES; _pendingWrites = [NSMutableArray array]; + _onlineStateTracker = [[FSTOnlineStateTracker alloc] initWithWorkerDispatchQueue:queue]; } return self; } @@ -168,48 +138,14 @@ - (void)start { [self enableNetwork]; } -/** - * Updates our OnlineState to the new state, updating local state and notifying the - * onlineStateHandler as appropriate. - */ -- (void)updateOnlineState:(FSTOnlineState)newState { - // Update and broadcast the new state. - if (newState != self.watchStreamOnlineState) { - if (newState == FSTOnlineStateHealthy) { - // We've connected to watch at least once. Don't warn the developer about being offline going - // forward. - self.shouldWarnOffline = NO; - } else if (newState == FSTOnlineStateUnknown) { - // The state is set to unknown when a healthy stream is closed (e.g. due to a token timeout) - // or when we have no active listens and therefore there's no need to start the stream. - // Assuming there is (possibly in the future) an active listen, then we will eventually move - // to state Online or Failed, but we always want to make at least kOnlineAttemptsBeforeFailure - // attempts before failing, so we reset the count here. - self.watchStreamFailures = 0; - } - self.watchStreamOnlineState = newState; - [self.onlineStateDelegate applyChangedOnlineState:newState]; - } +@dynamic onlineStateDelegate; + +- (nullable id)onlineStateDelegate { + return self.onlineStateTracker.onlineStateDelegate; } -/** - * Updates our FSTOnlineState as appropriate after the watch stream reports a failure. The first - * failure moves us to the 'Unknown' state. We then may allow multiple failures (based on - * kOnlineAttemptsBeforeFailure) before we actually transition to FSTOnlineStateFailed. - */ -- (void)updateOnlineStateAfterFailure { - if (self.watchStreamOnlineState == FSTOnlineStateHealthy) { - [self updateOnlineState:FSTOnlineStateUnknown]; - } else { - self.watchStreamFailures++; - if (self.watchStreamFailures >= kOnlineAttemptsBeforeFailure) { - if (self.shouldWarnOffline) { - FSTWarn(@"Could not reach Firestore backend."); - self.shouldWarnOffline = NO; - } - [self updateOnlineState:FSTOnlineStateFailed]; - } - } +- (void)setOnlineStateDelegate:(nullable id)delegate { + self.onlineStateTracker.onlineStateDelegate = delegate; } #pragma mark Online/Offline state @@ -234,18 +170,17 @@ - (void)enableNetwork { if ([self shouldStartWatchStream]) { [self startWatchStream]; + } else { + [self.onlineStateTracker updateState:FSTOnlineStateUnknown]; } [self fillWritePipeline]; // This may start the writeStream. - - // We move back to the unknown state because we might not want to re-open the stream - [self updateOnlineState:FSTOnlineStateUnknown]; } - (void)disableNetwork { [self disableNetworkInternal]; - // Set the FSTOnlineState to failed so get()'s return from cache, etc. - [self updateOnlineState:FSTOnlineStateFailed]; + // Set the FSTOnlineState to Offline so get()s return from cache, etc. + [self.onlineStateTracker updateState:FSTOnlineStateOffline]; } /** Disables the network, setting the FSTOnlineState to the specified targetOnlineState. */ @@ -269,9 +204,9 @@ - (void)disableNetworkInternal { - (void)shutdown { FSTLog(@"FSTRemoteStore %p shutting down", (__bridge void *)self); [self disableNetworkInternal]; - // Set the FSTOnlineState to Unknown (rather than Failed) to avoid potentially triggering + // Set the FSTOnlineState to Unknown (rather than Offline) to avoid potentially triggering // spurious listener events with cached data, etc. - [self updateOnlineState:FSTOnlineStateUnknown]; + [self.onlineStateTracker updateState:FSTOnlineStateUnknown]; } - (void)userDidChange:(const User &)user { @@ -282,7 +217,7 @@ - (void)userDidChange:(const User &)user { // for the new user and re-fill the write pipeline with new mutations from the LocalStore // (since mutations are per-user). [self disableNetworkInternal]; - [self updateOnlineState:FSTOnlineStateUnknown]; + [self.onlineStateTracker updateState:FSTOnlineStateUnknown]; [self enableNetwork]; } } @@ -293,6 +228,7 @@ - (void)startWatchStream { FSTAssert([self shouldStartWatchStream], @"startWatchStream: called when shouldStartWatchStream: is false."); [self.watchStream startWithDelegate:self]; + [self.onlineStateTracker handleWatchStreamStart]; } - (void)listenToTargetWithQueryData:(FSTQueryData *)queryData { @@ -364,8 +300,8 @@ - (void)watchStreamDidOpen { - (void)watchStreamDidChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snapshotVersion { - // Mark the connection as healthy because we got a message from the server. - [self updateOnlineState:FSTOnlineStateHealthy]; + // Mark the connection as Online because we got a message from the server. + [self.onlineStateTracker updateState:FSTOnlineStateOnline]; FSTWatchTargetChange *watchTargetChange = [change isKindOfClass:[FSTWatchTargetChange class]] ? (FSTWatchTargetChange *)change : nil; @@ -396,19 +332,20 @@ - (void)watchStreamDidChange:(FSTWatchChange *)change - (void)watchStreamWasInterruptedWithError:(nullable NSError *)error { FSTAssert([self isNetworkEnabled], - @"watchStreamDidClose should only be called when the network is enabled"); + @"watchStreamWasInterruptedWithError: should only be called when the network is " + "enabled"); [self cleanUpWatchStreamState]; + [self.onlineStateTracker handleWatchStreamFailure]; // If the watch stream closed due to an error, retry the connection if there are any active // watch targets. if ([self shouldStartWatchStream]) { - [self updateOnlineStateAfterFailure]; [self startWatchStream]; } else { // We don't need to restart the watch stream because there are no active targets. The online // state is set to unknown because there is no active attempt at establishing a connection. - [self updateOnlineState:FSTOnlineStateUnknown]; + [self.onlineStateTracker updateState:FSTOnlineStateUnknown]; } } @@ -602,8 +539,6 @@ - (void)commitBatch:(FSTMutationBatch *)batch { } - (void)writeStreamDidOpen { - self.writeStreamOpenTime = [NSDate date]; - [self.writeStream writeHandshake]; } diff --git a/Firestore/Source/Remote/FSTStream.mm b/Firestore/Source/Remote/FSTStream.mm index 6bec3ad90cf..6735df18323 100644 --- a/Firestore/Source/Remote/FSTStream.mm +++ b/Firestore/Source/Remote/FSTStream.mm @@ -620,7 +620,7 @@ - (instancetype)initWithDatabase:(const DatabaseInfo *)database serializer:(FSTSerializerBeta *)serializer { self = [super initWithDatabase:database workerDispatchQueue:workerDispatchQueue - connectionTimerID:FSTTimerIDListenStreamConnection + connectionTimerID:FSTTimerIDListenStreamConnectionBackoff idleTimerID:FSTTimerIDListenStreamIdle credentials:credentials responseMessageClass:[GCFSListenResponse class]]; @@ -705,7 +705,7 @@ - (instancetype)initWithDatabase:(const DatabaseInfo *)database serializer:(FSTSerializerBeta *)serializer { self = [super initWithDatabase:database workerDispatchQueue:workerDispatchQueue - connectionTimerID:FSTTimerIDWriteStreamConnection + connectionTimerID:FSTTimerIDWriteStreamConnectionBackoff idleTimerID:FSTTimerIDWriteStreamIdle credentials:credentials responseMessageClass:[GCFSWriteResponse class]]; diff --git a/Firestore/Source/Util/FSTDispatchQueue.h b/Firestore/Source/Util/FSTDispatchQueue.h index 9b28c9cdb48..79226000620 100644 --- a/Firestore/Source/Util/FSTDispatchQueue.h +++ b/Firestore/Source/Util/FSTDispatchQueue.h @@ -23,11 +23,24 @@ NS_ASSUME_NONNULL_BEGIN * can then be used from tests to check for the presence of callbacks or to run them early. */ typedef NS_ENUM(NSInteger, FSTTimerID) { - FSTTimerIDAll, // Sentinel value to be used with runDelayedCallbacksUntil: to run all blocks. + /** All can be used with runDelayedCallbacksUntil: to run all timers. */ + FSTTimerIDAll, + + /** + * The following 4 timers are used in FSTStream for the listen and write streams. The "Idle" timer + * is used to close the stream due to inactivity. The "ConnectionBackoff" timer is used to + * restart a stream once the appropriate backoff delay has elapsed. + */ FSTTimerIDListenStreamIdle, - FSTTimerIDListenStreamConnection, + FSTTimerIDListenStreamConnectionBackoff, FSTTimerIDWriteStreamIdle, - FSTTimerIDWriteStreamConnection + FSTTimerIDWriteStreamConnectionBackoff, + + /** + * A timer used in FSTOnlineStateTracker to transition from FSTOnlineState Unknown to Offline + * after a set timeout, rather than waiting indefinitely for success or failure. + */ + FSTTimerIDOnlineStateTimeout }; /** @@ -80,6 +93,13 @@ typedef NS_ENUM(NSInteger, FSTTimerID) { */ - (void)dispatchAsyncAllowingSameQueue:(void (^)(void))block; +/** + * Wrapper for dispatch_sync(). Mostly meant for use in tests. + * + * @param block The block to run. + */ +- (void)dispatchSync:(void (^)(void))block; + /** * Schedules a callback after the specified delay. * diff --git a/Firestore/Source/Util/FSTDispatchQueue.mm b/Firestore/Source/Util/FSTDispatchQueue.mm index 5bd7f276b21..3184d293dad 100644 --- a/Firestore/Source/Util/FSTDispatchQueue.mm +++ b/Firestore/Source/Util/FSTDispatchQueue.mm @@ -187,6 +187,10 @@ - (void)dispatchAsyncAllowingSameQueue:(void (^)(void))block { dispatch_async(self.queue, block); } +- (void)dispatchSync:(void (^)(void))block { + dispatch_sync(self.queue, block); +} + - (FSTDelayedCallback *)dispatchAfterDelay:(NSTimeInterval)delay timerID:(FSTTimerID)timerID block:(void (^)(void))block {