diff options
author | Michael Lehenbauer <mikelehen@gmail.com> | 2018-03-05 09:49:41 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-03-05 09:49:41 -0800 |
commit | 34ebf10b0acc65f1924d723e82085d4104bc281d (patch) | |
tree | 98237b8eef85abdc7d464ef221a37d3bb6937be7 /Firestore/Example | |
parent | 1c40e7aada6b32bbc621f06fb5f380149606a58d (diff) |
Add 10 second timeout waiting for connection before client behaves as-if offline. (#872)
[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.
Diffstat (limited to 'Firestore/Example')
8 files changed, 264 insertions, 48 deletions
diff --git a/Firestore/Example/Tests/Core/FSTEventManagerTests.mm b/Firestore/Example/Tests/Core/FSTEventManagerTests.mm index fcde17d..f5f7b5b 100644 --- a/Firestore/Example/Tests/Core/FSTEventManagerTests.mm +++ b/Firestore/Example/Tests/Core/FSTEventManagerTests.mm @@ -143,9 +143,9 @@ NS_ASSUME_NONNULL_BEGIN .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 @@ NS_ASSUME_NONNULL_BEGIN [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 4856b5f..1b26360 100644 --- a/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm +++ b/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm @@ -340,10 +340,10 @@ NS_ASSUME_NONNULL_BEGIN [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 @@ NS_ASSUME_NONNULL_BEGIN 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 @@ NS_ASSUME_NONNULL_BEGIN 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 @@ NS_ASSUME_NONNULL_BEGIN 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 4323ccd..77581d4 100644 --- a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm +++ b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm @@ -175,7 +175,9 @@ NS_ASSUME_NONNULL_BEGIN 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 43b2a5f..b00ea07 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 @@ static NSString *const kNoIOSTag = @"no-ios"; } } +- (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 @@ static NSString *const kNoIOSTag = @"no-ios"; [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 466a347..f3d9a5d 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" @@ -224,6 +225,11 @@ typedef std::unordered_map<firebase::firestore::auth::User, - (void)enableNetwork; /** + * Runs a pending timer callback on the FSTDispatchQueue. + */ +- (void)runTimer:(FSTTimerID)timerID; + +/** * Switches the FSTSyncEngine to a new user. The test driver tracks the outstanding mutations for * each user, so future receiveWriteAck/Error operations will validate the write sent to the mock * datastore matches the next outstanding write for that user. diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm index 07dc84f..bfcd4dd 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm @@ -31,7 +31,6 @@ #import "Firestore/Source/Remote/FSTDatastore.h" #import "Firestore/Source/Remote/FSTWatchChange.h" #import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTDispatchQueue.h" #import "Firestore/Source/Util/FSTLogger.h" #import "Firestore/Example/Tests/Core/FSTSyncEngine+Testing.h" @@ -72,6 +71,7 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, strong, readonly) FSTRemoteStore *remoteStore; @property(nonatomic, strong, readonly) FSTLocalStore *localStore; @property(nonatomic, strong, readonly) FSTSyncEngine *syncEngine; +@property(nonatomic, strong, readonly) FSTDispatchQueue *dispatchQueue; #pragma mark - Data structures for holding events sent by the watch stream. @@ -117,16 +117,19 @@ NS_ASSUME_NONNULL_BEGIN _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 @@ NS_ASSUME_NONNULL_BEGIN } - (void)start { - [self.localStore start]; - [self.remoteStore start]; + [self.dispatchQueue dispatchSync:^{ + [self.localStore start]; + [self.remoteStore start]; + }]; } - (void)validateUsage { @@ -180,8 +185,10 @@ NS_ASSUME_NONNULL_BEGIN } - (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 @@ NS_ASSUME_NONNULL_BEGIN } - (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 @@ NS_ASSUME_NONNULL_BEGIN [[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 @@ NS_ASSUME_NONNULL_BEGIN } FSTLog(@"Failing a write."); - [self.datastore failWriteWithError:error]; + [self.dispatchQueue dispatchSync:^{ + [self.datastore failWriteWithError:error]; + }]; return write; } @@ -278,13 +299,19 @@ NS_ASSUME_NONNULL_BEGIN [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 @@ NS_ASSUME_NONNULL_BEGIN 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<NSString *, id> *)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<FSTDocumentKey *, FSTBoxedTargetID *> *)currentLimboDocuments { diff --git a/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json b/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json index 6cbbc80..fc9f295 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 9f5b52d..5ef860c 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 |