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 5152a85
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 167 deletions.
288 changes: 139 additions & 149 deletions Sources/Confidence/Confidence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,25 @@ import Foundation
import Combine
import os

// swiftlint:disable:next type_body_length
public class Confidence: ConfidenceEventSender {
// User configurations
private let clientSecret: String
private var region: ConfidenceRegion
private let parent: ConfidenceContextProvider?
private let region: ConfidenceRegion
private let debugLogger: DebugLogger?

// Resources related to managing context and flags
private let parentContextProvider: ConfidenceContextProvider?
private let contextManager: ContextManager
private var cache = FlagResolution.EMPTY

// Core components managing internal SDK functionality
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 var cache = FlagResolution.EMPTY
private var storage: Storage

// Synchronization and task management resources
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 +45,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 +76,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 All @@ -111,6 +106,13 @@ public class Confidence: ConfidenceEventSender {
try storage.save(data: resolution)
}

/**
Returns true if any flag is found in storage.
*/
public func isStorageEmpty() -> Bool {
return storage.isEmpty()
}

/**
Get evaluation data for a specific flag. Evaluation data includes the variant's name and reason/error information.
- Parameter key:expects dot-notation to retrieve a specific entry in the flag's value, e.g. "flagname.myentry"
Expand Down Expand Up @@ -146,143 +148,47 @@ public class Confidence: ConfidenceEventSender {
return getEvaluation(key: key, defaultValue: defaultValue).value
}

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

public func track(eventName: String, data: ConfidenceStruct) throws {
try eventSenderEngine.emit(
eventName: eventName,
data: data,
context: getContext()
)
}

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)
}
}
}


/**
Adds/override entry to local context data. Does not trigger fetchAndActivate after the context change.
*/
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
debugLogger?.logContext(
action: "PutContextLocal",
context: context)
}
let newContext = contextManager.updateContext(withValues: context, removedKeys: removedKeys)
debugLogger?.logContext(
action: "PutContextLocal",
context: newContext)
}

public func putContext(context: ConfidenceStruct) async {
await withSemaphoreAsync { [weak self] in
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)
} catch {
debugLogger?.logMessage(
message: "Error when putting context: \(error)",
isWarning: true)
}
}
}

public func putContext(context: ConfidenceStruct, removeKeys removedKeys: [String] = []) async {
await withSemaphoreAsync { [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
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 +202,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 +230,51 @@ 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 track(eventName: String, data: ConfidenceStruct) throws {
try eventSenderEngine.emit(
eventName: eventName,
data: data,
context: getContext()
)
}

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 +285,56 @@ 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
}
}
}

extension Confidence {
public class Builder {
// Must be configured or configured automatically
Expand Down
Loading

0 comments on commit 5152a85

Please sign in to comment.