Skip to content

Commit

Permalink
feat: Add SafeStateManager
Browse files Browse the repository at this point in the history
Signed-off-by: Fabrizio Demaria <[email protected]>
  • Loading branch information
fabriziodemaria committed Dec 17, 2024
1 parent 7e45173 commit d4598c6
Showing 1 changed file with 72 additions and 27 deletions.
99 changes: 72 additions & 27 deletions Sources/OpenFeature/OpenFeatureAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,9 @@ import Foundation
/// A global singleton which holds base configuration for the OpenFeature library.
/// Configuration here will be shared across all ``Client``s.
public class OpenFeatureAPI {
private var _provider: FeatureProvider? {
get {
providerSubject.value
}
set {
providerSubject.send(newValue)
}
}
private var providerStatus = ProviderStatus.notReady
private var _context: EvaluationContext?
private(set) var hooks: [any Hook] = []
private var providerSubject = CurrentValueSubject<FeatureProvider?, Never>(nil)
private var stateManager = SafeStateManager()
private let eventHandler = EventHandler()
private(set) var hooks: [any Hook] = []

/// The ``OpenFeatureAPI`` singleton
static public let shared = OpenFeatureAPI()
Expand All @@ -29,21 +19,18 @@ public class OpenFeatureAPI {
}

public func setProvider(provider: FeatureProvider, initialContext: EvaluationContext?) {
self._provider = provider
if let context = initialContext {
self._context = context
}
stateManager.setProvider(provider: provider, initialContext: initialContext)
do {
try provider.initialize(initialContext: self._context)
providerStatus = .ready
try provider.initialize(initialContext: initialContext)
stateManager.update(providerStatus: .ready)
eventHandler.send(.ready)
} catch {
switch error {
case OpenFeatureError.providerFatalError:
providerStatus = .fatal
stateManager.update(providerStatus: .fatal)
eventHandler.send(.error(errorCode: .providerFatal))
default:
providerStatus = .error
stateManager.update(providerStatus: .error)
eventHandler.send(.error(message: error.localizedDescription))
}
}
Expand All @@ -54,20 +41,19 @@ public class OpenFeatureAPI {
}

public func clearProvider() {
self._provider = nil
stateManager.clearProvider()
}

public func setEvaluationContext(evaluationContext: EvaluationContext) {
providerStatus = .reconciling
eventHandler.send(.reconciling)
do {
let oldContext = self._context
self._context = evaluationContext
stateManager.update(evaluationContext: evaluationContext, providerStatus: .reconciling)
eventHandler.send(.reconciling)
try getProvider()?.onContextSet(oldContext: oldContext, newContext: evaluationContext)
providerStatus = .ready
stateManager.update(providerStatus: .ready)
eventHandler.send(.contextChanged)
} catch {
providerStatus = .error
stateManager.update(providerStatus: .error)
eventHandler.send(.error(message: error.localizedDescription))
}
}
Expand All @@ -77,7 +63,7 @@ public class OpenFeatureAPI {
}

public func getProviderStatus() -> ProviderStatus {
return providerStatus
return _providerStatus
}

public func getProviderMetadata() -> ProviderMetadata? {
Expand Down Expand Up @@ -116,6 +102,22 @@ public class OpenFeatureAPI {
}
}

/// Accessory getters for properties managed in the state manager
extension OpenFeatureAPI {
private var _provider: FeatureProvider? {
stateManager.providerSubject.value
}
private var _context: EvaluationContext? {
stateManager.evaluationContext
}
private var _providerStatus: ProviderStatus {
stateManager.providerStatus
}
private var providerSubject: CurrentValueSubject<FeatureProvider?, Never> {
stateManager.providerSubject
}
}

extension OpenFeatureAPI {
public func setProviderAndWait(provider: FeatureProvider) async {
await setProviderAndWait(provider: provider, initialContext: nil)
Expand All @@ -137,3 +139,46 @@ extension OpenFeatureAPI {
}
}
}

/// This helper struct maintains the provider, its state and the global evaluation context
/// It is designed to be thread safe on write: context and status are updated atomically, for example.
/// The allowed bulk-changes are also executed in a serial fashion to guarantee thread-safety.
struct SafeStateManager {
private let queue = DispatchQueue(label: "com.providerDescriptor.queue")

private(set) var provider: FeatureProvider?
private(set) var providerSubject = CurrentValueSubject<FeatureProvider?, Never>(nil)
private(set) var evaluationContext: EvaluationContext? = nil
private(set) var providerStatus: ProviderStatus = .notReady

mutating func setProvider(provider: FeatureProvider, initialContext: EvaluationContext? = nil) {
queue.sync {
self.provider = provider
self.providerStatus = .notReady
if let initialContext = initialContext {
self.evaluationContext = initialContext
}
providerSubject.send(provider)
}
}

mutating func update(evaluationContext: EvaluationContext? = nil, providerStatus: ProviderStatus? = nil) {
queue.sync {
if let newContext = evaluationContext {
self.evaluationContext = newContext
}

if let newStatus = providerStatus {
self.providerStatus = newStatus
}
}
}

mutating func clearProvider() {
queue.sync {
self.provider = nil
self.providerSubject.send(nil)
self.providerStatus = .notReady
}
}
}

0 comments on commit d4598c6

Please sign in to comment.