Skip to content

Commit

Permalink
feat: handle status codes for retrying in uploader (#95)
Browse files Browse the repository at this point in the history
* feat: handle status codes for retrying in uploader
  • Loading branch information
nickybondarenko authored Apr 17, 2024
1 parent 8f253ce commit 85b89ed
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 92 deletions.
22 changes: 13 additions & 9 deletions Sources/Confidence/ConfidenceClient/RemoteConfidenceClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,25 @@ public class RemoteConfidenceClient: ConfidenceClient {
sendTime: timeString,
sdk: Sdk(id: metadata.name, version: metadata.version)
)

do {
let result: HttpClientResult<PublishEventResponse> =
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)
}
Expand Down
58 changes: 0 additions & 58 deletions Tests/ConfidenceProviderTests/EventSenderEngineTest.swift

This file was deleted.

120 changes: 120 additions & 0 deletions Tests/ConfidenceTests/EventSenderEngineTest.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
}
}
3 changes: 3 additions & 0 deletions Tests/ConfidenceTests/Helpers/MockedClientURLProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class MockedClientURLProtocol: URLProtocol {
case malformedResponse
case badRequest
case success
case needRetryLater
}

override class func canInit(with request: URLRequest) -> Bool {
Expand Down Expand Up @@ -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:
Expand Down
25 changes: 0 additions & 25 deletions Tests/ConfidenceTests/RemoteConfidenceClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit 85b89ed

Please sign in to comment.