From 45bddcd0634d545030ef07a03f64c413e8d81b83 Mon Sep 17 00:00:00 2001 From: Abhash Kumar Singh Date: Fri, 15 Sep 2023 14:44:11 -0700 Subject: [PATCH] fix(datastore): Continue initial sync if atleast one model from schema succeeds --- .../InitialSync/InitialSyncOrchestrator.swift | 11 +- .../InitialSyncOrchestratorTests.swift | 111 +++++++++++++++++- Package.resolved | 8 +- 3 files changed, 120 insertions(+), 10 deletions(-) diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOrchestrator.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOrchestrator.swift index 872fb56fc3..a759b86553 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOrchestrator.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOrchestrator.swift @@ -164,7 +164,16 @@ final class AWSInitialSyncOrchestrator: InitialSyncOrchestrator { allMessages.joined(separator: "\n"), underlyingError ) - return .failure(syncError) + + // send success if atleast one model succeeded + let syncableModelsCount = ModelRegistry.modelSchemas.filter { $0.isSyncable }.count + if syncableModelsCount == syncErrors.count { + return .failure(syncError) + } else { + self.log.verbose("\(#function) Atleast one model sync succeeded. Sending completion result as .success with error: \(syncError)") + return .successfulVoid + } + } private func dispatchSyncQueriesStarted(for modelNames: [String]) { diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/InitialSync/InitialSyncOrchestratorTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/InitialSync/InitialSyncOrchestratorTests.swift index e106b30d10..b66177f9e8 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/InitialSync/InitialSyncOrchestratorTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/InitialSync/InitialSyncOrchestratorTests.swift @@ -102,12 +102,113 @@ class InitialSyncOrchestratorTests: XCTestCase { sink.cancel() } + /// - Given: An InitialSyncOrchestrator with a model dependency graph, API is expected to return an error for all models + /// - When: + /// - The orchestrator starts up + /// - Then: + /// - Finish with an error when all sync queries fail + func testFinishWithAPIErrorWhenAllSyncQueriesFail() async throws { + ModelRegistry.reset() + PostCommentModelRegistration().registerModels(registry: ModelRegistry.self) + let responder = QueryRequestListenerResponder> { request, listener in + if request.document.contains("SyncPosts") { + let event: GraphQLOperation>.OperationResult = + .failure(APIError.operationError("", "", nil)) + listener?(event) + } else if request.document.contains("SyncComments") { + let event: GraphQLOperation>.OperationResult = + .failure(APIError.operationError("", "", nil)) + listener?(event) + } + + return nil + } + + let apiPlugin = MockAPICategoryPlugin() + apiPlugin.responders[.queryRequestListener] = responder + + let storageAdapter = MockSQLiteStorageEngineAdapter() + storageAdapter.returnOnQueryModelSyncMetadata(nil) + + let reconciliationQueue = MockReconciliationQueue() + + let orchestrator: AWSInitialSyncOrchestrator = + AWSInitialSyncOrchestrator(dataStoreConfiguration: .default, + authModeStrategy: AWSDefaultAuthModeStrategy(), + api: apiPlugin, + reconciliationQueue: reconciliationQueue, + storageAdapter: storageAdapter) + + let syncCallbackReceived = expectation(description: "Sync callback received, sync operation is complete") + let syncQueriesStartedReceived = expectation(description: "syncQueriesStarted received") + + let filter = HubFilters.forEventName(HubPayload.EventName.DataStore.syncQueriesStarted) + let hubListener = Amplify.Hub.listen(to: .dataStore, isIncluded: filter) { payload in + guard let syncQueriesStartedEvent = payload.data as? SyncQueriesStartedEvent else { + XCTFail("Failed to cast payload data as SyncQueriesStartedEvent") + return + } + XCTAssertEqual(syncQueriesStartedEvent.models.count, 2) + syncQueriesStartedReceived.fulfill() + } + + guard try await HubListenerTestUtilities.waitForListener(with: hubListener, timeout: 5.0) else { + XCTFail("Listener not registered for hub") + return + } + + let syncStartedReceived = expectation(description: "Sync started received, sync operation started") + syncStartedReceived.expectedFulfillmentCount = 2 + let finishedReceived = expectation(description: "InitialSyncOperation finished paginating and offering") + finishedReceived.expectedFulfillmentCount = 2 + let failureCompletionReceived = expectation(description: "InitialSyncOrchestrator completed with failure") + let sink = orchestrator + .publisher + .sink(receiveCompletion: { completion in + switch completion { + case .finished: + XCTFail("Should have finished with failure") + case .failure: + failureCompletionReceived.fulfill() + } + }, receiveValue: { value in + switch value { + case .started: + syncStartedReceived.fulfill() + case .finished(let modelName, let error): + if modelName == "Post" { + guard case .api(let apiError, _) = error, case .operationError = apiError as? APIError else { + XCTFail("Should be api error") + return + } + } else if modelName == "Comment" { + guard case .api(let apiError, _) = error, case .operationError = apiError as? APIError else { + XCTFail("Should be api error") + return + } + } + finishedReceived.fulfill() + default: + break + } + }) + + orchestrator.sync { _ in + syncCallbackReceived.fulfill() + } + + await waitForExpectations(timeout: 1) + XCTAssertEqual(orchestrator.syncOperationQueue.maxConcurrentOperationCount, 1) + Amplify.Hub.removeListener(hubListener) + sink.cancel() + } + /// - Given: An InitialSyncOrchestrator with a model dependency graph, API is expected to return an error for certain models /// - When: /// - The orchestrator starts up /// - Then: - /// - Finish with an error for each sync query that fails. - func testFinishWithAPIError() async throws { + /// - Finish with a success when at least one sync query succeeds + func testFinishWithAPISuccessWhenAtlestOneSyncQuerySucceeds() async throws { ModelRegistry.reset() PostCommentModelRegistration().registerModels(registry: ModelRegistry.self) let responder = QueryRequestListenerResponder> { request, listener in @@ -162,15 +263,15 @@ class InitialSyncOrchestratorTests: XCTestCase { syncStartedReceived.expectedFulfillmentCount = 2 let finishedReceived = expectation(description: "InitialSyncOperation finished paginating and offering") finishedReceived.expectedFulfillmentCount = 2 - let failureCompletionReceived = expectation(description: "InitialSyncOrchestrator completed with failure") + let successCompletionReceived = expectation(description: "InitialSyncOrchestrator completed with success") let sink = orchestrator .publisher .sink(receiveCompletion: { completion in switch completion { case .finished: - XCTFail("Should have finished with failure") + successCompletionReceived.fulfill() case .failure: - failureCompletionReceived.fulfill() + XCTFail("Should have finished with success") } }, receiveValue: { value in switch value { diff --git a/Package.resolved b/Package.resolved index 681d2bacf7..2ef4d7901c 100644 --- a/Package.resolved +++ b/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/aws-amplify/aws-appsync-realtime-client-ios.git", "state" : { - "revision" : "c7ec93dcbbcd8abc90c74203937f207a7fcaa611", - "version" : "3.1.1" + "revision" : "b036e83716789c13a3480eeb292b70caa54114f2", + "version" : "3.1.0" } }, { @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", - "version" : "1.5.3" + "revision" : "32e8d724467f8fe623624570367e3d50c5638e46", + "version" : "1.5.2" } }, {