Skip to content

Commit

Permalink
fix: Make Confidence.cache thread-safe (#167)
Browse files Browse the repository at this point in the history
* fix: Make Confidence.cache thread-safe

* fix: different queues for different accesses

* fix: Confidence.cache thread-safe reads
  • Loading branch information
fabriziodemaria authored Nov 5, 2024
1 parent 448fb93 commit df2c37f
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 11 deletions.
40 changes: 29 additions & 11 deletions Sources/Confidence/Confidence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ public class Confidence: ConfidenceEventSender {
private let eventSenderEngine: EventSenderEngine
private let contextSubject = CurrentValueSubject<ConfidenceStruct, Never>([:])
private var removedContextKeys: Set<String> = Set()
private let confidenceQueue = DispatchQueue(label: "com.confidence.queue")
private let contextSubjectQueue = DispatchQueue(label: "com.confidence.queue.contextsubject")
private let cacheQueue = DispatchQueue(label: "com.confidence.queue.cache")
private let flagApplier: FlagApplier
private var cache = FlagResolution.EMPTY
private var storage: Storage
Expand Down Expand Up @@ -70,14 +71,20 @@ public class Confidence: ConfidenceEventSender {
}
.store(in: &cancellables)
}

/**
Activating the cache means that the flag data on disk is loaded into memory, so consumers can access flag values.
Errors can be thrown if something goes wrong access data on disk.
*/
public func activate() throws {
let savedFlags = try storage.load(defaultValue: FlagResolution.EMPTY)
self.cache = savedFlags
debugLogger?.logFlags(action: "Activate", flag: "")
try cacheQueue.sync { [weak self] in
guard let self = self else {
return
}
let savedFlags = try storage.load(defaultValue: FlagResolution.EMPTY)
cache = savedFlags
debugLogger?.logFlags(action: "Activate", flag: "")
}
}

/**
Expand Down Expand Up @@ -133,12 +140,23 @@ public class Confidence: ConfidenceEventSender {
- Parameter defaultValue: returned in case of errors or in case of the variant's rule indicating to use the default value.
*/
public func getEvaluation<T>(key: String, defaultValue: T) -> Evaluation<T> {
self.cache.evaluate(
flagName: key,
defaultValue: defaultValue,
context: getContext(),
flagApplier: flagApplier
)
cacheQueue.sync { [weak self] in
guard let self = self else {
return Evaluation(
value: defaultValue,
variant: nil,
reason: .error,
errorCode: .providerNotReady,
errorMessage: "Confidence instance deallocated before end of evaluation"
)
}
return self.cache.evaluate(
flagName: key,
defaultValue: defaultValue,
context: getContext(),
flagApplier: flagApplier
)
}
}

/**
Expand Down Expand Up @@ -278,7 +296,7 @@ public class Confidence: ConfidenceEventSender {
}

private func withLock(callback: @escaping (Confidence) -> Void) {
confidenceQueue.sync { [weak self] in
contextSubjectQueue.sync { [weak self] in
guard let self = self else {
return
}
Expand Down
23 changes: 23 additions & 0 deletions Tests/ConfidenceTests/ConfidenceTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,29 @@ class ConfidenceTest: XCTestCase {
XCTAssertEqual(error as? ConfidenceError, ConfidenceError.invalidContextInMessage)
}
}

func testConcurrentActivate() async {
for _ in 1...100 {
Task {
await concurrentActivate()
}
}
}

private func concurrentActivate() async {
let confidence = Confidence.Builder(clientSecret: "test")
.build()

await withTaskGroup(of: Void.self) { group in
for _ in 0..<10000 {
group.addTask {
// no need to handle errors
// race condition crashes will surface regardless
try? confidence.activate()
}
}
}
}
}

final class DispatchQueueFake: DispatchQueueType {
Expand Down

0 comments on commit df2c37f

Please sign in to comment.