From 85b89ed592828b7e8d14ea60a914f8edca72416e Mon Sep 17 00:00:00 2001 From: Nicky <31034418+nickybondarenko@users.noreply.github.com> Date: Wed, 17 Apr 2024 14:49:46 +0200 Subject: [PATCH] feat: handle status codes for retrying in uploader (#95) * feat: handle status codes for retrying in uploader --- .../RemoteConfidenceClient.swift | 22 ++-- .../EventSenderEngineTest.swift | 58 --------- .../EventSenderEngineTest.swift | 120 ++++++++++++++++++ .../EventUploaderMock.swift | 11 ++ .../Helpers/MockedClientURLProtocol.swift | 3 + .../RemoteConfidenceClientTests.swift | 25 ---- 6 files changed, 147 insertions(+), 92 deletions(-) delete mode 100644 Tests/ConfidenceProviderTests/EventSenderEngineTest.swift create mode 100644 Tests/ConfidenceTests/EventSenderEngineTest.swift rename Tests/{ConfidenceProviderTests => ConfidenceTests}/EventUploaderMock.swift (80%) diff --git a/Sources/Confidence/ConfidenceClient/RemoteConfidenceClient.swift b/Sources/Confidence/ConfidenceClient/RemoteConfidenceClient.swift index c6a712d5..2b3d6713 100644 --- a/Sources/Confidence/ConfidenceClient/RemoteConfidenceClient.swift +++ b/Sources/Confidence/ConfidenceClient/RemoteConfidenceClient.swift @@ -38,21 +38,25 @@ public class RemoteConfidenceClient: ConfidenceClient { sendTime: timeString, sdk: Sdk(id: metadata.name, version: metadata.version) ) - do { let result: HttpClientResult = try await self.httpClient.post(path: ":publish", data: request) switch result { case .success(let successData): - guard successData.response.status == .ok else { - throw successData.response.mapStatusToError(error: successData.decodedError) - } - let indexedErrorsCount = successData.decodedData?.errors.count ?? 0 - if indexedErrorsCount > 0 { - Logger(subsystem: "com.confidence.client", category: "network").error( - "Backend reported errors for \(indexedErrorsCount) event(s) in batch") + let status = successData.response.statusCode + switch status { + case 200: + // clean up in case of success + return true + case 429: + // we shouldn't clean up for rate limiting + return false + case 400...499: + // if batch couldn't be processed, we should clean it up + return true + default: + return false } - return true case .failure(let errorData): throw handleError(error: errorData) } diff --git a/Tests/ConfidenceProviderTests/EventSenderEngineTest.swift b/Tests/ConfidenceProviderTests/EventSenderEngineTest.swift deleted file mode 100644 index a1978b54..00000000 --- a/Tests/ConfidenceProviderTests/EventSenderEngineTest.swift +++ /dev/null @@ -1,58 +0,0 @@ -import Foundation -import Common -import XCTest - -@testable import Confidence - -final class MinSizeFlushPolicy: FlushPolicy { - private var maxSize = 5 - private var size = 0 - func reset() { - size = 0 - } - - func hit(event: ConfidenceEvent) { - size += 1 - } - - func shouldFlush() -> Bool { - return size >= maxSize - } -} - -final class EventSenderEngineTest: XCTestCase { - func testAddingEventsWithSizeFlushPolicyWorks() throws { - let flushPolicies = [MinSizeFlushPolicy()] - let uploader = EventUploaderMock() - let eventSenderEngine = EventSenderEngineImpl( - clientSecret: "CLIENT_SECRET", - uploader: uploader, - storage: EventStorageMock(), - flushPolicies: flushPolicies - ) - - let expectation = XCTestExpectation(description: "Upload finished") - let cancellable = uploader.subject.sink { _ in - expectation.fulfill() - } - - var events: [ConfidenceEvent] = [] - for i in 0..<5 { - events.append(ConfidenceEvent( - name: "\(i)", - payload: [:], - eventTime: Date.backport.now) - ) - eventSenderEngine.emit(definition: "\(i)", payload: [:], context: [:]) - } - - wait(for: [expectation], timeout: 5) - let uploadRequest = try XCTUnwrap(uploader.calledRequest) - XCTAssertTrue(uploadRequest.map { $0.eventDefinition } == events.map { $0.name }) - - uploader.reset() - eventSenderEngine.emit(definition: "Hello", payload: [:], context: [:]) - XCTAssertNil(uploader.calledRequest) - cancellable.cancel() - } -} diff --git a/Tests/ConfidenceTests/EventSenderEngineTest.swift b/Tests/ConfidenceTests/EventSenderEngineTest.swift new file mode 100644 index 00000000..2c5cb475 --- /dev/null +++ b/Tests/ConfidenceTests/EventSenderEngineTest.swift @@ -0,0 +1,120 @@ +import Foundation +import Common +import XCTest + +@testable import Confidence + +final class MinSizeFlushPolicy: FlushPolicy { + private var maxSize = 5 + private var size = 0 + func reset() { + size = 0 + } + + func hit(event: ConfidenceEvent) { + size += 1 + } + + func shouldFlush() -> Bool { + return size >= maxSize + } +} + +final class ImmidiateFlushPolicy: FlushPolicy { + private var size = 0 + + func reset() { + size = 0 + } + + func hit(event: ConfidenceEvent) { + size += 1 + } + + func shouldFlush() -> Bool { + return size > 0 + } +} + +final class EventSenderEngineTest: XCTestCase { + func testAddingEventsWithSizeFlushPolicyWorks() throws { + let flushPolicies = [MinSizeFlushPolicy()] + let uploader = EventUploaderMock() + let eventSenderEngine = EventSenderEngineImpl( + clientSecret: "CLIENT_SECRET", + uploader: uploader, + storage: EventStorageMock(), + flushPolicies: flushPolicies + ) + + let expectation = XCTestExpectation(description: "Upload finished") + let cancellable = uploader.subject.sink { _ in + expectation.fulfill() + } + + var events: [ConfidenceEvent] = [] + for i in 0..<5 { + events.append(ConfidenceEvent( + name: "\(i)", + payload: [:], + eventTime: Date.backport.now) + ) + eventSenderEngine.emit(definition: "\(i)", payload: [:], context: [:]) + } + + wait(for: [expectation], timeout: 5) + let uploadRequest = try XCTUnwrap(uploader.calledRequest) + XCTAssertTrue(uploadRequest.map { $0.eventDefinition } == events.map { $0.name }) + + uploader.reset() + eventSenderEngine.emit(definition: "Hello", payload: [:], context: [:]) + XCTAssertNil(uploader.calledRequest) + cancellable.cancel() + } + + func testRemoveEventsFromStorageOnBadRequest() throws { + MockedClientURLProtocol.mockedOperation = .badRequest + let client = RemoteConfidenceClient( + options: ConfidenceClientOptions(credentials: ConfidenceClientCredentials.clientSecret(secret: "")), + session: MockedClientURLProtocol.mockedSession(), + metadata: ConfidenceMetadata(name: "", version: "")) + + let flushPolicies = [ImmidiateFlushPolicy()] + let storage = EventStorageMock() + let eventSenderEngine = EventSenderEngineImpl( + clientSecret: "CLIENT_SECRET", + uploader: client, + storage: storage, + flushPolicies: flushPolicies + ) + eventSenderEngine.emit(definition: "testEvent", payload: ConfidenceStruct(), context: ConfidenceStruct()) + let expectation = expectation(description: "events batched") + storage.eventsRemoved{ + expectation.fulfill() + } + wait(for: [expectation], timeout: 2) + + XCTAssertEqual(storage.isEmpty(), true) + } + + func testKeepEventsInStorageForRetry() throws { + MockedClientURLProtocol.mockedOperation = .needRetryLater + let client = RemoteConfidenceClient( + options: ConfidenceClientOptions(credentials: ConfidenceClientCredentials.clientSecret(secret: "")), + session: MockedClientURLProtocol.mockedSession(), + metadata: ConfidenceMetadata(name: "", version: "")) + + let flushPolicies = [ImmidiateFlushPolicy()] + let storage = EventStorageMock() + let eventSenderEngine = EventSenderEngineImpl( + clientSecret: "CLIENT_SECRET", + uploader: client, + storage: storage, + flushPolicies: flushPolicies + ) + + eventSenderEngine.emit(definition: "testEvent", payload: ConfidenceStruct(), context: ConfidenceStruct()) + + XCTAssertEqual(storage.isEmpty(), false) + } +} diff --git a/Tests/ConfidenceProviderTests/EventUploaderMock.swift b/Tests/ConfidenceTests/EventUploaderMock.swift similarity index 80% rename from Tests/ConfidenceProviderTests/EventUploaderMock.swift rename to Tests/ConfidenceTests/EventUploaderMock.swift index 4e96bab9..ffffeb46 100644 --- a/Tests/ConfidenceProviderTests/EventUploaderMock.swift +++ b/Tests/ConfidenceTests/EventUploaderMock.swift @@ -20,6 +20,8 @@ final class EventUploaderMock: ConfidenceClient { final class EventStorageMock: EventStorage { private var events: [ConfidenceEvent] = [] private var batches: [String: [ConfidenceEvent]] = [:] + var removeCallback: () -> Void = {} + func startNewBatch() throws { batches[("\(batches.count)")] = events events.removeAll() @@ -42,5 +44,14 @@ final class EventStorageMock: EventStorage { func remove(id: String) throws { batches.removeValue(forKey: id) + removeCallback() + } + + internal func isEmpty() -> Bool { + return self.events.isEmpty && self.batches.isEmpty + } + + internal func eventsRemoved(callback: @escaping () -> Void) { + removeCallback = callback } } diff --git a/Tests/ConfidenceTests/Helpers/MockedClientURLProtocol.swift b/Tests/ConfidenceTests/Helpers/MockedClientURLProtocol.swift index ee5d80cf..fbd15dce 100644 --- a/Tests/ConfidenceTests/Helpers/MockedClientURLProtocol.swift +++ b/Tests/ConfidenceTests/Helpers/MockedClientURLProtocol.swift @@ -12,6 +12,7 @@ class MockedClientURLProtocol: URLProtocol { case malformedResponse case badRequest case success + case needRetryLater } override class func canInit(with request: URLRequest) -> Bool { @@ -65,6 +66,8 @@ class MockedClientURLProtocol: URLProtocol { switch MockedClientURLProtocol.mockedOperation { case .badRequest: respondWithError(statusCode: 400, code: 0, message: "explanation about malformed request") + case .needRetryLater: + respondWithError(statusCode: 502, code: 0, message: "service unavailable") case .malformedResponse: malformedResponse() case .firstEventFails: diff --git a/Tests/ConfidenceTests/RemoteConfidenceClientTests.swift b/Tests/ConfidenceTests/RemoteConfidenceClientTests.swift index dd448ba5..c17aace4 100644 --- a/Tests/ConfidenceTests/RemoteConfidenceClientTests.swift +++ b/Tests/ConfidenceTests/RemoteConfidenceClientTests.swift @@ -56,31 +56,6 @@ class RemoteConfidenceClientTest: XCTestCase { XCTAssertTrue(processed) } - func testBadRequestThrows() async throws { - MockedClientURLProtocol.mockedOperation = .badRequest - let client = RemoteConfidenceClient( - options: ConfidenceClientOptions( - credentials: ConfidenceClientCredentials.clientSecret(secret: "")), - session: MockedClientURLProtocol.mockedSession(), - metadata: ConfidenceMetadata(name: "", version: "")) - - var caughtError: ConfidenceError? - do { - _ = try await client.upload(events: [ - NetworkEvent( - eventDefinition: "testEvent", - payload: NetworkStruct.init(fields: [:]), - eventTime: Date.backport.nowISOString - ) - ]) - } catch { - // swiftlint:disable:next force_cast - caughtError = error as! ConfidenceError? - } - let expectedError = ConfidenceError.badRequest(message: "explanation about malformed request") - XCTAssertEqual(caughtError, expectedError) - } - func testNMalformedResponseThrows() async throws { MockedClientURLProtocol.mockedOperation = .malformedResponse let client = RemoteConfidenceClient(