Skip to content

Commit dccac91

Browse files
authored
fix(datastore-v1): retry on subscription connection error (#2581)
1 parent 8c40657 commit dccac91

File tree

7 files changed

+128
-10
lines changed

7 files changed

+128
-10
lines changed

AmplifyPlugins/API/AWSAPICategoryPlugin/Operation/AWSGraphQLSubscriptionOperation.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,12 @@ final public class AWSGraphQLSubscriptionOperation<R: Decodable>: GraphQLSubscri
189189
return
190190
} else if case ConnectionProviderError.unauthorized = error {
191191
errorDescription += ": \(APIError.UnauthorizedMessageString)"
192+
} else if case ConnectionProviderError.connection = error {
193+
errorDescription += ": connection"
194+
let error = URLError(.networkConnectionLost)
195+
dispatch(result: .failure(APIError.networkError(errorDescription, nil, error)))
196+
finish()
197+
return
192198
}
193199

194200
dispatch(result: .failure(APIError.operationError(errorDescription, "", error)))

AmplifyPlugins/API/AWSAPICategoryPluginTests/AWSAPICategoryPlugin+ConfigureTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import XCTest
1010
@testable import Amplify
1111
@testable import AWSAPICategoryPlugin
1212

13-
class AWSAPICategoryPluginConfigureTests: AWSAPICategoryPluginTestBase {
13+
class AWSAPICategoryPluginConfigureTests {
1414

1515
func testPluginKey() {
16+
let apiPlugin = AWSAPIPlugin()
1617
XCTAssertEqual(apiPlugin.key, "awsAPIPlugin")
1718
}
1819

AmplifyPlugins/API/AWSAPICategoryPluginTests/AWSAPICategoryPluginTestBase.swift

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class AWSAPICategoryPluginTestBase: XCTestCase {
3030
let testPath = "testPath"
3131

3232
override func setUp() {
33+
Amplify.reset()
3334
apiPlugin = AWSAPIPlugin()
3435

3536
let authService = MockAWSAuthService()
@@ -62,7 +63,6 @@ class AWSAPICategoryPluginTestBase: XCTestCase {
6263
XCTFail("Failed to create endpoint config")
6364
}
6465

65-
Amplify.reset()
6666
let config = AmplifyConfiguration()
6767
do {
6868
try Amplify.configure(config)
@@ -72,9 +72,6 @@ class AWSAPICategoryPluginTestBase: XCTestCase {
7272
}
7373

7474
override func tearDown() {
75-
if let api = apiPlugin {
76-
api.reset {
77-
}
78-
}
75+
Amplify.reset()
7976
}
8077
}

AmplifyPlugins/API/AWSAPICategoryPluginTests/Operation/GraphQLSubscribeTests.swift

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ class GraphQLSubscribeTests: OperationTestBase {
3333
var subscriptionItem: SubscriptionItem!
3434
var subscriptionEventHandler: SubscriptionEventHandler!
3535

36+
var expectedCompletionFailureError: APIError?
37+
3638
override func setUpWithError() throws {
3739
try super.setUpWithError()
3840

@@ -122,7 +124,23 @@ class GraphQLSubscribeTests: OperationTestBase {
122124
/// - The value handler is not invoked with with a data value
123125
/// - The value handler is invoked with a disconnection message
124126
/// - The completion handler is invoked with an error termination
125-
func testConnectionError() throws {
127+
func testConnectionErrorWithLimitExceeded() throws {
128+
receivedCompletionFinish.shouldTrigger = false
129+
receivedCompletionFailure.shouldTrigger = true
130+
receivedConnected.shouldTrigger = false
131+
receivedDisconnected.shouldTrigger = false
132+
receivedSubscriptionEventData.shouldTrigger = false
133+
receivedSubscriptionEventError.shouldTrigger = false
134+
135+
subscribe()
136+
wait(for: [onSubscribeInvoked], timeout: 0.05)
137+
138+
subscriptionEventHandler(.failed(ConnectionProviderError.limitExceeded(nil)), subscriptionItem)
139+
expectedCompletionFailureError = APIError.operationError("", "", ConnectionProviderError.limitExceeded(nil))
140+
waitForExpectations(timeout: 0.05)
141+
}
142+
143+
func testConnectionErrorWithSubscriptionError() throws {
126144
receivedCompletionFinish.shouldTrigger = false
127145
receivedCompletionFailure.shouldTrigger = true
128146
receivedConnected.shouldTrigger = false
@@ -134,8 +152,42 @@ class GraphQLSubscribeTests: OperationTestBase {
134152
wait(for: [onSubscribeInvoked], timeout: 0.05)
135153

136154
subscriptionEventHandler(.connection(.connecting), subscriptionItem)
137-
subscriptionEventHandler(.failed("Error"), subscriptionItem)
155+
subscriptionEventHandler(.failed(ConnectionProviderError.subscription("", nil)), subscriptionItem)
156+
expectedCompletionFailureError = APIError.operationError("", "", ConnectionProviderError.subscription("", nil))
157+
waitForExpectations(timeout: 0.05)
158+
}
159+
160+
func testConnectionErrorWithConnectionUnauthorizedError() throws {
161+
receivedCompletionFinish.shouldTrigger = false
162+
receivedCompletionFailure.shouldTrigger = true
163+
receivedConnected.shouldTrigger = false
164+
receivedDisconnected.shouldTrigger = false
165+
receivedSubscriptionEventData.shouldTrigger = false
166+
receivedSubscriptionEventError.shouldTrigger = false
138167

168+
subscribe()
169+
wait(for: [onSubscribeInvoked], timeout: 0.05)
170+
171+
subscriptionEventHandler(.connection(.connecting), subscriptionItem)
172+
subscriptionEventHandler(.failed(ConnectionProviderError.unauthorized), subscriptionItem)
173+
expectedCompletionFailureError = APIError.operationError("", "", ConnectionProviderError.unauthorized)
174+
waitForExpectations(timeout: 0.05)
175+
}
176+
177+
func testConnectionErrorWithConnectionProviderConnectionError() throws {
178+
receivedCompletionFinish.shouldTrigger = false
179+
receivedCompletionFailure.shouldTrigger = true
180+
receivedConnected.shouldTrigger = false
181+
receivedDisconnected.shouldTrigger = false
182+
receivedSubscriptionEventData.shouldTrigger = false
183+
receivedSubscriptionEventError.shouldTrigger = false
184+
185+
subscribe()
186+
wait(for: [onSubscribeInvoked], timeout: 0.05)
187+
188+
subscriptionEventHandler(.connection(.connecting), subscriptionItem)
189+
subscriptionEventHandler(.failed(ConnectionProviderError.connection), subscriptionItem)
190+
expectedCompletionFailureError = APIError.networkError("", nil, URLError(.networkConnectionLost))
139191
waitForExpectations(timeout: 0.05)
140192
}
141193

@@ -286,7 +338,11 @@ class GraphQLSubscribeTests: OperationTestBase {
286338
}
287339
}, completionListener: { result in
288340
switch result {
289-
case .failure:
341+
case .failure(let error):
342+
if let apiError = error as? APIError,
343+
let expectedError = self.expectedCompletionFailureError {
344+
XCTAssertEqual(apiError, expectedError)
345+
}
290346
self.receivedCompletionFailure.fulfill()
291347
case .success:
292348
self.receivedCompletionFinish.fulfill()
@@ -296,3 +352,43 @@ class GraphQLSubscribeTests: OperationTestBase {
296352
return operation
297353
}
298354
}
355+
356+
extension APIError: Equatable {
357+
public static func == (lhs: APIError, rhs: APIError) -> Bool {
358+
switch (lhs, rhs) {
359+
case (.unknown, .unknown),
360+
(.invalidConfiguration, .invalidConfiguration),
361+
(.httpStatusError, .httpStatusError),
362+
(.pluginError, .pluginError):
363+
return true
364+
case (.operationError(_, _, let lhs), .operationError(_, _, let rhs)):
365+
if let lhs = lhs as? ConnectionProviderError, let rhs = rhs as? ConnectionProviderError {
366+
switch (lhs, rhs) {
367+
case (.connection, .connection),
368+
(.jsonParse, .jsonParse),
369+
(.limitExceeded, .limitExceeded),
370+
(.subscription, .subscription),
371+
(.unauthorized, .unauthorized),
372+
(.unknown, .unknown):
373+
return true
374+
default:
375+
return false
376+
}
377+
} else if lhs == nil && rhs == nil {
378+
return true
379+
} else {
380+
return false
381+
}
382+
case (.networkError(_, _, let lhs), .networkError(_, _, let rhs)):
383+
if let lhs = lhs as? URLError, let rhs = rhs as? URLError {
384+
return lhs.code == rhs.code
385+
} else if lhs == nil && rhs == nil {
386+
return true
387+
} else {
388+
return false
389+
}
390+
default:
391+
return false
392+
}
393+
}
394+
}

AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/RemoteSyncEngine+Retryable.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ extension RemoteSyncEngine {
3636
urlErrorOptional = underlyingError
3737
} else if let urlError = error as? URLError {
3838
urlErrorOptional = urlError
39+
} else if let dataStoreError = error as? DataStoreError,
40+
case .api(let amplifyError, _) = dataStoreError,
41+
let apiError = amplifyError as? APIError,
42+
case .networkError(_, _, let error) = apiError,
43+
let urlError = error as? URLError {
44+
urlErrorOptional = urlError
3945
}
4046

4147
let advice = requestRetryablePolicy.retryRequestAdvice(urlError: urlErrorOptional,

AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/RequestRetryablePolicy.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ class RequestRetryablePolicy: RequestRetryable {
3939
.cannotFindHost,
4040
.timedOut,
4141
.dataNotAllowed,
42-
.cannotParseResponse:
42+
.cannotParseResponse,
43+
.networkConnectionLost:
4344
let waitMillis = retryDelayInMillseconds(for: attemptNumber)
4445
return RequestRetryAdvice(shouldRetry: true, retryInterval: .milliseconds(waitMillis))
4546
default:

AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Sync/RequestRetryablePolicyTests.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,17 @@ class RequestRetryablePolicyTests: XCTestCase {
188188
assertMilliseconds(retryAdvice.retryInterval, greaterThan: 200, lessThan: 300)
189189
}
190190

191+
func testNetworkConnectionLostError() {
192+
let retryableErrorCode = URLError.init(.networkConnectionLost)
193+
194+
let retryAdvice = retryPolicy.retryRequestAdvice(urlError: retryableErrorCode,
195+
httpURLResponse: nil,
196+
attemptNumber: 1)
197+
198+
XCTAssert(retryAdvice.shouldRetry)
199+
assertMilliseconds(retryAdvice.retryInterval, greaterThan: 200, lessThan: 300)
200+
}
201+
191202
func testHTTPTooManyRedirectsError() {
192203
let nonRetryableErrorCode = URLError.init(.httpTooManyRedirects)
193204

0 commit comments

Comments
 (0)