Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add timeout to fetchAndActivate #160

Merged
merged 11 commits into from
Jul 16, 2024
21 changes: 18 additions & 3 deletions Sources/Confidence/Confidence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,8 @@ public class Confidence: ConfidenceEventSender {
storage: storage,
context: context,
parent: self,
debugLogger: debugLogger)
debugLogger: debugLogger
)
}
}

Expand All @@ -288,6 +289,7 @@ extension Confidence {
internal var region: ConfidenceRegion = .global
internal var metadata: ConfidenceMetadata?
internal var initialContext: ConfidenceStruct = [:]
internal var timeout: Double = 10

// Injectable for testing
internal var flagApplier: FlagApplier?
Expand Down Expand Up @@ -337,6 +339,15 @@ extension Confidence {
return self
}

/**
Sets the timeout for the network requests to the Confidence backend.
The default is 10 seconds.
*/
public func withTimeout(timeout: Double) -> Builder {
nickybondarenko marked this conversation as resolved.
Show resolved Hide resolved
self.timeout = timeout
return self
}

public func build() -> Confidence {
var debugLogger: DebugLogger?
if loggerLevel != LoggerLevel.NONE {
Expand All @@ -347,15 +358,19 @@ extension Confidence {
}
let options = ConfidenceClientOptions(
credentials: ConfidenceClientCredentials.clientSecret(secret: clientSecret),
region: region)
region: region,
timeoutIntervalForRequest: timeout)
let metadata = ConfidenceMetadata(
name: sdkId,
version: "0.2.4") // x-release-please-version
let uploader = RemoteConfidenceClient(
options: options,
metadata: metadata
)
let httpClient = NetworkClient(baseUrl: BaseUrlMapper.from(region: options.region))
let httpClient = NetworkClient(
baseUrl: BaseUrlMapper.from(region: options.region),
timeoutIntervalForRequests: options.timeoutIntervalForRequest
)
let flagApplier = flagApplier ?? FlagApplierWithRetries(
httpClient: httpClient,
storage: DefaultStorage(filePath: "confidence.flags.apply"),
Expand Down
5 changes: 4 additions & 1 deletion Sources/Confidence/ConfidenceClientOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ struct ConfidenceClientOptions {
public var credentials: ConfidenceClientCredentials
public var region: ConfidenceRegion
public var initializationStrategy: InitializationStrategy
public var timeoutIntervalForRequest: Double

public init(
credentials: ConfidenceClientCredentials,
region: ConfidenceRegion? = nil,
initializationStrategy: InitializationStrategy = .fetchAndActivate
initializationStrategy: InitializationStrategy = .fetchAndActivate,
timeoutIntervalForRequest: Double = 0
nickybondarenko marked this conversation as resolved.
Show resolved Hide resolved
) {
self.credentials = credentials
self.region = region ?? .global
self.initializationStrategy = initializationStrategy
self.timeoutIntervalForRequest = timeoutIntervalForRequest
}
}

Expand Down
5 changes: 4 additions & 1 deletion Sources/Confidence/Http/NetworkClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ final class NetworkClient: HttpClient {
private let retry: Retry
private let session: URLSession
private let baseUrl: String
private var timeoutIntervalForRequests: Double

public init(
session: URLSession? = nil,
baseUrl: String,
defaultHeaders: [String: String] = [:],
retry: Retry = .none
retry: Retry = .none,
timeoutIntervalForRequests: Double
) {
self.session =
session
Expand All @@ -24,6 +26,7 @@ final class NetworkClient: HttpClient {
self.headers = defaultHeaders
self.retry = retry
self.baseUrl = baseUrl
self.timeoutIntervalForRequests = timeoutIntervalForRequests
nickybondarenko marked this conversation as resolved.
Show resolved Hide resolved
}

public func post<T: Decodable>(
Expand Down
6 changes: 5 additions & 1 deletion Sources/Confidence/RemoteConfidenceClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ public class RemoteConfidenceClient: ConfidenceClient {
case .usa:
self.baseUrl = "https://events.us.confidence.dev/v1/events"
}
self.httpClient = NetworkClient(session: session, baseUrl: baseUrl)
self.httpClient = NetworkClient(
session: session,
baseUrl: baseUrl,
timeoutIntervalForRequests: options.timeoutIntervalForRequest
)
self.metadata = metadata
self.debugLogger = debugLogger
}
Expand Down
37 changes: 19 additions & 18 deletions Sources/Confidence/RemoteResolveConfidenceClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ class RemoteConfidenceResolveClient: ConfidenceResolveClient {
self.metadata = metadata
self.httpClient = NetworkClient(
session: session,
baseUrl: BaseUrlMapper.from(region: options.region))
baseUrl: BaseUrlMapper.from(region: options.region),
timeoutIntervalForRequests: options.timeoutIntervalForRequest)
}

// MARK: Resolver
Expand All @@ -34,24 +35,24 @@ class RemoteConfidenceResolveClient: ConfidenceResolveClient {
)

do {
let result: HttpClientResult<ResolveFlagsResponse> =
try await self.httpClient.post(path: ":resolve", data: request)
switch result {
case .success(let successData):
guard successData.response.status == .ok else {
throw successData.response.mapStatusToError(error: successData.decodedError)
let result: HttpClientResult<ResolveFlagsResponse> =
try await self.httpClient.post(path: ":resolve", data: request)
switch result {
case .success(let successData):
guard successData.response.status == .ok else {
throw successData.response.mapStatusToError(error: successData.decodedError)
}
guard let response = successData.decodedData else {
throw ConfidenceError.parseError(message: "Unable to parse request response")
}
let resolvedValues = try response.resolvedFlags.map { resolvedFlag in
try convert(resolvedFlag: resolvedFlag)
}
return ResolvesResult(resolvedValues: resolvedValues, resolveToken: response.resolveToken)
case .failure(let errorData):
throw handleError(error: errorData)
}
}
guard let response = successData.decodedData else {
throw ConfidenceError.parseError(message: "Unable to parse request response")
}
let resolvedValues = try response.resolvedFlags.map { resolvedFlag in
try convert(resolvedFlag: resolvedFlag)
}
return ResolvesResult(resolvedValues: resolvedValues, resolveToken: response.resolveToken)
case .failure(let errorData):
throw handleError(error: errorData)
}
}
}

public func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult {
Expand Down
39 changes: 39 additions & 0 deletions Tests/ConfidenceTests/ConfidenceTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,45 @@ class ConfidenceTest: XCTestCase {
XCTAssertEqual(error as? ConfidenceError, ConfidenceError.invalidContextInMessage)
}
}

func testRequestTimedOut() async throws {
class FakeClient: ConfidenceResolveClient {
var resolvedValues: [ResolvedValue] = []
func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult {
try await Task.sleep(nanoseconds: 5000000000)
return .init(resolvedValues: resolvedValues, resolveToken: "token")
}
}

let client = FakeClient()
client.resolvedValues = [
ResolvedValue(
variant: "default",
value: .init(structure: ["size": .init(integer: 3)]),
flag: "flag",
resolveReason: .match)
]

let confidence = Confidence.Builder(clientSecret: "test")
.withContext(initialContext: ["targeting_key": .init(string: "user2")])
.withFlagResolverClient(flagResolver: client)
.withFlagApplier(flagApplier: flagApplier)
.withTimeout(timeout: 1)
.build()

try await confidence.fetchAndActivate()
let evaluation = try confidence.getEvaluation(
key: "flag.size",
defaultValue: 0)

XCTAssertEqual(evaluation.value, 3)
nickybondarenko marked this conversation as resolved.
Show resolved Hide resolved
XCTAssertNil(evaluation.errorCode)
XCTAssertNil(evaluation.errorMessage)
XCTAssertEqual(evaluation.reason, .match)
XCTAssertEqual(evaluation.variant, "default")
await fulfillment(of: [flagApplier.applyExpectation], timeout: 1)
XCTAssertEqual(flagApplier.applyCallCount, 1)
}
}

final class DispatchQueueFake: DispatchQueueType {
Expand Down