Skip to content

Commit

Permalink
Merge pull request #35 from spotify/networking
Browse files Browse the repository at this point in the history
Concurrent networking
  • Loading branch information
Calibretto committed Aug 1, 2023
2 parents bdf364f + b2a20c6 commit 83c5ccb
Show file tree
Hide file tree
Showing 12 changed files with 384 additions and 259 deletions.
37 changes: 23 additions & 14 deletions Sources/ConfidenceProvider/Apply/FlagApplierWithRetries.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import Foundation
import OpenFeature
import os

typealias ApplyFlagHTTPResponse = HttpClientResponse<ApplyFlagsResponse>
typealias ApplyFlagResult = Result<ApplyFlagHTTPResponse, Error>

final class FlagApplierWithRetries: FlagApplier {
private let storage: Storage
private let httpClient: HttpClient
Expand Down Expand Up @@ -83,7 +86,11 @@ final class FlagApplierWithRetries: FlagApplier {
try? storage.save(data: storedData)
}

private func executeApply(resolveToken: String, items: [FlagApply], completion: @escaping (Bool) -> Void) {
private func executeApply(
resolveToken: String,
items: [FlagApply],
completion: @escaping (Bool) -> Void
) {
let applyFlagRequestItems = items.map { applyEvent in
AppliedFlagRequestItem(
flag: applyEvent.name,
Expand All @@ -97,23 +104,25 @@ final class FlagApplierWithRetries: FlagApplier {
resolveToken: resolveToken
)

do {
try performRequest(request: request)
completion(true)
} catch {
self.logApplyError(error: error)
completion(false)
performRequest(request: request) { result in
switch(result) {
case .success(_):
completion(true)
case .failure(let error):
self.logApplyError(error: error)
completion(false)
}
}
}

private func performRequest(request: ApplyFlagsRequest) throws {
private func performRequest(
request: ApplyFlagsRequest,
completion: @escaping (ApplyFlagResult) -> Void
) {
do {
let result = try self.httpClient.post(path: ":apply", data: request, resultType: ApplyFlagsResponse.self)
guard result.response.status == .ok else {
throw result.response.mapStatusToError(error: result.decodedError)
}
} catch let error {
throw handleError(error: error)
try httpClient.post(path: ":apply", data: request, completion: completion)
} catch {
completion(.failure(handleError(error: error)))
}
}

Expand Down
64 changes: 33 additions & 31 deletions Sources/ConfidenceProvider/Cache/PersistentProviderCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,26 @@ public class PersistentProviderCache: ProviderCache {
private var persistQueue = DispatchQueue(label: "com.confidence.cache.persist")
private static let currentVersion = "0.0.1"

private var _cache: [String: ResolvedValue]
private var cache: [String: ResolvedValue] {
get {
return rwCacheQueue.sync { _cache }
}

set(newCache) {
rwCacheQueue.async(flags: .barrier) { self._cache = newCache }
}
}

private var storage: Storage
private var cache: [String: ResolvedValue]
private var curResolveToken: String?
private var curEvalContextHash: String?
private var persistPublisher = PassthroughSubject<CacheEvent, Never>()
private var cancellable = Set<AnyCancellable>()

init(storage: Storage, cache: [String: ResolvedValue], curResolveToken: String?, curEvalContextHash: String?) {
self.storage = storage
self.cache = cache
self._cache = cache
self.curResolveToken = curResolveToken
self.curEvalContextHash = curEvalContextHash

Expand All @@ -36,37 +46,31 @@ public class PersistentProviderCache: ProviderCache {
}

public func getValue(flag: String, ctx: EvaluationContext) throws -> CacheGetValueResult? {
return try rwCacheQueue.sync {
if let value = self.cache[flag] {
guard let curResolveToken = curResolveToken else {
throw ConfidenceError.noResolveTokenFromCache
}
return .init(
resolvedValue: value, needsUpdate: curEvalContextHash != ctx.hash(), resolveToken: curResolveToken)
} else {
return nil
if let value = self.cache[flag] {
guard let curResolveToken = curResolveToken else {
throw ConfidenceError.noResolveTokenFromCache
}
return .init(
resolvedValue: value, needsUpdate: curEvalContextHash != ctx.hash(), resolveToken: curResolveToken)
} else {
return nil
}
}

public func clearAndSetValues(values: [ResolvedValue], ctx: EvaluationContext, resolveToken: String) throws {
rwCacheQueue.sync(flags: .barrier) {
self.cache = [:]
self.curResolveToken = resolveToken
self.curEvalContextHash = ctx.hash()
values.forEach { value in
self.cache[value.flag] = value
}
self.cache = [:]
self.curResolveToken = resolveToken
self.curEvalContextHash = ctx.hash()
values.forEach { value in
self.cache[value.flag] = value
}
self.persistPublisher.send(.persist)
}

public func clear() throws {
try rwCacheQueue.sync(flags: .barrier) {
try self.storage.clear()
self.cache = [:]
self.curResolveToken = nil
}
try self.storage.clear()
self.cache = [:]
self.curResolveToken = nil
}

public static func from(storage: Storage) -> PersistentProviderCache {
Expand Down Expand Up @@ -105,14 +109,12 @@ extension PersistentProviderCache {
}

func persist() throws {
try rwCacheQueue.sync {
try self.storage.save(
data: StoredCacheData(
version: PersistentProviderCache.currentVersion,
cache: self.cache,
curResolveToken: self.curResolveToken,
curEvalContextHash: self.curEvalContextHash))
}
try self.storage.save(
data: StoredCacheData(
version: PersistentProviderCache.currentVersion,
cache: self.cache,
curResolveToken: self.curResolveToken,
curEvalContextHash: self.curEvalContextHash))
}

public struct ResolvedKey: Hashable, Codable {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Foundation
import OpenFeature

public protocol ConfidenceClient: Resolver {
func resolve(ctx: EvaluationContext) throws -> ResolvesResult
public protocol ConfidenceClient {
func resolve(ctx: EvaluationContext) async throws -> ResolvesResult
}

public struct ResolvedValue: Codable, Equatable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class LocalStorageResolver: Resolver {
guard let getResult = getResult else {
throw OpenFeatureError.flagNotFoundError(key: flag)
}
guard !getResult.needsUpdate else {
guard getResult.needsUpdate == false else {
throw ConfidenceError.cachedValueExpired
}
return .init(resolvedValue: getResult.resolvedValue, resolveToken: getResult.resolveToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,19 @@ public class RemoteConfidenceClient: ConfidenceClient {
self.applyOnResolve = applyOnResolve
}

public func resolve(flags: [String], ctx: EvaluationContext) throws -> ResolvesResult {
// MARK: Resolver

public func resolve(flags: [String], ctx: EvaluationContext) async throws -> ResolvesResult {
let request = ResolveFlagsRequest(
flags: flags.map { "flags/\($0)" },
evaluationContext: try getEvaluationContextStruct(ctx: ctx),
clientSecret: options.credentials.getSecret(),
apply: applyOnResolve)

do {
let result = try self.httpClient.post(
path: ":resolve", data: request, resultType: ResolveFlagsResponse.self)
let result: HttpClientResponse<ResolveFlagsResponse> = try await self.httpClient.post(path: ":resolve",

Check warning on line 34 in Sources/ConfidenceProvider/ConfidenceClient/RemoteConfidenceClient.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Multiline Arguments Violation: Arguments should be either on the same line, or one per line (multiline_arguments)
data: request)

Check warning on line 35 in Sources/ConfidenceProvider/ConfidenceClient/RemoteConfidenceClient.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Indentation Width Violation: Code should be indented using one tab or 4 spaces (indentation_width)

guard result.response.status == .ok else {
throw result.response.mapStatusToError(error: result.decodedError)
}
Expand All @@ -48,17 +51,11 @@ public class RemoteConfidenceClient: ConfidenceClient {
}
}

public func resolve(ctx: EvaluationContext) throws -> ResolvesResult {
return try resolve(flags: [], ctx: ctx)
public func resolve(ctx: EvaluationContext) async throws -> ResolvesResult {
return try await resolve(flags: [], ctx: ctx)
}

public func resolve(flag: String, ctx: EvaluationContext) throws -> ResolveResult {
let resolveResult = try resolve(flags: [flag], ctx: ctx)
guard let resolvedValue = resolveResult.resolvedValues.first else {
throw OpenFeatureError.flagNotFoundError(key: flag)
}
return ResolveResult(resolvedValue: resolvedValue, resolveToken: resolveResult.resolveToken)
}
// MARK: Private

private func convert(resolvedFlag: ResolvedFlag, ctx: EvaluationContext) throws -> ResolvedValue {
guard let responseFlagSchema = resolvedFlag.flagSchema,
Expand Down Expand Up @@ -101,7 +98,7 @@ public class RemoteConfidenceClient: ConfidenceClient {
case .noSegmentMatch, .noTreatmentMatch: return .noMatch
case .match: return .match
case .archived: return .disabled
case .targetngKeyError: return .targetingKeyError
case .targetingKeyError: return .targetingKeyError
}
}
}
Expand Down Expand Up @@ -132,7 +129,7 @@ enum ResolveReason: String, Codable, CaseIterableDefaultsLast {
case noSegmentMatch = "RESOLVE_REASON_NO_SEGMENT_MATCH"
case noTreatmentMatch = "RESOLVE_REASON_NO_TREATMENT_MATCH"
case archived = "RESOLVE_REASON_FLAG_ARCHIVED"
case targetngKeyError = "RESOLVE_REASON_TARGETING_KEY_ERROR"
case targetingKeyError = "RESOLVE_REASON_TARGETING_KEY_ERROR"
case error = "RESOLVE_REASON_ERROR"
case unknown
}
Expand Down
34 changes: 24 additions & 10 deletions Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,23 @@ public class ConfidenceFeatureProvider: FeatureProvider {
self.flagApplier = flagApplier
}

public func initialize(initialContext: OpenFeature.EvaluationContext?) {
public func initialize(initialContext: OpenFeature.EvaluationContext?) async {
guard let initialContext = initialContext else {
return
}
processNewContext(context: initialContext)

await processNewContext(context: initialContext)
}

public func onContextSet(oldContext: OpenFeature.EvaluationContext?, newContext: OpenFeature.EvaluationContext) {
public func onContextSet(
oldContext: OpenFeature.EvaluationContext?,
newContext: OpenFeature.EvaluationContext
) async {
guard oldContext?.hash() != newContext.hash() else {
return
}
processNewContext(context: newContext)

await processNewContext(context: newContext)
}

public func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws
Expand Down Expand Up @@ -112,10 +117,10 @@ public class ConfidenceFeatureProvider: FeatureProvider {
}
}

private func processNewContext(context: OpenFeature.EvaluationContext) {
private func processNewContext(context: OpenFeature.EvaluationContext) async {
// Racy: eval ctx and ctx in cache might differ until the latter is updated, resulting in STALE evaluations
do {
let resolveResult = try client.resolve(ctx: context)
let resolveResult = try await client.resolve(ctx: context)
guard let resolveToken = resolveResult.resolveToken else {
throw ConfidenceError.noResolveTokenFromServer
}
Expand Down Expand Up @@ -157,9 +162,14 @@ public class ConfidenceFeatureProvider: FeatureProvider {
}

do {
let resolverResult = try self.resolver.resolve(flag: path.flag, ctx: ctx)
let resolverResult = try resolver.resolve(flag: path.flag, ctx: ctx)

guard let value = resolverResult.resolvedValue.value else {
return resolveFlagNoValue(defaultValue: defaultValue, resolverResult: resolverResult, ctx: ctx)
return resolveFlagNoValue(
defaultValue: defaultValue,
resolverResult: resolverResult,
ctx: ctx
)
}

let pathValue: Value = try getValue(path: path.path, value: value)
Expand All @@ -170,11 +180,14 @@ public class ConfidenceFeatureProvider: FeatureProvider {
let evaluationResult = ProviderEvaluation(
value: typedValue,
variant: resolverResult.resolvedValue.variant,
reason: Reason.targetingMatch.rawValue)
reason: Reason.targetingMatch.rawValue
)

processResultForApply(
resolverResult: resolverResult,
ctx: ctx,
applyTime: Date.backport.now)
applyTime: Date.backport.now
)
return evaluationResult
} catch ConfidenceError.cachedValueExpired {
return ProviderEvaluation(value: defaultValue, variant: nil, reason: Reason.stale.rawValue)
Expand Down Expand Up @@ -295,6 +308,7 @@ public class ConfidenceFeatureProvider: FeatureProvider {
await flagApplier.apply(flagName: flag, resolveToken: resolveToken)
}
}

private func logApplyError(error: Error) {
switch error {
case ConfidenceError.applyStatusTransitionError, ConfidenceError.cachedValueExpired,
Expand Down
Loading

0 comments on commit 83c5ccb

Please sign in to comment.