Compare commits

...

4 Commits

Author SHA1 Message Date
Michael Law 458422c806 temp code for retry issue 2022-02-11 11:15:09 -05:00
Michael Law 3c31b9830c add more unit tests 2022-02-11 00:54:18 -05:00
Michael Law a9ed4bfc87 add better documentation 2022-02-11 00:51:11 -05:00
Michael Law d6f79565ec fix: Retry on MaxSubscriptionReached 2022-02-10 18:43:19 -05:00
9 changed files with 180 additions and 10 deletions

View File

@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
0B59161BB37D32073E4FD61B /* Pods_AppSyncRTCSample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7CF486070B34EFD15B4DB8FC /* Pods_AppSyncRTCSample.framework */; };
2143D46727B5D9B40066B2F7 /* RealTimeConnectionProviderResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2143D46627B5D9B40066B2F7 /* RealTimeConnectionProviderResponseTests.swift */; };
2164E65D2639AD5600385027 /* StarscreamAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2164E65C2639AD5600385027 /* StarscreamAdapterTests.swift */; };
2164E674263C58CE00385027 /* AppSyncRealTimeClientTestBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2164E673263C58CD00385027 /* AppSyncRealTimeClientTestBase.swift */; };
217F39992405D9D500F1A0B3 /* AppSyncRealTimeClient.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 217F398F2405D9D500F1A0B3 /* AppSyncRealTimeClient.framework */; };
@ -121,6 +122,7 @@
/* Begin PBXFileReference section */
18D6E56CE03BAC33493CC19B /* Pods-HostApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HostApp.debug.xcconfig"; path = "Target Support Files/Pods-HostApp/Pods-HostApp.debug.xcconfig"; sourceTree = "<group>"; };
2143D46627B5D9B40066B2F7 /* RealTimeConnectionProviderResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealTimeConnectionProviderResponseTests.swift; sourceTree = "<group>"; };
2164E65C2639AD5600385027 /* StarscreamAdapterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarscreamAdapterTests.swift; sourceTree = "<group>"; };
2164E673263C58CD00385027 /* AppSyncRealTimeClientTestBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSyncRealTimeClientTestBase.swift; sourceTree = "<group>"; };
217F398F2405D9D500F1A0B3 /* AppSyncRealTimeClient.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AppSyncRealTimeClient.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -263,6 +265,14 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
2143D46527B5D9730066B2F7 /* AppSyncRealTimeConnection */ = {
isa = PBXGroup;
children = (
2143D46627B5D9B40066B2F7 /* RealTimeConnectionProviderResponseTests.swift */,
);
path = AppSyncRealTimeConnection;
sourceTree = "<group>";
};
217F39852405D9D500F1A0B3 = {
isa = PBXGroup;
children = (
@ -421,6 +431,7 @@
217F39EC2406EA4000F1A0B3 /* ConnectionProvider */ = {
isa = PBXGroup;
children = (
2143D46527B5D9730066B2F7 /* AppSyncRealTimeConnection */,
FA67507F24D339B0005A1345 /* ConnectionProviderStaleConnectionTests.swift */,
217F39ED2406EA4000F1A0B3 /* ConnectionProviderTests.swift */,
FA67507C24D338FA005A1345 /* RealtimeConnectionProviderTestBase.swift */,
@ -1139,6 +1150,7 @@
files = (
21D38B89240A39E400EC2A8D /* OIDCAuthInterceptorTests.swift in Sources */,
217F39F22406EA4000F1A0B3 /* ConnectionProviderTests.swift in Sources */,
2143D46727B5D9B40066B2F7 /* RealTimeConnectionProviderResponseTests.swift in Sources */,
FA67507824D3244A005A1345 /* MockWebsocketProvider.swift in Sources */,
217F39F02406EA4000F1A0B3 /* AppSyncSubscriptionConnectionTests.swift in Sources */,
FA67507B24D338CB005A1345 /* Error+Extension.swift in Sources */,

View File

@ -18,7 +18,6 @@ extension AppSyncSubscriptionConnection {
return
}
if connectionState == .connected {
AppSyncLogger.debug("[AppSyncSubscriptionConnection] \(#function): connection is connected, start subscription.")
startSubscription()
}
}
@ -32,9 +31,10 @@ extension AppSyncSubscriptionConnection {
else {
return
}
AppSyncLogger.debug("[AppSyncSubscriptionConnection] \(#function): connection is connected, start subscription for \(subscriptionItem.identifier) ")
subscriptionState = .inProgress
throttled = false
guard let payload = convertToPayload(
for: subscriptionItem.requestString,
variables: subscriptionItem.variables

View File

@ -16,9 +16,6 @@ extension AppSyncSubscriptionConnection {
}
guard response.id == subscriptionItem.identifier else {
AppSyncLogger.verbose("""
[AppSyncSubscriptionConnection] \(#function): \(subscriptionItem.identifier). Ignoring data event for \(response.id ?? "(null)")
""")
return
}

View File

@ -15,12 +15,52 @@ extension AppSyncSubscriptionConnection {
return
}
// If the error identifier is not for the this connection
// If the error identifier is not for the this subscription
// we return immediately without handling the error.
if case let ConnectionProviderError.subscription(identifier, _) = error,
identifier != subscriptionItem.identifier {
return
}
if case let ConnectionProviderError.limitExceeded(identifier) = error {
// We do not know which subscription this is for, either ignore it or send the error back
// Don't go pass this check since it's not retryable, return from here
if identifier == nil {
// if we are subscribed already, then ignore it.
if subscriptionState == .subscribed {
return
} else {
// If we are .inProgress or .notSubscribed, set it to `.notSubscribed`, send the error back.
subscriptionState = .notSubscribed
if !throttled {
// TODO: we sent this back for this subscription, but we still don't really know which
// subscription was the one that was throttled.
subscriptionItem.subscriptionEventHandler(.failed(error), subscriptionItem)
}
// Once we've sent it once, circuit break here using some state on the subscription.
throttled = true
return
}
}
// If there is an identifier, and does not equal this subscription's identifier, ignore the error.
if identifier != subscriptionItem.identifier {
return
}
}
if case ConnectionProviderError.other = error {
AppSyncLogger.warn("[AppSyncSubscriptionConnection] \(#function): other error \(subscriptionItem.identifier)")
// Again this is only ever returned if there is no `id` and it is not LimitExceeded
// the default case should also be throttled at the subscription as we don't know whether it
// happened because of this subscription
// of because of something else.
subscriptionItem.subscriptionEventHandler(.failed(error), subscriptionItem)
return
}
AppSyncSubscriptionConnection.logExtendedErrorInfo(for: error)

View File

@ -27,6 +27,8 @@ public class AppSyncSubscriptionConnection: SubscriptionConnection, RetryableCon
/// Retry logic to handle
var retryHandler: ConnectionRetryHandler?
var throttled: Bool = false
public init(provider: ConnectionProvider) {
self.connectionProvider = provider

View File

@ -111,6 +111,13 @@ extension RealtimeConnectionProvider: AppSyncWebsocketDelegate {
return
}
if response.isRateLimitExceededError() {
AppSyncLogger.debug("[RealtimeConnectionProvider] isRateLimitExceededError")
let limitExceedError = ConnectionProviderError.limitExceeded(nil)
updateCallback(event: .error(limitExceedError))
return
}
// Return back as generic error if there is no identifier.
guard let identifier = response.id else {
let genericError = ConnectionProviderError.other
@ -118,9 +125,7 @@ extension RealtimeConnectionProvider: AppSyncWebsocketDelegate {
return
}
// Map to limit exceed error if we get MaxSubscriptionsReachedException
if let errorType = response.payload?["errorType"],
errorType == "MaxSubscriptionsReachedException" {
if response.isMaxSubscriptionReachedError() {
let limitExceedError = ConnectionProviderError.limitExceeded(identifier)
updateCallback(event: .error(limitExceedError))
return

View File

@ -7,6 +7,8 @@
import Foundation
/// More information about the response can be found here
/// https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html#connection-init-message
struct RealtimeConnectionProviderResponse {
/// Subscription Identifier
@ -52,3 +54,67 @@ extension RealtimeConnectionProviderResponse: Decodable {
case responseType = "type"
}
}
/// Helper methods to check which type of errors, such as `MaxSubscriptionsReachedError`, `LimitExceededError`.
/// Errors have the following shape
///
/// "type": "error": A constant <string> parameter.
/// "id": <string>: The ID of the corresponding registered subscription, if relevant.
/// "payload" <Object>: An object that contains the corresponding error information.
///
/// More information can be found here
/// https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html#error-message
extension RealtimeConnectionProviderResponse {
func isMaxSubscriptionReachedError() -> Bool {
// It is expected to contain payload with corresponding error information
guard let payload = payload else {
return false
}
// Keep this here for backwards compatibility for previously provisioned AppSync services
if let errorType = payload["errorType"],
errorType == "MaxSubscriptionsReachedException" {
return true
}
// The observed response from the service
// { "id":"DB23EC80-C51A-4FEE-82F7-AA4949B4F299",
// "type":"error",
// "payload": {
// "errors": {
// "errorType":"MaxSubscriptionsReachedError",
// "message":"Max number of 100 subscriptions reached" }}}
if let errors = payload["errors"],
case let .object(errorsObject) = errors,
let errorType = errorsObject["errorType"],
errorType == "MaxSubscriptionsReachedError" {
return true
}
return false
}
func isRateLimitExceededError() -> Bool {
// It is expected to contain payload with corresponding error information
guard let payload = payload else {
return false
}
// The observed response from the service
// { "type":"error",
// "payload": {
// "errors": {
// "errorType":"LimitExceededError",
// "message":"Rate limit exceeded" }}}
if let errors = payload["errors"],
case let .object(errorsObject) = errors,
let errorType = errorsObject["errorType"],
errorType == "LimitExceededError" {
return true
}
return false
}
}

View File

@ -218,6 +218,10 @@ class AppSyncRealTimeClientIntegrationTests: AppSyncRealTimeClientTestBase {
wait(for: [expectedPerforms], timeout: 1)
assertStatus(of: realTimeConnectionProvider, equals: .notConnected)
}
func testMaxSubscriptionConnection() {
}
// MARK: - Helpers

View File

@ -0,0 +1,44 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//
import XCTest
@testable import AppSyncRealTimeClient
class RealTimeConnectionProviderResponseTests: XCTestCase {
func testIsMaxSubscriptionReached_EmptyPayload() throws {
let response = RealtimeConnectionProviderResponse(
id: "id",
payload: nil,
type: .error
)
XCTAssertFalse(response.isMaxSubscriptionReachedError())
}
func testIsMaxSubscriptionReached_MaxSubscriptionsReachedException() throws {
let payload = ["errorType": AppSyncJSONValue.string("MaxSubscriptionsReachedException")]
let response = RealtimeConnectionProviderResponse(
id: "id",
payload: payload,
type: .error
)
XCTAssertTrue(response.isMaxSubscriptionReachedError())
}
func testIsMaxSubscriptionReached_MaxSubscriptionsReachedError() throws {
let payload = ["errors": AppSyncJSONValue.object(["errorType": "MaxSubscriptionsReachedError"])]
let response = RealtimeConnectionProviderResponse(
id: "id",
payload: payload,
type: .error
)
XCTAssertTrue(response.isMaxSubscriptionReachedError())
}
}