Skip to content

Commit dc576ad

Browse files
committed
Adds ability to pass a context.Context to provider with new public methods.
1 parent 7fd7ab6 commit dc576ad

File tree

6 files changed

+426
-2
lines changed

6 files changed

+426
-2
lines changed

openfeature/context_aware_test.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package openfeature
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
)
8+
9+
// testContextAwareProvider is a test provider that implements ContextAwareStateHandler
10+
type testContextAwareProvider struct {
11+
initDelay time.Duration
12+
}
13+
14+
func (p *testContextAwareProvider) Metadata() Metadata {
15+
return Metadata{Name: "test-context-aware-provider"}
16+
}
17+
18+
// InitWithContext implements ContextAwareStateHandler
19+
func (p *testContextAwareProvider) InitWithContext(ctx context.Context, evalCtx EvaluationContext) error {
20+
select {
21+
case <-time.After(p.initDelay):
22+
return nil
23+
case <-ctx.Done():
24+
return ctx.Err()
25+
}
26+
}
27+
28+
// Init implements StateHandler for backward compatibility
29+
func (p *testContextAwareProvider) Init(evalCtx EvaluationContext) error {
30+
return p.InitWithContext(context.Background(), evalCtx)
31+
}
32+
33+
func (p *testContextAwareProvider) Shutdown() {}
34+
35+
func (p *testContextAwareProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, flatCtx FlattenedContext) BoolResolutionDetail {
36+
return BoolResolutionDetail{
37+
Value: defaultValue,
38+
ProviderResolutionDetail: ProviderResolutionDetail{Reason: DefaultReason},
39+
}
40+
}
41+
42+
func (p *testContextAwareProvider) StringEvaluation(ctx context.Context, flag string, defaultValue string, flatCtx FlattenedContext) StringResolutionDetail {
43+
return StringResolutionDetail{
44+
Value: defaultValue,
45+
ProviderResolutionDetail: ProviderResolutionDetail{Reason: DefaultReason},
46+
}
47+
}
48+
49+
func (p *testContextAwareProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, flatCtx FlattenedContext) FloatResolutionDetail {
50+
return FloatResolutionDetail{
51+
Value: defaultValue,
52+
ProviderResolutionDetail: ProviderResolutionDetail{Reason: DefaultReason},
53+
}
54+
}
55+
56+
func (p *testContextAwareProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, flatCtx FlattenedContext) IntResolutionDetail {
57+
return IntResolutionDetail{
58+
Value: defaultValue,
59+
ProviderResolutionDetail: ProviderResolutionDetail{Reason: DefaultReason},
60+
}
61+
}
62+
63+
func (p *testContextAwareProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx FlattenedContext) InterfaceResolutionDetail {
64+
return InterfaceResolutionDetail{
65+
Value: defaultValue,
66+
ProviderResolutionDetail: ProviderResolutionDetail{Reason: DefaultReason},
67+
}
68+
}
69+
70+
func (p *testContextAwareProvider) Hooks() []Hook {
71+
return []Hook{}
72+
}
73+
74+
func TestContextAwareInitialization(t *testing.T) {
75+
// Save original state
76+
originalAPI := api
77+
originalEventing := eventing
78+
defer func() {
79+
api = originalAPI
80+
eventing = originalEventing
81+
}()
82+
83+
// Create fresh API for isolated testing
84+
exec := newEventExecutor()
85+
testAPI := newEvaluationAPI(exec)
86+
api = testAPI
87+
eventing = exec
88+
89+
t.Run("fast provider succeeds within timeout", func(t *testing.T) {
90+
fastProvider := &testContextAwareProvider{initDelay: 10 * time.Millisecond}
91+
92+
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
93+
defer cancel()
94+
95+
err := SetProviderWithContextAndWait(ctx, fastProvider)
96+
if err != nil {
97+
t.Errorf("Expected fast provider to succeed, got error: %v", err)
98+
}
99+
})
100+
101+
t.Run("slow provider times out", func(t *testing.T) {
102+
slowProvider := &testContextAwareProvider{initDelay: 200 * time.Millisecond}
103+
104+
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
105+
defer cancel()
106+
107+
err := SetProviderWithContextAndWait(ctx, slowProvider)
108+
if err == nil {
109+
t.Error("Expected timeout error but got success")
110+
}
111+
if err != context.DeadlineExceeded {
112+
t.Errorf("Expected context deadline exceeded, got: %v", err)
113+
}
114+
})
115+
116+
t.Run("async initialization returns immediately", func(t *testing.T) {
117+
asyncProvider := &testContextAwareProvider{initDelay: 50 * time.Millisecond}
118+
119+
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
120+
defer cancel()
121+
122+
start := time.Now()
123+
err := SetProviderWithContext(ctx, asyncProvider)
124+
elapsed := time.Since(start)
125+
126+
if err != nil {
127+
t.Errorf("Async setup should not fail: %v", err)
128+
}
129+
if elapsed > 25*time.Millisecond {
130+
t.Errorf("Async setup took too long: %v", elapsed)
131+
}
132+
})
133+
134+
t.Run("named provider with context works", func(t *testing.T) {
135+
namedProvider := &testContextAwareProvider{initDelay: 10 * time.Millisecond}
136+
137+
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
138+
defer cancel()
139+
140+
err := SetNamedProviderWithContextAndWait(ctx, "test-domain", namedProvider)
141+
if err != nil {
142+
t.Errorf("Named provider should succeed: %v", err)
143+
}
144+
})
145+
146+
t.Run("backward compatibility with regular provider", func(t *testing.T) {
147+
legacyProvider := &NoopProvider{}
148+
149+
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
150+
defer cancel()
151+
152+
err := SetProviderWithContextAndWait(ctx, legacyProvider)
153+
if err != nil {
154+
t.Errorf("Legacy provider should work: %v", err)
155+
}
156+
})
157+
}
158+
159+
func TestContextAwareStateHandlerDetection(t *testing.T) {
160+
// Test that the initializerWithContext function correctly detects ContextAwareStateHandler
161+
evalCtx := EvaluationContext{}
162+
163+
t.Run("detects ContextAwareStateHandler", func(t *testing.T) {
164+
provider := &testContextAwareProvider{initDelay: 1 * time.Millisecond}
165+
166+
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
167+
defer cancel()
168+
169+
event, err := initializerWithContext(ctx, provider, evalCtx)
170+
if err != nil {
171+
t.Errorf("Context-aware provider should initialize successfully: %v", err)
172+
}
173+
if event.EventType != ProviderReady {
174+
t.Errorf("Expected ProviderReady event, got: %v", event.EventType)
175+
}
176+
})
177+
178+
t.Run("falls back to regular StateHandler", func(t *testing.T) {
179+
provider := &NoopProvider{}
180+
181+
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
182+
defer cancel()
183+
184+
event, err := initializerWithContext(ctx, provider, evalCtx)
185+
if err != nil {
186+
t.Errorf("Regular provider should initialize successfully: %v", err)
187+
}
188+
if event.EventType != ProviderReady {
189+
t.Errorf("Expected ProviderReady event, got: %v", event.EventType)
190+
}
191+
})
192+
193+
t.Run("handles timeout in context-aware provider", func(t *testing.T) {
194+
provider := &testContextAwareProvider{initDelay: 100 * time.Millisecond}
195+
196+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
197+
defer cancel()
198+
199+
event, err := initializerWithContext(ctx, provider, evalCtx)
200+
if err == nil {
201+
t.Error("Expected timeout error")
202+
}
203+
if err != context.DeadlineExceeded {
204+
t.Errorf("Expected deadline exceeded, got: %v", err)
205+
}
206+
if event.EventType != ProviderError {
207+
t.Errorf("Expected ProviderError event, got: %v", event.EventType)
208+
}
209+
})
210+
}

openfeature/interfaces.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ type evaluationImpl interface {
6767
SetLogger(l logr.Logger)
6868

6969
ForEvaluation(clientName string) (FeatureProvider, []Hook, EvaluationContext)
70+
71+
// Context-aware provider setup methods
72+
SetProviderWithContext(ctx context.Context, provider FeatureProvider) error
73+
SetProviderWithContextAndWait(ctx context.Context, provider FeatureProvider) error
74+
SetNamedProviderWithContext(ctx context.Context, clientName string, provider FeatureProvider, async bool) error
7075
}
7176

7277
// eventingImpl is an internal reference interface extending IEventing

openfeature/interfaces_mock.go

Lines changed: 42 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

openfeature/openfeature.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package openfeature
22

3-
import "github.com/go-logr/logr"
3+
import (
4+
"context"
5+
"github.com/go-logr/logr"
6+
)
47

58
// api is the global evaluationImpl implementation. This is a singleton and there can only be one instance.
69
var (
@@ -47,6 +50,21 @@ func SetProviderAndWait(provider FeatureProvider) error {
4750
return api.SetProviderAndWait(provider)
4851
}
4952

53+
// SetProviderWithContext sets the default [FeatureProvider] with context-aware initialization.
54+
// If the provider implements ContextAwareStateHandler, InitWithContext will be called with the provided context.
55+
// Provider initialization is asynchronous and status can be checked from provider status.
56+
// Returns an error immediately if provider is nil, or if context is cancelled during setup.
57+
func SetProviderWithContext(ctx context.Context, provider FeatureProvider) error {
58+
return api.SetProviderWithContext(ctx, provider)
59+
}
60+
61+
// SetProviderWithContextAndWait sets the default [FeatureProvider] with context-aware initialization and waits for completion.
62+
// If the provider implements ContextAwareStateHandler, InitWithContext will be called with the provided context.
63+
// Returns an error if initialization causes an error, or if context is cancelled during initialization.
64+
func SetProviderWithContextAndWait(ctx context.Context, provider FeatureProvider) error {
65+
return api.SetProviderWithContextAndWait(ctx, provider)
66+
}
67+
5068
// ProviderMetadata returns the default [FeatureProvider] metadata
5169
func ProviderMetadata() Metadata {
5270
return api.GetProviderMetadata()
@@ -64,6 +82,21 @@ func SetNamedProviderAndWait(domain string, provider FeatureProvider) error {
6482
return api.SetNamedProvider(domain, provider, false)
6583
}
6684

85+
// SetNamedProviderWithContext sets a [FeatureProvider] mapped to the given [Client] domain with context-aware initialization.
86+
// If the provider implements ContextAwareStateHandler, InitWithContext will be called with the provided context.
87+
// Provider initialization is asynchronous and status can be checked from provider status.
88+
// Returns an error immediately if provider is nil, or if context is cancelled during setup.
89+
func SetNamedProviderWithContext(ctx context.Context, domain string, provider FeatureProvider) error {
90+
return api.SetNamedProviderWithContext(ctx, domain, provider, true)
91+
}
92+
93+
// SetNamedProviderWithContextAndWait sets a provider mapped to the given [Client] domain with context-aware initialization and waits for completion.
94+
// If the provider implements ContextAwareStateHandler, InitWithContext will be called with the provided context.
95+
// Returns an error if initialization causes an error, or if context is cancelled during initialization.
96+
func SetNamedProviderWithContextAndWait(ctx context.Context, domain string, provider FeatureProvider) error {
97+
return api.SetNamedProviderWithContext(ctx, domain, provider, false)
98+
}
99+
67100
// NamedProviderMetadata returns the named provider's Metadata
68101
func NamedProviderMetadata(name string) Metadata {
69102
return api.GetNamedProviderMetadata(name)

0 commit comments

Comments
 (0)