Skip to content

Commit a87f422

Browse files
committed
feat: provider wrapper to support state race fix
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
1 parent 4ae2f18 commit a87f422

4 files changed

Lines changed: 70 additions & 9 deletions

File tree

openfeature/event_executor.go

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,16 @@ func (e *eventExecutor) GetClientRegistry(client string) scopedCallback {
150150
// emitOnRegistration fulfils the spec requirement to fire events if the
151151
// event type and the state of the associated provider are compatible.
152152
func (e *eventExecutor) emitOnRegistration(domain string, providerReference providerReference, eventType EventType, callback EventCallback) {
153-
state, ok := e.loadState(domain)
154-
if !ok {
155-
return
153+
var state State
154+
// state-managing providers own their state; read directly
155+
if smp, ok := providerReference.featureProvider.(StateManagingProvider); ok {
156+
state = smp.State()
157+
} else {
158+
var ok bool
159+
state, ok = e.loadState(domain)
160+
if !ok {
161+
return
162+
}
156163
}
157164

158165
var message string
@@ -185,6 +192,20 @@ func (e *eventExecutor) loadState(domain string) (State, bool) {
185192
}
186193

187194
func (e *eventExecutor) State(domain string) State {
195+
e.mu.Lock()
196+
defer e.mu.Unlock()
197+
198+
// find the provider reference for this domain
199+
ref, ok := e.namedProviderReference[domain]
200+
if !ok {
201+
ref = e.defaultProviderReference
202+
}
203+
204+
// state-managing providers own their state; read directly
205+
if smp, ok := ref.featureProvider.(StateManagingProvider); ok {
206+
return smp.State()
207+
}
208+
188209
state, _ := e.loadState(domain)
189210
return state
190211
}
@@ -297,6 +318,8 @@ func (e *eventExecutor) triggerEvent(event Event, handler FeatureProvider) {
297318
e.mu.Lock()
298319
defer e.mu.Unlock()
299320

321+
_, delegateManagesState := handler.(StateManagingProvider)
322+
300323
// first run API handlers
301324
for _, c := range e.apiRegistry[event.EventType] {
302325
e.executeHandler(*c, event)
@@ -308,7 +331,10 @@ func (e *eventExecutor) triggerEvent(event Event, handler FeatureProvider) {
308331
continue
309332
}
310333

311-
e.states.Store(domain, stateFromEvent(event))
334+
// state-managing providers own their state; skip SDK-side writes
335+
if !delegateManagesState {
336+
e.states.Store(domain, stateFromEvent(event))
337+
}
312338
for _, c := range e.scopedRegistry[domain].callbacks[event.EventType] {
313339
e.executeHandler(*c, event)
314340
}
@@ -319,7 +345,10 @@ func (e *eventExecutor) triggerEvent(event Event, handler FeatureProvider) {
319345
}
320346

321347
// handling the default provider
322-
e.states.Store(defaultDomain, stateFromEvent(event))
348+
// state-managing providers own their state; skip SDK-side writes
349+
if !delegateManagesState {
350+
e.states.Store(defaultDomain, stateFromEvent(event))
351+
}
323352
// invoke default provider bound (no provider associated) handlers by filtering
324353
for domain, registry := range e.scopedRegistry {
325354
if _, ok := e.namedProviderReference[domain]; ok {

openfeature/openfeature_api.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,16 @@ func (api *evaluationAPI) setNamedProviderWithContext(ctx context.Context, clien
117117
func (api *evaluationAPI) initNew(ctx context.Context, clientName string, newProvider FeatureProvider) <-chan error {
118118
errCh := make(chan error, 1)
119119

120+
_, delegateManagesState := newProvider.(StateManagingProvider)
121+
120122
// Initialize new provider async. The caller may wait on the channel.
121123
go func(executor *eventExecutor, evalCtx EvaluationContext, ctx context.Context, provider FeatureProvider, clientName string) {
122124
event, err := initializerWithContext(ctx, provider, evalCtx)
123-
executor.triggerEvent(event, provider)
125+
126+
// State-managing providers emit their own events; skip SDK-side emission.
127+
if !delegateManagesState {
128+
executor.triggerEvent(event, provider)
129+
}
124130

125131
if err != nil {
126132
if clientName == "" {

openfeature/provider.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,28 @@ func (s *NoopStateHandler) Shutdown() {
105105
// NOOP
106106
}
107107

108+
// StateManagingProvider is a provider that manages its own state. The SDK reads
109+
// state from the provider rather than maintaining shadow state. Implementations
110+
// MUST ensure that State() is safe for concurrent access and that state
111+
// transitions and associated event emissions are atomic from the perspective of
112+
// external observers.
113+
//
114+
// Legacy providers that do not implement this interface continue to have their
115+
// state managed by the SDK (deprecated behavior; to be removed in the next
116+
// major version).
117+
type StateManagingProvider interface {
118+
FeatureProvider
119+
StateHandler
120+
EventHandler
121+
122+
// State returns the current provider state. Must reflect NotReadyState
123+
// before Init is called and after Shutdown completes. Must reflect
124+
// ReadyState if Init returns nil.
125+
//
126+
// This method must be safe for concurrent access.
127+
State() State
128+
}
129+
108130
// Eventing
109131

110132
// EventHandler is the eventing contract enforced for FeatureProvider

openfeature/reference.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import (
66

77
// newProviderRef creates a new providerReference instance that wraps around a FeatureProvider implementation
88
func newProviderRef(provider FeatureProvider) providerReference {
9+
_, managesState := provider.(StateManagingProvider)
910
return providerReference{
10-
featureProvider: provider,
11-
kind: reflect.TypeOf(provider).Kind(),
12-
shutdownSemaphore: make(chan any, 1),
11+
featureProvider: provider,
12+
kind: reflect.TypeOf(provider).Kind(),
13+
shutdownSemaphore: make(chan any, 1),
14+
delegateManagesState: managesState,
1315
}
1416
}
1517

@@ -19,6 +21,8 @@ type providerReference struct {
1921
featureProvider FeatureProvider
2022
kind reflect.Kind
2123
shutdownSemaphore chan any
24+
// delegateManagesState is true when the provider implements StateManagingProvider
25+
delegateManagesState bool
2226
}
2327

2428
func (pr providerReference) equals(other providerReference) bool {

0 commit comments

Comments
 (0)