diff --git a/openfeature/event_executor.go b/openfeature/event_executor.go index 9673fc1a..fc4285e0 100644 --- a/openfeature/event_executor.go +++ b/openfeature/event_executor.go @@ -150,9 +150,16 @@ func (e *eventExecutor) GetClientRegistry(client string) scopedCallback { // emitOnRegistration fulfils the spec requirement to fire events if the // event type and the state of the associated provider are compatible. func (e *eventExecutor) emitOnRegistration(domain string, providerReference providerReference, eventType EventType, callback EventCallback) { - state, ok := e.loadState(domain) - if !ok { - return + var state State + // state-managing providers own their state; read directly + if smp, ok := providerReference.featureProvider.(StateManagingProvider); ok { + state = smp.State() + } else { + var ok bool + state, ok = e.loadState(domain) + if !ok { + return + } } var message string @@ -185,6 +192,20 @@ func (e *eventExecutor) loadState(domain string) (State, bool) { } func (e *eventExecutor) State(domain string) State { + e.mu.Lock() + defer e.mu.Unlock() + + // find the provider reference for this domain + ref, ok := e.namedProviderReference[domain] + if !ok { + ref = e.defaultProviderReference + } + + // state-managing providers own their state; read directly + if smp, ok := ref.featureProvider.(StateManagingProvider); ok { + return smp.State() + } + state, _ := e.loadState(domain) return state } @@ -297,6 +318,8 @@ func (e *eventExecutor) triggerEvent(event Event, handler FeatureProvider) { e.mu.Lock() defer e.mu.Unlock() + _, delegateManagesState := handler.(StateManagingProvider) + // first run API handlers for _, c := range e.apiRegistry[event.EventType] { e.executeHandler(*c, event) @@ -308,7 +331,10 @@ func (e *eventExecutor) triggerEvent(event Event, handler FeatureProvider) { continue } - e.states.Store(domain, stateFromEvent(event)) + // state-managing providers own their state; skip SDK-side writes + if !delegateManagesState { + e.states.Store(domain, stateFromEvent(event)) + } for _, c := range e.scopedRegistry[domain].callbacks[event.EventType] { e.executeHandler(*c, event) } @@ -319,7 +345,10 @@ func (e *eventExecutor) triggerEvent(event Event, handler FeatureProvider) { } // handling the default provider - e.states.Store(defaultDomain, stateFromEvent(event)) + // state-managing providers own their state; skip SDK-side writes + if !delegateManagesState { + e.states.Store(defaultDomain, stateFromEvent(event)) + } // invoke default provider bound (no provider associated) handlers by filtering for domain, registry := range e.scopedRegistry { if _, ok := e.namedProviderReference[domain]; ok { diff --git a/openfeature/openfeature_api.go b/openfeature/openfeature_api.go index 39a12ff8..bfd27ea3 100644 --- a/openfeature/openfeature_api.go +++ b/openfeature/openfeature_api.go @@ -117,10 +117,16 @@ func (api *evaluationAPI) setNamedProviderWithContext(ctx context.Context, clien func (api *evaluationAPI) initNew(ctx context.Context, clientName string, newProvider FeatureProvider) <-chan error { errCh := make(chan error, 1) + _, delegateManagesState := newProvider.(StateManagingProvider) + // Initialize new provider async. The caller may wait on the channel. go func(executor *eventExecutor, evalCtx EvaluationContext, ctx context.Context, provider FeatureProvider, clientName string) { event, err := initializerWithContext(ctx, provider, evalCtx) - executor.triggerEvent(event, provider) + + // State-managing providers emit their own events; skip SDK-side emission. + if !delegateManagesState { + executor.triggerEvent(event, provider) + } if err != nil { if clientName == "" { diff --git a/openfeature/provider.go b/openfeature/provider.go index c97b6675..e38c7628 100644 --- a/openfeature/provider.go +++ b/openfeature/provider.go @@ -105,6 +105,28 @@ func (s *NoopStateHandler) Shutdown() { // NOOP } +// StateManagingProvider is a provider that manages its own state. The SDK reads +// state from the provider rather than maintaining shadow state. Implementations +// MUST ensure that State() is safe for concurrent access and that state +// transitions and associated event emissions are atomic from the perspective of +// external observers. +// +// Legacy providers that do not implement this interface continue to have their +// state managed by the SDK (deprecated behavior; to be removed in the next +// major version). +type StateManagingProvider interface { + FeatureProvider + StateHandler + EventHandler + + // State returns the current provider state. Must reflect NotReadyState + // before Init is called and after Shutdown completes. Must reflect + // ReadyState if Init returns nil. + // + // This method must be safe for concurrent access. + State() State +} + // Eventing // EventHandler is the eventing contract enforced for FeatureProvider diff --git a/openfeature/reference.go b/openfeature/reference.go index 18c7da6b..ada4fc2f 100644 --- a/openfeature/reference.go +++ b/openfeature/reference.go @@ -6,10 +6,12 @@ import ( // newProviderRef creates a new providerReference instance that wraps around a FeatureProvider implementation func newProviderRef(provider FeatureProvider) providerReference { + _, managesState := provider.(StateManagingProvider) return providerReference{ - featureProvider: provider, - kind: reflect.TypeOf(provider).Kind(), - shutdownSemaphore: make(chan any, 1), + featureProvider: provider, + kind: reflect.TypeOf(provider).Kind(), + shutdownSemaphore: make(chan any, 1), + delegateManagesState: managesState, } } @@ -19,6 +21,8 @@ type providerReference struct { featureProvider FeatureProvider kind reflect.Kind shutdownSemaphore chan any + // delegateManagesState is true when the provider implements StateManagingProvider + delegateManagesState bool } func (pr providerReference) equals(other providerReference) bool {