Skip to content

Commit

Permalink
feat: Safe access to context via ContextManager
Browse files Browse the repository at this point in the history
  • Loading branch information
fabriziodemaria committed Dec 4, 2024
1 parent 6fad8bc commit 7a450f3
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 114 deletions.
239 changes: 126 additions & 113 deletions Sources/Confidence/Confidence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@ import os
// swiftlint:disable:next type_body_length
public class Confidence: ConfidenceEventSender {

Check failure on line 7 in Sources/Confidence/Confidence.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Superfluous Disable Command Violation: SwiftLint rule 'type_body_length' did not trigger a violation in the disabled region; remove the disable command (superfluous_disable_command)
private let clientSecret: String
private var region: ConfidenceRegion
private let parent: ConfidenceContextProvider?
private let region: ConfidenceRegion
private let debugLogger: DebugLogger?
private let eventSenderEngine: EventSenderEngine
private var context: ConfidenceStruct = [:]
private var removedContextKeys: Set<String> = Set()
private let cacheQueue = DispatchQueue(label: "com.confidence.queue.cache")
private let storage: Storage
private let flagApplier: FlagApplier

private let parentContextProvider: ConfidenceContextProvider?
private let contextManager: ContextManager
private var cache = FlagResolution.EMPTY
private var storage: Storage

private var cancellables = Set<AnyCancellable>()
private let debugLogger: DebugLogger?
private let cacheQueue = DispatchQueue(label: "com.confidence.queue.cache")
private let semaphore = DispatchSemaphore(value: 1)

// Internal for testing
Expand All @@ -40,9 +41,8 @@ public class Confidence: ConfidenceEventSender {
self.clientSecret = clientSecret
self.region = region
self.storage = storage
self.context = context
self.parent = parent
self.storage = storage
self.contextManager = ContextManager(initialContext: context)
self.parentContextProvider = parent
self.flagApplier = flagApplier
self.remoteFlagResolver = remoteFlagResolver
self.debugLogger = debugLogger
Expand Down Expand Up @@ -72,35 +72,26 @@ public class Confidence: ConfidenceEventSender {
Fetching is best-effort, so no error is propagated. Errors can still be thrown if something goes wrong access data on disk.
*/
public func fetchAndActivate() async throws {
do {
try await internalFetch()
} catch {
debugLogger?.logMessage(
message: "\(error)",
isWarning: true
)
}
await asyncFetch()
try activate()
}

/**
Fetch latest flag evaluations and store them on disk. Note that "activate" must be called for this data to be
made available in the app session.
*/
public func asyncFetch() {
Task {
do {
try await internalFetch()
} catch {
debugLogger?.logMessage(
message: "\(error )",
isWarning: true
)
}
public func asyncFetch() async {
do {
try await internalFetch()
} catch {
debugLogger?.logMessage(
message: "\(error )",
isWarning: true
)
}
}

func internalFetch() async throws {
private func internalFetch() async throws {
let context = getContext()
let resolvedFlags = try await remoteFlagResolver.resolve(ctx: context)
let resolution = FlagResolution(
Expand Down Expand Up @@ -146,7 +137,7 @@ public class Confidence: ConfidenceEventSender {
return getEvaluation(key: key, defaultValue: defaultValue).value
}

func isStorageEmpty() -> Bool {
public func isStorageEmpty() -> Bool {
return storage.isEmpty()
}

Expand All @@ -158,87 +149,35 @@ public class Confidence: ConfidenceEventSender {
)
}

public func track(producer: ConfidenceProducer) {
if let eventProducer = producer as? ConfidenceEventProducer {
eventProducer.produceEvents()
.sink { [weak self] event in
guard let self = self else {
return
}
do {
try self.track(eventName: event.name, data: event.data)
if event.shouldFlush {
eventSenderEngine.flush()
}
} catch {
Logger(subsystem: "com.confidence", category: "track").warning(
"Error from EventProducer, failed to track event: \(event.name)")
}
}
.store(in: &cancellables)
}

if let contextProducer = producer as? ConfidenceContextProducer {
contextProducer.produceContexts()
.sink { [weak self] context in
Task { [weak self] in
guard let self = self else { return }
await self.putContext(context: context)
}
}
.store(in: &cancellables)
}
}

public func flush() {
eventSenderEngine.flush()
}

public func getContext() -> ConfidenceStruct {
let parentContext = parent?.getContext() ?? [:]
var reconciledCtx = parentContext.filter {
!removedContextKeys.contains($0.key)
}
self.context.forEach { entry in
reconciledCtx.updateValue(entry.value, forKey: entry.key)
}
return reconciledCtx
let parentContext = parentContextProvider?.getContext() ?? [:]
return contextManager.getContext(parentContext: parentContext)
}

public func putContext(key: String, value: ConfidenceValue) async {
await withSemaphoreAsync { [weak self] in
guard let self = self else {
return
}
var map = self.context
map[key] = value
context = map
let newContext = contextManager.updateContext(withValues: [key: value], removedKeys: [])
do {
try await self.fetchAndActivate()
debugLogger?.logContext(action: "PutContext", context: context)
debugLogger?.logContext(action: "PutContext", context: newContext)
} catch {
debugLogger?.logMessage(message: "Error when putting context: \(error)", isWarning: true)
}
}
}


public func putContextLocal(context: ConfidenceStruct, removeKeys removedKeys: [String] = []) {
withSemaphore { [weak self] in
guard let self = self else {
return
}
var map = self.context
for removedKey in removedKeys {
map.removeValue(forKey: removedKey)
}
for entry in context {
map.updateValue(entry.value, forKey: entry.key)
}
self.context = map
let newContext = contextManager.updateContext(withValues: context, removedKeys: removedKeys)
debugLogger?.logContext(
action: "PutContextLocal",
context: context)
context: newContext)
}
}

Expand All @@ -247,16 +186,12 @@ public class Confidence: ConfidenceEventSender {
guard let self = self else {
return
}
var map = self.context
for entry in context {
map.updateValue(entry.value, forKey: entry.key)
}
self.context = map
let newContext = contextManager.updateContext(withValues: context, removedKeys: [])
do {
try await fetchAndActivate()
debugLogger?.logContext(
action: "PutContext - Done with FetchAndActivate",
context: context)
action: "PutContext",
context: newContext)
} catch {
debugLogger?.logMessage(
message: "Error when putting context: \(error)",
Expand All @@ -270,19 +205,12 @@ public class Confidence: ConfidenceEventSender {
guard let self = self else {
return
}
var map = self.context
for removedKey in removedKeys {
map.removeValue(forKey: removedKey)
}
for entry in context {
map.updateValue(entry.value, forKey: entry.key)
}
self.context = map
let newContext = contextManager.updateContext(withValues: context, removedKeys: removedKeys)
do {
try await self.fetchAndActivate()
debugLogger?.logContext(
action: "PutContext - Done with FetchAndActivate",
context: context)
action: "PutContext",
context: newContext)
} catch {
debugLogger?.logMessage(
message: "Error when putting context: \(error)",
Expand All @@ -296,15 +224,12 @@ public class Confidence: ConfidenceEventSender {
guard let self = self else {
return
}
var map = self.context
map.removeValue(forKey: key)
context = map
removedContextKeys.insert(key)
let newContext = contextManager.updateContext(withValues: [:], removedKeys: [key])
do {
try await self.fetchAndActivate()
debugLogger?.logContext(
action: "RemoveContext - Done with FetchAndActivate",
context: context)
action: "RemoveContext",
context: newContext)
} catch {
debugLogger?.logMessage(
message: "Error when removing context key: \(error)",
Expand All @@ -327,7 +252,43 @@ public class Confidence: ConfidenceEventSender {
)
}

func withSemaphoreAsync(callback: @escaping () async -> Void) async {
public func track(producer: ConfidenceProducer) {
if let eventProducer = producer as? ConfidenceEventProducer {
eventProducer.produceEvents()
.sink { [weak self] event in
guard let self = self else {
return
}
do {
try self.track(eventName: event.name, data: event.data)
if event.shouldFlush {
eventSenderEngine.flush()
}
} catch {
Logger(subsystem: "com.confidence", category: "track").warning(
"Error from EventProducer, failed to track event: \(event.name)")
}
}
.store(in: &cancellables)
}

if let contextProducer = producer as? ConfidenceContextProducer {
contextProducer.produceContexts()
.sink { [weak self] context in
Task { [weak self] in
guard let self = self else { return }
await self.putContext(context: context)
}
}
.store(in: &cancellables)
}
}

public func flush() {
eventSenderEngine.flush()
}

private func withSemaphoreAsync(callback: @escaping () async -> Void) async {
await withCheckedContinuation { continuation in
DispatchQueue.global().async {
self.semaphore.wait()
Expand All @@ -338,13 +299,65 @@ public class Confidence: ConfidenceEventSender {
semaphore.signal()
}

func withSemaphore(callback: @escaping () -> Void) {
private func withSemaphore(callback: @escaping () -> Void) {
self.semaphore.wait()
callback()
semaphore.signal()
}
}

private class ContextManager {
private var context: ConfidenceStruct = [:]
private var removedContextKeys: Set<String> = Set()
private let contextQueue = DispatchQueue(label: "com.confidence.queue.context")

public init(initialContext: ConfidenceStruct) {
context = initialContext
}

func updateContext(withValues: ConfidenceStruct, removedKeys: [String]) -> ConfidenceStruct {
contextQueue.sync { [weak self] in
guard let self = self else {
return [:]
}
var map = self.context
for removedKey in removedKeys {
map.removeValue(forKey: removedKey)
removedContextKeys.insert(removedKey)
}
for entry in withValues {
map.updateValue(entry.value, forKey: entry.key)
}
self.context = map
return self.context
}
}

func getContext(parentContext: ConfidenceStruct) -> ConfidenceStruct {
contextQueue.sync { [weak self] in
guard let self = self else {
return [:]
}
var reconciledCtx = parentContext.filter {
!self.removedContextKeys.contains($0.key)
}
context.forEach { entry in
reconciledCtx.updateValue(entry.value, forKey: entry.key)
}
return reconciledCtx
}
}

func getLocalContext() -> ConfidenceStruct {
contextQueue.sync { [weak self] in
guard let self = self else {
return [:]
}
return context
}
}
}

extension Confidence {
public class Builder {
// Must be configured or configured automatically
Expand Down
4 changes: 3 additions & 1 deletion Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ public class ConfidenceFeatureProvider: FeatureProvider {
if initializationStrategy == .activateAndFetchAsync {
try confidence.activate()
eventHandler.send(.ready)
confidence.asyncFetch()
Task {
await confidence.asyncFetch()
}
} else {
Task {
do {
Expand Down

0 comments on commit 7a450f3

Please sign in to comment.