Skip to content

Commit

Permalink
Retry Edge Hits for Recoverable URLErrors (#467)
Browse files Browse the repository at this point in the history
* Retry edge hits for recoverable URLErrors

* Add core pod from branch

* Refactor code to make it clean

* Update cocoapods cache version

* Update logic to use isRecoverable extension property

* Fix format and other swiftlint warnings

* Update to production pods

* Add explicit dependency on Services and bumped up min dependency version

---------

Co-authored-by: Emilia Dobrin <33132425+emdobrin@users.noreply.github.com>
Co-authored-by: Kevin Lind <40409666+kevinlind@users.noreply.github.com>
  • Loading branch information
3 people authored May 22, 2024
1 parent 0f9dbdc commit aa01c9b
Show file tree
Hide file tree
Showing 11 changed files with 165 additions and 43 deletions.
24 changes: 12 additions & 12 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ version: 2.1
orbs:
# codecov: codecov/codecov@3.2.5
macos: circleci/macos@2

# Workflows orchestrate a set of jobs to be run
workflows:
build-test:
Expand Down Expand Up @@ -36,7 +36,7 @@ workflows:
- main
- staging

commands:
commands:
install_dependencies:
steps:
# restore pods related caches
Expand All @@ -61,9 +61,9 @@ commands:
- restore_cache:
name: Restoring CocoaPods Cache
keys:
- cocoapods-cache-v5-{{ arch }}-{{ .Branch }}-{{ checksum "Podfile.lock" }}
- cocoapods-cache-v5-{{ arch }}-{{ .Branch }}
- cocoapods-cache-v5
- cocoapods-cache-v6-{{ arch }}-{{ .Branch }}-{{ checksum "Podfile.lock" }}
- cocoapods-cache-v6-{{ arch }}-{{ .Branch }}
- cocoapods-cache-v6

# install CocoaPods - using default CocoaPods version, not the bundle
- run:
Expand All @@ -73,7 +73,7 @@ commands:
# save pods related files
- save_cache:
name: Saving CocoaPods Cache
key: cocoapods-cache-v5-{{ arch }}-{{ .Branch }}-{{ checksum "Podfile.lock" }}
key: cocoapods-cache-v6-{{ arch }}-{{ .Branch }}-{{ checksum "Podfile.lock" }}
paths:
- ./Pods
- ./SampleApps/TestApp/Pods
Expand Down Expand Up @@ -139,7 +139,7 @@ jobs:
# flags: ios-functional-tests
# upload_name: Coverage report for iOS functional tests
# xtra_args: -c -v --xc --xp build/reports/iosFunctionalResults.xcresult

test-ios-integration:
macos:
xcode: 15.1.0 # Specify the Xcode version to use
Expand All @@ -166,7 +166,7 @@ jobs:
- install_dependencies

- prestart_tvos_simulator

- run:
name: Run tvOS Unit Tests
command: make unit-test-tvos
Expand All @@ -187,11 +187,11 @@ jobs:
# flags: tvos-functional-tests
# upload_name: Coverage report for tvOS functional tests
# xtra_args: -c -v --xc --xp build/reports/tvosFunctionalResults.xcresult
build_xcframework_and_app:

build_xcframework_and_app:
macos:
xcode: 15.1.0 # Specify the Xcode version to use

steps:
- checkout
# Verify XCFramework archive builds
Expand All @@ -202,4 +202,4 @@ jobs:
- run:
name: Build Test App
command: make build-app

1 change: 1 addition & 0 deletions AEPEdge.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Pod::Spec.new do |s|
s.pod_target_xcconfig = { 'BUILD_LIBRARY_FOR_DISTRIBUTION' => 'YES' }
s.dependency 'AEPCore', '>= 5.0.0', '< 6.0.0'
s.dependency 'AEPEdgeIdentity', '>= 5.0.0', '< 6.0.0'
s.dependency 'AEPServices', '>= 5.1.0', '< 6.0.0'

s.source_files = 'Sources/**/*.swift'
end
5 changes: 3 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ let package = Package(
.library(name: "AEPEdge", targets: ["AEPEdge"])
],
dependencies: [
.package(url: "https://github.com/adobe/aepsdk-core-ios.git", .upToNextMajor(from: "5.0.0")),
.package(url: "https://github.com/adobe/aepsdk-core-ios.git", .upToNextMajor(from: "5.1.0")),
.package(url: "https://github.com/adobe/aepsdk-edgeidentity-ios.git", .upToNextMajor(from: "5.0.0"))
],
targets: [
.target(name: "AEPEdge",
dependencies: [
.product(name: "AEPCore", package: "aepsdk-core-ios"),
.product(name: "AEPCore", package: "aepsdk-core-ios"),
.product(name: "AEPServices", package: "aepsdk-core-ios"),
.product(name: "AEPEdgeIdentity", package: "aepsdk-edgeidentity-ios")
],
path: "Sources")
Expand Down
1 change: 1 addition & 0 deletions Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pod 'SwiftLint', '0.52.0'

def core_pods
pod 'AEPCore'
pod 'AEPServices'
end

def edge_pods
Expand Down
18 changes: 10 additions & 8 deletions Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@ PODS:
- AEPAssurance (5.0.0):
- AEPCore (< 6.0.0, >= 5.0.0)
- AEPServices (< 6.0.0, >= 5.0.0)
- AEPCore (5.0.0):
- AEPCore (5.1.0):
- AEPRulesEngine (< 6.0.0, >= 5.0.0)
- AEPServices (< 6.0.0, >= 5.0.0)
- AEPServices (< 6.0.0, >= 5.1.0)
- AEPEdge (5.0.1):
- AEPCore (< 6.0.0, >= 5.0.0)
- AEPEdgeIdentity (< 6.0.0, >= 5.0.0)
- AEPServices (< 6.0.0, >= 5.1.0)
- AEPEdgeConsent (5.0.0):
- AEPCore (< 6.0.0, >= 5.0.0)
- AEPEdge (< 6.0.0, >= 5.0.0)
- AEPEdgeIdentity (5.0.0):
- AEPCore (< 6.0.0, >= 5.0.0)
- AEPRulesEngine (5.0.0)
- AEPServices (5.0.0)
- AEPServices (5.1.0)
- AEPTestUtils (5.0.0):
- AEPCore
- AEPServices
Expand All @@ -26,6 +27,7 @@ DEPENDENCIES:
- AEPEdge (from `./AEPEdge.podspec`)
- AEPEdgeConsent
- AEPEdgeIdentity
- AEPServices
- AEPTestUtils (from `https://github.com/adobe/aepsdk-testutils-ios.git`, tag `5.0.0`)
- SwiftLint (= 0.52.0)

Expand Down Expand Up @@ -53,15 +55,15 @@ CHECKOUT OPTIONS:

SPEC CHECKSUMS:
AEPAssurance: 7f260ded4df38a70a06efebade8c33a3e3221984
AEPCore: f1c3e9238bb12e7e1103f4407c341ebc65aeab5b
AEPEdge: 0873041dfb29f3126260f2dc16d548a1fefbe0c4
AEPCore: 55489e1c4e48a88659055f8e96290f2cccce9747
AEPEdge: 013661d205b3d4a12432201c6f6c50ad707df18b
AEPEdgeConsent: d7db1d19eb4c1e2146360ed3c8df315f671b26d5
AEPEdgeIdentity: 3161ff33434586962946912d6b8e9e8fca1c4d23
AEPRulesEngine: fe5800653a4bee07b1e41e61b4d5551f0dba557b
AEPServices: e42e5118128e81c0f797fdfb1dc9c4a714d644b8
AEPServices: c38b809c32336f10f773525e65c71a949abe54a7
AEPTestUtils: 20495b368da57904ca2e9f241d1d8b114f9887b5
SwiftLint: 13280e21cdda6786ad908dc6e416afe5acd1fcb7

PODFILE CHECKSUM: 559b845c969523123ac98dd7a3e4559871b3c2aa
PODFILE CHECKSUM: 0beefce457cb2f47f1caa5a1f85d8e90e4fd47d4

COCOAPODS: 1.14.3
COCOAPODS: 1.13.0
19 changes: 17 additions & 2 deletions Sources/EdgeNetworkHandlers/EdgeNetworkService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,30 @@ class EdgeNetworkService {

ServiceProvider.shared.networkService.connectAsync(networkRequest: networkRequest) { (connection: HttpConnection) in
if connection.error != nil {
// handle generic error
// retry for recoverable url error codes
if let urlError = connection.error as? URLError, urlError.isRecoverable {
let retryInterval = EdgeConstants.Defaults.RETRY_INTERVAL
let errorMessage = "failed with recoverable URL error:(\(urlError.localizedDescription)) code:(\(urlError.errorCode))."

Log.debug(label: EdgeConstants.LOG_TAG,
"\(self.SELF_TAG) - Edge request with url:\(url.absoluteString) \(errorMessage)). Will retry request in \(retryInterval) seconds.")

completion(false, retryInterval) // failed, but recoverable so retry
return
}

// handle non-recoverable URLErrors and other non URLErrors
let errorMessage = "failed with unrecoverable error:(\(connection.error?.localizedDescription ?? "Unknown Error")) code:(\(connection.responseCode ?? -1))"
Log.warning(label: EdgeConstants.LOG_TAG,
"\(self.SELF_TAG) - Edge request with url:\(url.absoluteString) \(errorMessage). Dropping the request.")
self.handleError(connection: connection, responseCallback: responseCallback)
responseCallback.onComplete()
completion(true, nil) // don't retry
return
}

guard let responseCode = connection.responseCode else {
Log.warning(label: EdgeConstants.LOG_TAG, "\(self.SELF_TAG) - Connection to Experience Edge returned unknown error")
Log.warning(label: EdgeConstants.LOG_TAG, "\(self.SELF_TAG) - Edge request with url:\(url.absoluteString) failed with unknown error. Dropping the request.")
self.handleError(connection: connection, responseCallback: responseCallback)
responseCallback.onComplete()
completion(true, nil) // failed, but unrecoverable, don't retry
Expand Down
90 changes: 83 additions & 7 deletions Tests/FunctionalTests/AEPEdgeFunctionalTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,8 @@ class AEPEdgeFunctionalTests: TestBase, AnyCodableAsserts {
"xdm.identityMap.ECID[0].authenticatedState",
"xdm.identityMap.ECID[0].primary",
"events[0].xdm._id",
"events[0].xdm.timestamp"))
"events[0].xdm.timestamp",
"meta.konductorConfig.streaming.recordSeparator"))

let requestUrl = resultNetworkRequests[0].url
XCTAssertTrue(requestUrl.absoluteURL.absoluteString.hasPrefix(TestConstants.EX_EDGE_INTERACT_PROD_URL_STR))
Expand Down Expand Up @@ -415,7 +416,7 @@ class AEPEdgeFunctionalTests: TestBase, AnyCodableAsserts {
pathOptions:
CollectionEqualCount(scope: .subtree),
ValueTypeMatch(paths: "xdm.identityMap.ECID", scope: .subtree),
ValueTypeMatch(paths: "events[0].xdm._id", "events[0].xdm.timestamp"))
ValueTypeMatch(paths: "events[0].xdm._id", "events[0].xdm.timestamp", "meta.konductorConfig.streaming.recordSeparator"))

let requestUrl = resultNetworkRequests[0].url
XCTAssertTrue(requestUrl.absoluteURL.absoluteString.hasPrefix(TestConstants.EX_EDGE_INTERACT_PROD_URL_STR))
Expand Down Expand Up @@ -479,7 +480,7 @@ class AEPEdgeFunctionalTests: TestBase, AnyCodableAsserts {
pathOptions:
CollectionEqualCount(scope: .subtree),
ValueTypeMatch(paths: "xdm.identityMap.ECID", scope: .subtree),
ValueTypeMatch(paths: "events[0].xdm._id", "events[0].xdm.timestamp"))
ValueTypeMatch(paths: "events[0].xdm._id", "events[0].xdm.timestamp", "meta.konductorConfig.streaming.recordSeparator"))

let requestUrl = resultNetworkRequests[0].url
XCTAssertTrue(requestUrl.absoluteURL.absoluteString.hasPrefix(TestConstants.EX_EDGE_INTERACT_PROD_URL_STR))
Expand Down Expand Up @@ -647,7 +648,8 @@ class AEPEdgeFunctionalTests: TestBase, AnyCodableAsserts {
assertTypeMatch(
expected: createExpectedPayload(),
actual: resultNetworkRequests[0],
pathOptions: CollectionEqualCount(scope: .subtree))
pathOptions: CollectionEqualCount(scope: .subtree),
ValueTypeMatch(paths: "meta.konductorConfig.streaming.recordSeparator", scope: .subtree))

resetTestExpectations()
mockNetworkService.reset()
Expand Down Expand Up @@ -725,7 +727,8 @@ class AEPEdgeFunctionalTests: TestBase, AnyCodableAsserts {
assertTypeMatch(
expected: createExpectedPayload(),
actual: resultNetworkRequests[0],
pathOptions: CollectionEqualCount(scope: .subtree))
pathOptions: CollectionEqualCount(scope: .subtree),
ValueTypeMatch(paths: "meta.konductorConfig.streaming.recordSeparator", scope: .subtree))

resetTestExpectations()
mockNetworkService.reset()
Expand Down Expand Up @@ -822,7 +825,8 @@ class AEPEdgeFunctionalTests: TestBase, AnyCodableAsserts {
assertTypeMatch(
expected: createExpectedPayload(),
actual: resultNetworkRequests[0],
pathOptions: CollectionEqualCount(scope: .subtree))
pathOptions: CollectionEqualCount(scope: .subtree),
ValueTypeMatch(paths: "meta.konductorConfig.streaming.recordSeparator", scope: .subtree))
}

// MARK: Paired request-response events
Expand Down Expand Up @@ -1171,6 +1175,78 @@ class AEPEdgeFunctionalTests: TestBase, AnyCodableAsserts {
XCTAssertEqual(expectedEdgeEventError, edgeEventError)
}

func testSendEvent_recoverableNetworkTransportError_retries() {
let edgeResponse = EdgeResponse(requestId: "test-req-id", handle: nil, errors: nil, warnings: nil)
let responseData = try? JSONEncoder().encode(edgeResponse)

// no connection, hits will be retried
let responseConnection: HttpConnection = HttpConnection(data: responseData,
response: nil,
error: URLError(URLError.notConnectedToInternet) as Error)
mockNetworkService.setMockResponse(url: TestConstants.EX_EDGE_INTERACT_PROD_URL_STR, httpMethod: HttpMethod.post, responseConnection: responseConnection)
mockNetworkService.setExpectationForNetworkRequest(url: TestConstants.EX_EDGE_INTERACT_PROD_URL_STR, httpMethod: HttpMethod.post, expectedCount: 1)

let experienceEvent = ExperienceEvent(xdm: ["testString": "xdm",
"testInt": 10,
"testBool": false,
"testDouble": 12.89,
"testArray": ["arrayElem1", 2, true],
"testDictionary": ["key": "val"]])
Edge.sendEvent(experienceEvent: experienceEvent)
mockNetworkService.assertAllNetworkRequestExpectations()
resetTestExpectations()
mockNetworkService.reset()

// good connection, hits sent
let httpConnection: HttpConnection = HttpConnection(data: responseBody.data(using: .utf8),
response: HTTPURLResponse(url: exEdgeInteractProdUrl,
statusCode: 200,
httpVersion: nil,
headerFields: nil),
error: nil)
mockNetworkService.setExpectationForNetworkRequest(url: TestConstants.EX_EDGE_INTERACT_PROD_URL_STR, httpMethod: HttpMethod.post, expectedCount: 1)
mockNetworkService.setMockResponse(url: TestConstants.EX_EDGE_INTERACT_PROD_URL_STR, httpMethod: HttpMethod.post, responseConnection: httpConnection)

mockNetworkService.assertAllNetworkRequestExpectations()
}

func testSendEvent_unrecoverableNetworkTransportError_noRetry() {
let response = """
{
"title":"Unexpected Error",
"detail": "Request to Experience Edge failed with an unknown exception"
}
"""

// no connection, hits will be retried
let responseConnection: HttpConnection = HttpConnection(data: response.data(using: .utf8),
response: nil,
error: URLError(URLError.cannotFindHost) as Error)
mockNetworkService.setMockResponse(url: TestConstants.EX_EDGE_INTERACT_PROD_URL_STR, httpMethod: HttpMethod.post, responseConnection: responseConnection)
mockNetworkService.setExpectationForNetworkRequest(url: TestConstants.EX_EDGE_INTERACT_PROD_URL_STR, httpMethod: HttpMethod.post, expectedCount: 1)
setExpectationEvent(type: TestConstants.EventType.EDGE, source: TestConstants.EventSource.REQUEST_CONTENT, expectedCount: 1) // the send event
setExpectationEvent(type: TestConstants.EventType.EDGE, source: TestConstants.EventSource.ERROR_RESPONSE_CONTENT, expectedCount: 1) // 1 error events

let experienceEvent = ExperienceEvent(xdm: ["testString": "xdm"], data: nil)
Edge.sendEvent(experienceEvent: experienceEvent)

mockNetworkService.assertAllNetworkRequestExpectations()
assertExpectedEvents(ignoreUnexpectedEvents: false)

let resultEvents = getDispatchedEventsWith(type: TestConstants.EventType.EDGE,
source: TestConstants.EventSource.ERROR_RESPONSE_CONTENT)
guard let eventDataDict = resultEvents[0].data else {
XCTFail("Failed to convert event data to [String: Any]")
return
}

let jsonData = try! JSONSerialization.data(withJSONObject: eventDataDict)
let expectedEdgeEventError = try? JSONDecoder().decode(EdgeEventError.self, from: response.data(using: .utf8)!)
let edgeEventError = try? JSONDecoder().decode(EdgeEventError.self, from: jsonData)

XCTAssertEqual(expectedEdgeEventError, edgeEventError)
}

// MARK: Test Send Event with Configurable Endpoint
func testSendEvent_withConfigurableEndpoint_withEmptyConfigEndpoint_UsesProduction() {
let responseConnection: HttpConnection = HttpConnection(data: responseBody.data(using: .utf8),
Expand Down Expand Up @@ -1410,7 +1486,7 @@ class AEPEdgeFunctionalTests: TestBase, AnyCodableAsserts {
"konductorConfig": {
"streaming": {
"enabled": true,
"recordSeparator": "\u0000",
"recordSeparator": "STRING_TYPE",
"lineFeed": "\n"
}
},
Expand Down
12 changes: 6 additions & 6 deletions Tests/FunctionalTests/Edge+ConsentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ class EdgeConsentTests: TestBase, AnyCodableAsserts {
"konductorConfig": {
"streaming": {
"enabled": true,
"recordSeparator": "\u0000",
"recordSeparator": "STRING_TYPE",
"lineFeed": "\n"
}
}
Expand All @@ -271,8 +271,8 @@ class EdgeConsentTests: TestBase, AnyCodableAsserts {
expected: expectedJSON,
actual: consentRequests[0],
pathOptions:
ValueTypeMatch(paths: "identityMap.ECID[0].id", "consent[0].value.metadata.time"),
CollectionEqualCount(scope: .subtree))
CollectionEqualCount(scope: .subtree),
ValueTypeMatch(paths: "identityMap.ECID[0].id", "consent[0].value.metadata.time", "meta.konductorConfig.streaming.recordSeparator"))
}

func testCollectConsentYes_sendsRequestToEdgeNetwork() {
Expand Down Expand Up @@ -322,7 +322,7 @@ class EdgeConsentTests: TestBase, AnyCodableAsserts {
"konductorConfig": {
"streaming": {
"enabled": true,
"recordSeparator": "\u0000",
"recordSeparator": "STRING_TYPE",
"lineFeed": "\n"
}
}
Expand All @@ -333,8 +333,8 @@ class EdgeConsentTests: TestBase, AnyCodableAsserts {
expected: expectedJSON,
actual: consentRequests[0],
pathOptions:
ValueTypeMatch(paths: "identityMap.ECID[0].id", "consent[0].value.metadata.time"),
CollectionEqualCount(scope: .subtree))
CollectionEqualCount(scope: .subtree),
ValueTypeMatch(paths: "identityMap.ECID[0].id", "consent[0].value.metadata.time", "meta.konductorConfig.streaming.recordSeparator"))
}

func testCollectConsentOtherThanYesNo_doesNotSendRequestToEdgeNetwork() {
Expand Down
4 changes: 2 additions & 2 deletions Tests/FunctionalTests/EdgeQueuedEntityFunctionalTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,8 @@ class EdgeQueuedEntityFunctionalTests: TestBase, AnyCodableAsserts {
/// - entityId: the UUID to identify the `DataEntity`
private func mockQueuedEvent(dataQueue: DataQueue, edgeConfig: [String: AnyCodable], entityId: String = "entity-uuid") {
let experienceEvent = Event(name: "queued event", type: EventType.edge, source: EventSource.requestContent, data: ["xdm": ["test": "data"]])
var edgeEntity = EdgeDataEntity(event: experienceEvent, configuration: edgeConfig, identityMap: [:])
var entity = DataEntity(uniqueIdentifier: entityId, timestamp: Date(), data: try? JSONEncoder().encode(edgeEntity))
let edgeEntity = EdgeDataEntity(event: experienceEvent, configuration: edgeConfig, identityMap: [:])
let entity = DataEntity(uniqueIdentifier: entityId, timestamp: Date(), data: try? JSONEncoder().encode(edgeEntity))

dataQueue.add(dataEntity: entity)
}
Expand Down
Loading

0 comments on commit aa01c9b

Please sign in to comment.