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

refactor!: Context APIs changes and documentation/onboarding #180

Merged
merged 11 commits into from
Dec 10, 2024
33 changes: 22 additions & 11 deletions Sources/Confidence/Confidence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ public class Confidence: ConfidenceEventSender {
// Synchronization and task management resources
private var cancellables = Set<AnyCancellable>()
private let cacheQueue = DispatchQueue(label: "com.confidence.queue.cache")
private var currentFetchTask: Task<(), Never>?
private var currentFetchTask: Task<(), Never>? {
didSet {
if let oldTask = oldValue {
oldTask.cancel()
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

}

// Internal for testing
internal let remoteFlagResolver: ConfidenceResolveClient
Expand Down Expand Up @@ -155,7 +161,6 @@ public class Confidence: ConfidenceEventSender {
}

public func putContextAndWait(key: String, value: ConfidenceValue) async {
self.currentFetchTask?.cancel()
self.currentFetchTask = Task {
let newContext = contextManager.updateContext(withValues: [key: value], removedKeys: [])
do {
Expand All @@ -169,7 +174,6 @@ public class Confidence: ConfidenceEventSender {
}

public func putContextAndWait(context: ConfidenceStruct, removedKeys: [String] = []) async {
self.currentFetchTask?.cancel()
self.currentFetchTask = Task {
let newContext = contextManager.updateContext(withValues: context, removedKeys: removedKeys)
do {
Expand All @@ -183,7 +187,6 @@ public class Confidence: ConfidenceEventSender {
}

public func putContextAndWait(context: ConfidenceStruct) async {
self.currentFetchTask?.cancel()
self.currentFetchTask = Task {
let newContext = contextManager.updateContext(withValues: context, removedKeys: [])
do {
Expand All @@ -201,7 +204,6 @@ public class Confidence: ConfidenceEventSender {
}

public func removeContextAndWait(key: String) async {
self.currentFetchTask?.cancel()
self.currentFetchTask = Task {
let newContext = contextManager.updateContext(withValues: [:], removedKeys: [key])
do {
Expand Down Expand Up @@ -229,35 +231,30 @@ public class Confidence: ConfidenceEventSender {
}

public func putContext(key: String, value: ConfidenceValue) {
self.currentFetchTask?.cancel()
self.currentFetchTask = Task {
await putContextAndWait(key: key, value: value)
}
}

public func putContext(context: ConfidenceStruct) {
self.currentFetchTask?.cancel()
self.currentFetchTask = Task {
await putContextAndWait(context: context)
}
}

public func putContext(context: ConfidenceStruct, removeKeys removedKeys: [String] = []) {
self.currentFetchTask?.cancel()
self.currentFetchTask = Task {
await putContextAndWait(context: context, removedKeys: removedKeys)
}
}

public func removeContext(key: String) {
self.currentFetchTask?.cancel()
self.currentFetchTask = Task {
await removeContextAndWait(key: key)
}
}

public func putContext(context: ConfidenceStruct, removedKeys: [String]) {
self.currentFetchTask?.cancel()
self.currentFetchTask = Task {
let newContext = contextManager.updateContext(withValues: context, removedKeys: removedKeys)
do {
Expand All @@ -277,8 +274,22 @@ public class Confidence: ConfidenceEventSender {
Ensures all the already-started context changes prior to this function have been reconciliated
*/
public func awaitReconciliation() async {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest awaitReady() instead...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep the same name used in Kotlin, i.e. awaitReconciliation

I'd be happy to create a pair of "renaming PRs" for both repos after this though, if we want to reach proper alignment (this would make sense especially now that the APIs between platforms are much more similar!)

if let task = self.currentFetchTask {
while let task = self.currentFetchTask {
// If current task is cancelled, return
if task.isCancelled {
return
}
// Wait for result of current task
await task.value
// If current task gets cancelled, check again if a new task was set
if task.isCancelled {
continue
}
// If current task finished successfully
// and the set task has not changed, we are done waiting
if self.currentFetchTask == task {
return
}
}
}

Expand Down
60 changes: 59 additions & 1 deletion Tests/ConfidenceTests/ConfidenceTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ class ConfidenceTest: XCTestCase {
XCTAssertEqual(2, client.resolveContexts.count)
XCTAssertEqual(confidence.getContext(), client.resolveContexts[1])
}

// swiftlint:enable function_body_length

func testRefresh() async throws {
class FakeClient: ConfidenceResolveClient {
var resolveStats: Int = 0
Expand Down Expand Up @@ -490,6 +490,64 @@ class ConfidenceTest: XCTestCase {
XCTAssertEqual(flagApplier.applyCallCount, 1)
}

func testAwaitReconciliationFailingTask() async throws {
class FakeClient: XCTestCase, ConfidenceResolveClient {
var resolveStats: Int = 0
var resolvedValues: [ResolvedValue] = []

func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult {
self.resolveStats += 1
if resolveStats == 1 {
// Delay to ensure the second putContext cancels this Task
try await Task.sleep(nanoseconds: 2000000)
XCTFail("This line shouldn't be reached as task is expected to be cancelled")
return .init(resolvedValues: [], resolveToken: "token")
} else {
if ctx["hello"] == .init(string: "world") {
return .init(resolvedValues: resolvedValues, resolveToken: "token")
} else {
return .init(resolvedValues: [], resolveToken: "token")
}
}
}
}

let client = FakeClient()
client.resolvedValues = [
ResolvedValue(
variant: "control",
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)
.withStorage(storage: storage)
.build()

confidence.putContext(context: ["hello": .init(string: "not-world")])
Task {
confidence.putContext(context: ["hello": .init(string: "world")])
}
await confidence.awaitReconciliation()
let evaluation = confidence.getEvaluation(
key: "flag.size",
defaultValue: 0
)

XCTAssertEqual(client.resolveStats, 2)
XCTAssertEqual(evaluation.value, 3)
XCTAssertNil(evaluation.errorCode)
XCTAssertNil(evaluation.errorMessage)
XCTAssertEqual(evaluation.reason, .match)
XCTAssertEqual(evaluation.variant, "control")
await fulfillment(of: [flagApplier.applyExpectation], timeout: 1)
XCTAssertEqual(flagApplier.applyCallCount, 1)
}

func testResolveBooleanFlag() async throws {
class FakeClient: ConfidenceResolveClient {
var resolveStats: Int = 0
Expand Down
Loading