From 9917f7e0bffd2e391ee6a3b500feaf183e215790 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Thu, 21 Nov 2024 11:05:35 +0100 Subject: [PATCH 1/4] refactor!: putContext returns after reconciliation --- Sources/Confidence/Confidence.swift | 45 +++++++++---------- .../Confidence/ConfidenceEventSender.swift | 2 +- .../ConfidenceContextTests.swift | 20 ++++----- Tests/ConfidenceTests/ConfidenceTest.swift | 14 ++---- 4 files changed, 36 insertions(+), 45 deletions(-) diff --git a/Sources/Confidence/Confidence.swift b/Sources/Confidence/Confidence.swift index 64aadbc4..1df8c54b 100644 --- a/Sources/Confidence/Confidence.swift +++ b/Sources/Confidence/Confidence.swift @@ -50,26 +50,6 @@ public class Confidence: ConfidenceEventSender { if let visitorId { putContext(context: ["visitor_id": ConfidenceValue.init(string: visitorId)]) } - - contextChanges().sink { [weak self] context in - guard let self = self else { - return - } - self.currentFetchTask?.cancel() - self.currentFetchTask = Task { - do { - let context = self.getContext() - try await self.fetchAndActivate() - self.contextReconciliatedChanges.send(context.hash()) - } catch { - debugLogger?.logMessage( - message: "\(error)", - isWarning: true - ) - } - } - } - .store(in: &cancellables) } /** @@ -238,22 +218,24 @@ public class Confidence: ConfidenceEventSender { return reconciledCtx } - public func putContext(key: String, value: ConfidenceValue) { - withLock { confidence in + public func putContext(key: String, value: ConfidenceValue) async { + await withLockAsync { confidence in var map = confidence.contextSubject.value map[key] = value confidence.contextSubject.value = map + try! await self.fetchAndActivate() confidence.debugLogger?.logContext(action: "PutContext", context: confidence.contextSubject.value) } } - public func putContext(context: ConfidenceStruct) { - withLock { confidence in + public func putContext(context: ConfidenceStruct) async { + await withLockAsync { confidence in var map = confidence.contextSubject.value for entry in context { map.updateValue(entry.value, forKey: entry.key) } confidence.contextSubject.value = map + try! await self.fetchAndActivate() confidence.debugLogger?.logContext(action: "PutContext", context: confidence.contextSubject.value) } } @@ -304,6 +286,21 @@ public class Confidence: ConfidenceEventSender { callback(self) } } + + private func withLockAsync(callback: @escaping (Confidence) async -> Void) async { + await withCheckedContinuation { continuation in + contextSubjectQueue.async { [weak self] in + guard let self = self else { + continuation.resume() + return + } + Task { + await callback(self) // Await the async closure + continuation.resume() + } + } + } + } } extension Confidence { diff --git a/Sources/Confidence/ConfidenceEventSender.swift b/Sources/Confidence/ConfidenceEventSender.swift index 18e7b829..ed1d962f 100644 --- a/Sources/Confidence/ConfidenceEventSender.swift +++ b/Sources/Confidence/ConfidenceEventSender.swift @@ -21,7 +21,7 @@ public protocol ConfidenceEventSender: ConfidenceContextProvider { /** Adds/override entry to local context data */ - func putContext(key: String, value: ConfidenceValue) + func putContext(key: String, value: ConfidenceValue) async /** Removes entry from localcontext data It hides entries with this key from parents' data (without modifying parents' data) diff --git a/Tests/ConfidenceTests/ConfidenceContextTests.swift b/Tests/ConfidenceTests/ConfidenceContextTests.swift index 155c952d..9e087b39 100644 --- a/Tests/ConfidenceTests/ConfidenceContextTests.swift +++ b/Tests/ConfidenceTests/ConfidenceContextTests.swift @@ -30,7 +30,7 @@ final class ConfidenceContextTests: XCTestCase { XCTAssertEqual(confidenceChild.getContext(), expected) } - func testWithContextUpdateParent() { + func testWithContextUpdateParent() async { let client = RemoteConfidenceResolveClient( options: ConfidenceClientOptions( credentials: ConfidenceClientCredentials.clientSecret(secret: ""), timeoutIntervalForRequest: 10), @@ -51,7 +51,7 @@ final class ConfidenceContextTests: XCTestCase { let confidenceChild: ConfidenceEventSender = confidenceParent.withContext( ["k2": ConfidenceValue(string: "v2")] ) - confidenceParent.putContext( + await confidenceParent.putContext( key: "k3", value: ConfidenceValue(string: "v3")) let expected = [ @@ -62,7 +62,7 @@ final class ConfidenceContextTests: XCTestCase { XCTAssertEqual(confidenceChild.getContext(), expected) } - func testUpdateLocalContext() { + func testUpdateLocalContext() async { let client = RemoteConfidenceResolveClient( options: ConfidenceClientOptions( credentials: ConfidenceClientCredentials.clientSecret(secret: ""), timeoutIntervalForRequest: 10), @@ -80,7 +80,7 @@ final class ConfidenceContextTests: XCTestCase { parent: nil, debugLogger: nil ) - confidence.putContext( + await confidence.putContext( key: "k1", value: ConfidenceValue(string: "v3")) let expected = [ @@ -89,7 +89,7 @@ final class ConfidenceContextTests: XCTestCase { XCTAssertEqual(confidence.getContext(), expected) } - func testUpdateLocalContextWithoutOverride() { + func testUpdateLocalContextWithoutOverride() async { let client = RemoteConfidenceResolveClient( options: ConfidenceClientOptions( credentials: ConfidenceClientCredentials.clientSecret(secret: ""), timeoutIntervalForRequest: 10), @@ -110,7 +110,7 @@ final class ConfidenceContextTests: XCTestCase { let confidenceChild: ConfidenceEventSender = confidenceParent.withContext( ["k2": ConfidenceValue(string: "v2")] ) - confidenceChild.putContext( + await confidenceChild.putContext( key: "k2", value: ConfidenceValue(string: "v4")) let expected = [ @@ -120,7 +120,7 @@ final class ConfidenceContextTests: XCTestCase { XCTAssertEqual(confidenceChild.getContext(), expected) } - func testUpdateParentContextWithOverride() { + func testUpdateParentContextWithOverride() async { let client = RemoteConfidenceResolveClient( options: ConfidenceClientOptions( credentials: ConfidenceClientCredentials.clientSecret(secret: ""), timeoutIntervalForRequest: 10), @@ -141,7 +141,7 @@ final class ConfidenceContextTests: XCTestCase { let confidenceChild: ConfidenceEventSender = confidenceParent.withContext( ["k2": ConfidenceValue(string: "v2")] ) - confidenceParent.putContext( + await confidenceParent.putContext( key: "k2", value: ConfidenceValue(string: "v4")) let expected = [ @@ -235,7 +235,7 @@ final class ConfidenceContextTests: XCTestCase { XCTAssertEqual(confidenceChild.getContext(), expected) } - func testRemoveContextEntryFromParentAndChildThenUpdate() { + func testRemoveContextEntryFromParentAndChildThenUpdate() async { let client = RemoteConfidenceResolveClient( options: ConfidenceClientOptions( credentials: ConfidenceClientCredentials.clientSecret(secret: ""), timeoutIntervalForRequest: 10), @@ -260,7 +260,7 @@ final class ConfidenceContextTests: XCTestCase { ] ) confidenceChild.removeKey(key: "k1") - confidenceChild.putContext(key: "k1", value: ConfidenceValue(string: "v4")) + await confidenceChild.putContext(key: "k1", value: ConfidenceValue(string: "v4")) let expected = [ "k2": ConfidenceValue(string: "v2"), "k1": ConfidenceValue(string: "v4"), diff --git a/Tests/ConfidenceTests/ConfidenceTest.swift b/Tests/ConfidenceTests/ConfidenceTest.swift index 51dc65d5..09adda67 100644 --- a/Tests/ConfidenceTests/ConfidenceTest.swift +++ b/Tests/ConfidenceTests/ConfidenceTest.swift @@ -98,9 +98,9 @@ class ConfidenceTest: XCTestCase { // Initialize allows to start listening for context changes in "confidence" // Let the internal "resolve" finish await fulfillment(of: [resolve1Completed], timeout: 5.0) - confidence.putContext(key: "new", value: ConfidenceValue(string: "value")) + await confidence.putContext(key: "new", value: ConfidenceValue(string: "value")) await fulfillment(of: [resolve2Started], timeout: 5.0) // Ensure resolve 2 starts before 3 - confidence.putContext(key: "new2", value: ConfidenceValue(string: "value2")) + await confidence.putContext(key: "new2", value: ConfidenceValue(string: "value2")) await fulfillment(of: [resolve3Completed], timeout: 5.0) resolve2Continues.fulfill() // Allow second resolve to continue, regardless if cancelled or not await fulfillment(of: [resolve2Cancelled], timeout: 5.0) // Second resolve is cancelled @@ -142,14 +142,8 @@ class ConfidenceTest: XCTestCase { resolveReason: .match) ] - let expectation = expectation(description: "context is synced") - let cancellable = confidence.contextReconciliatedChanges.sink { _ in - expectation.fulfill() - } - confidence.putContext(context: ["targeting_key": .init(string: "user2")]) - await fulfillment(of: [expectation], timeout: 1) - cancellable.cancel() + await confidence.putContext(context: ["targeting_key": .init(string: "user2")]) let evaluation = confidence.getEvaluation( key: "flag.size", defaultValue: 0) @@ -400,7 +394,7 @@ class ConfidenceTest: XCTestCase { .build() try await confidence.fetchAndActivate() - confidence.putContext(context: ["hello": .init(string: "world")]) + await confidence.putContext(context: ["hello": .init(string: "world")]) let evaluation = confidence.getEvaluation( key: "flag.size", defaultValue: 0) From f90a6aedb14a1a9a72881a461f9a658ac3881634 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Fri, 22 Nov 2024 09:09:20 +0100 Subject: [PATCH 2/4] fix: Handle try in putContext --- Sources/Confidence/Confidence.swift | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Sources/Confidence/Confidence.swift b/Sources/Confidence/Confidence.swift index 1df8c54b..507ae8f0 100644 --- a/Sources/Confidence/Confidence.swift +++ b/Sources/Confidence/Confidence.swift @@ -3,6 +3,7 @@ import Foundation import Combine import os +// swiftlint:disable:next type_body_length public class Confidence: ConfidenceEventSender { private let clientSecret: String private var region: ConfidenceRegion @@ -223,8 +224,12 @@ public class Confidence: ConfidenceEventSender { var map = confidence.contextSubject.value map[key] = value confidence.contextSubject.value = map - try! await self.fetchAndActivate() - confidence.debugLogger?.logContext(action: "PutContext", context: confidence.contextSubject.value) + do { + try await self.fetchAndActivate() + confidence.debugLogger?.logContext(action: "PutContext", context: confidence.contextSubject.value) + } catch { + confidence.debugLogger?.logMessage(message: "Error when putting context: \(error)", isWarning: true) + } } } @@ -235,8 +240,12 @@ public class Confidence: ConfidenceEventSender { map.updateValue(entry.value, forKey: entry.key) } confidence.contextSubject.value = map - try! await self.fetchAndActivate() - confidence.debugLogger?.logContext(action: "PutContext", context: confidence.contextSubject.value) + do { + try await self.fetchAndActivate() + confidence.debugLogger?.logContext(action: "PutContext", context: confidence.contextSubject.value) + } catch { + confidence.debugLogger?.logMessage(message: "Error when putting context: \(error)", isWarning: true) + } } } From 0b3621708df5edb27d5fb490dee0e0159f199529 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Fri, 22 Nov 2024 09:29:05 +0100 Subject: [PATCH 3/4] refactor: More changes to context APIs --- Sources/Confidence/Confidence.swift | 63 +++++++++++++++---- .../Confidence/ConfidenceEventSender.swift | 2 +- .../ConfidenceFeatureProvider.swift | 2 +- 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/Sources/Confidence/Confidence.swift b/Sources/Confidence/Confidence.swift index 507ae8f0..659cf3c3 100644 --- a/Sources/Confidence/Confidence.swift +++ b/Sources/Confidence/Confidence.swift @@ -49,7 +49,7 @@ public class Confidence: ConfidenceEventSender { self.remoteFlagResolver = remoteFlagResolver self.debugLogger = debugLogger if let visitorId { - putContext(context: ["visitor_id": ConfidenceValue.init(string: visitorId)]) + putContextLocal(context: ["visitor_id": ConfidenceValue.init(string: visitorId)]) } } @@ -195,10 +195,10 @@ public class Confidence: ConfidenceEventSender { if let contextProducer = producer as? ConfidenceContextProducer { contextProducer.produceContexts() .sink { [weak self] context in - guard let self = self else { - return + Task { [weak self] in + guard let self = self else { return } + await self.putContext(context: context) } - self.putContext(context: context) } .store(in: &cancellables) } @@ -233,6 +233,23 @@ public class Confidence: ConfidenceEventSender { } } + + public func putContextLocal(context: ConfidenceStruct, removeKeys removedKeys: [String] = []) { + withLock { confidence in + var map = confidence.contextSubject.value + for removedKey in removedKeys { + map.removeValue(forKey: removedKey) + } + for entry in context { + map.updateValue(entry.value, forKey: entry.key) + } + confidence.contextSubject.value = map + confidence.debugLogger?.logContext( + action: "PutContext", + context: confidence.contextSubject.value) + } + } + public func putContext(context: ConfidenceStruct) async { await withLockAsync { confidence in var map = confidence.contextSubject.value @@ -242,15 +259,19 @@ public class Confidence: ConfidenceEventSender { confidence.contextSubject.value = map do { try await self.fetchAndActivate() - confidence.debugLogger?.logContext(action: "PutContext", context: confidence.contextSubject.value) + confidence.debugLogger?.logContext( + action: "PutContext & FetchAndActivate", + context: confidence.contextSubject.value) } catch { - confidence.debugLogger?.logMessage(message: "Error when putting context: \(error)", isWarning: true) + confidence.debugLogger?.logMessage( + message: "Error when putting context: \(error)", + isWarning: true) } } } - public func putContext(context: ConfidenceStruct, removeKeys removedKeys: [String] = []) { - withLock { confidence in + public func putContext(context: ConfidenceStruct, removeKeys removedKeys: [String] = []) async { + await withLockAsync { confidence in var map = confidence.contextSubject.value for removedKey in removedKeys { map.removeValue(forKey: removedKey) @@ -259,17 +280,35 @@ public class Confidence: ConfidenceEventSender { map.updateValue(entry.value, forKey: entry.key) } confidence.contextSubject.value = map - confidence.debugLogger?.logContext(action: "PutContext", context: confidence.contextSubject.value) + do { + try await self.fetchAndActivate() + confidence.debugLogger?.logContext( + action: "PutContext & FetchAndActivate", + context: confidence.contextSubject.value) + } catch { + confidence.debugLogger?.logMessage( + message: "Error when putting context: \(error)", + isWarning: true) + } } } - public func removeKey(key: String) { - withLock { confidence in + public func removeKey(key: String) async { + await withLockAsync { confidence in var map = confidence.contextSubject.value map.removeValue(forKey: key) confidence.contextSubject.value = map confidence.removedContextKeys.insert(key) - confidence.debugLogger?.logContext(action: "RemoveContext", context: confidence.contextSubject.value) + do { + try await self.fetchAndActivate() + confidence.debugLogger?.logContext( + action: "RemoveContextKey & FetchAndActivate", + context: confidence.contextSubject.value) + } catch { + confidence.debugLogger?.logMessage( + message: "Error when removing context key: \(error)", + isWarning: true) + } } } diff --git a/Sources/Confidence/ConfidenceEventSender.swift b/Sources/Confidence/ConfidenceEventSender.swift index ed1d962f..ce52762b 100644 --- a/Sources/Confidence/ConfidenceEventSender.swift +++ b/Sources/Confidence/ConfidenceEventSender.swift @@ -26,7 +26,7 @@ public protocol ConfidenceEventSender: ConfidenceContextProvider { Removes entry from localcontext data It hides entries with this key from parents' data (without modifying parents' data) */ - func removeKey(key: String) + func removeKey(key: String) async /** Creates a child event sender instance that maintains access to its parent's data */ diff --git a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift index 538518a3..e055e7ef 100644 --- a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift +++ b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift @@ -86,7 +86,7 @@ public class ConfidenceFeatureProvider: FeatureProvider { } private func updateConfidenceContext(context: EvaluationContext, removedKeys: [String] = []) { - confidence.putContext(context: ConfidenceTypeMapper.from(ctx: context), removeKeys: removedKeys) + confidence.putContextLocal(context: ConfidenceTypeMapper.from(ctx: context), removeKeys: removedKeys) } public func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws From d2233db8cb424614e3548d49f764cc984666d354 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Fri, 22 Nov 2024 09:30:22 +0100 Subject: [PATCH 4/4] refactor!: Rename remove context key protocol --- Sources/Confidence/Confidence.swift | 2 +- Sources/Confidence/ConfidenceEventSender.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Confidence/Confidence.swift b/Sources/Confidence/Confidence.swift index 659cf3c3..782b46d7 100644 --- a/Sources/Confidence/Confidence.swift +++ b/Sources/Confidence/Confidence.swift @@ -293,7 +293,7 @@ public class Confidence: ConfidenceEventSender { } } - public func removeKey(key: String) async { + public func removeContext(key: String) async { await withLockAsync { confidence in var map = confidence.contextSubject.value map.removeValue(forKey: key) diff --git a/Sources/Confidence/ConfidenceEventSender.swift b/Sources/Confidence/ConfidenceEventSender.swift index ce52762b..f595a6e9 100644 --- a/Sources/Confidence/ConfidenceEventSender.swift +++ b/Sources/Confidence/ConfidenceEventSender.swift @@ -26,7 +26,7 @@ public protocol ConfidenceEventSender: ConfidenceContextProvider { Removes entry from localcontext data It hides entries with this key from parents' data (without modifying parents' data) */ - func removeKey(key: String) async + func removeContext(key: String) async /** Creates a child event sender instance that maintains access to its parent's data */