Skip to content

Commit ef3e3de

Browse files
committed
Add OpenFeature 1.17.0 context timeout support
- Add ability to pass context into openfeature provider to support cancellation - Update to OpenFeature v1.17.0 from pinned pre-release version - Implement ContextAwareStateHandler with InitWithContext and ShutdownWithContext - Add comprehensive timeout tests for SetProviderWithContextAndWait and ShutdownWithContext - Simplify InitWithContext implementation using channels instead of sync.Cond - Replace complex mutex lock/unlock cycles with clean channel-based signaling - All tests passing with improved error handling for test scenarios
1 parent 53b84e7 commit ef3e3de

File tree

4 files changed

+229
-43
lines changed

4 files changed

+229
-43
lines changed

go.mod

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ require (
2323
github.com/json-iterator/go v1.1.12
2424
github.com/klauspost/compress v1.18.0
2525
github.com/minio/simdjson-go v0.4.5
26-
github.com/open-feature/go-sdk v1.16.0
26+
github.com/open-feature/go-sdk v1.17.0
2727
github.com/puzpuzpuz/xsync/v3 v3.5.1
2828
github.com/quasilyte/go-ruleguard/dsl v0.3.22
2929
github.com/richardartoul/molecule v1.0.1-0.20240531184615-7ca0df43c0b3
@@ -37,11 +37,11 @@ require (
3737
go.uber.org/goleak v1.3.0
3838
go.yaml.in/yaml/v3 v3.0.4
3939
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0
40-
golang.org/x/mod v0.27.0
40+
golang.org/x/mod v0.28.0
4141
golang.org/x/sync v0.17.0
42-
golang.org/x/sys v0.35.0
42+
golang.org/x/sys v0.36.0
4343
golang.org/x/time v0.12.0
44-
golang.org/x/tools v0.36.0
44+
golang.org/x/tools v0.37.0
4545
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028
4646
google.golang.org/grpc v1.75.0
4747
google.golang.org/protobuf v1.36.7
@@ -96,8 +96,8 @@ require (
9696
go.uber.org/mock v0.6.0 // indirect
9797
go.uber.org/multierr v1.11.0 // indirect
9898
go.uber.org/zap v1.27.0 // indirect
99-
golang.org/x/net v0.43.0 // indirect
100-
golang.org/x/text v0.29.0 // indirect
99+
golang.org/x/net v0.44.0 // indirect
100+
golang.org/x/text v0.30.0 // indirect
101101
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
102102
gopkg.in/ini.v1 v1.67.0 // indirect
103103
gopkg.in/yaml.v3 v3.0.1 // indirect

go.sum

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
109109
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
110110
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
111111
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
112-
github.com/open-feature/go-sdk v1.16.0 h1:5NCHYv5slvNBIZhYXAzAufo0OI59OACZ5tczVqSE+Tg=
113-
github.com/open-feature/go-sdk v1.16.0/go.mod h1:EIF40QcoYT1VbQkMPy2ZJH4kvZeY+qGUXAorzSWgKSo=
112+
github.com/open-feature/go-sdk v1.17.0 h1:/OUBBw5d9D61JaNZZxb2Nnr5/EJrEpjtKCTY3rspJQk=
113+
github.com/open-feature/go-sdk v1.17.0/go.mod h1:lPxPSu1UnZ4E3dCxZi5gV3et2ACi8O8P+zsTGVsDZUw=
114114
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.133.0 h1:iPei+89a2EK4LuN4HeIRzZNE6XxCyrKfBKG3BkK/ViU=
115115
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.133.0/go.mod h1:asV77TgnGfc7A+a9jggdsnlLlW5dnJT8RroVuf5slko=
116116
github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.133.0 h1:4ca2pM3+xDMB9H3UnhjAiNg7EpIydZ7HdohOexU8xb8=
@@ -255,15 +255,15 @@ golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5N
255255
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
256256
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
257257
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
258-
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
259-
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
258+
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
259+
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
260260
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
261261
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
262262
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
263263
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
264264
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
265-
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
266-
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
265+
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
266+
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
267267
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
268268
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
269269
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -283,22 +283,22 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
283283
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
284284
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
285285
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
286-
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
287-
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
286+
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
287+
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
288288
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
289289
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
290290
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
291-
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
292-
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
291+
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
292+
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
293293
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
294294
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
295295
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
296296
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
297297
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
298298
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
299299
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
300-
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
301-
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
300+
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
301+
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
302302
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
303303
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
304304
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

openfeature/provider.go

Lines changed: 91 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ import (
1010
"errors"
1111
"fmt"
1212
"sync"
13+
"time"
1314

1415
"github.com/DataDog/dd-trace-go/v2/internal"
1516
"github.com/DataDog/dd-trace-go/v2/internal/log"
1617
"github.com/open-feature/go-sdk/openfeature"
1718
)
1819

1920
var _ openfeature.FeatureProvider = (*DatadogProvider)(nil)
21+
var _ openfeature.ContextAwareStateHandler = (*DatadogProvider)(nil)
2022

2123
// Sentinel errors for error classification
2224
var (
@@ -35,7 +37,8 @@ type DatadogProvider struct {
3537
configuration *universalFlagsConfiguration
3638
metadata openfeature.Metadata
3739

38-
configChange sync.Cond
40+
configReady chan struct{} // Closed when first config is loaded
41+
configOnce sync.Once // Ensure channel is closed only once
3942
}
4043

4144
type ProviderConfig struct {
@@ -61,22 +64,29 @@ func NewDatadogProvider(ProviderConfig) (openfeature.FeatureProvider, error) {
6164
}
6265

6366
func newDatadogProvider() *DatadogProvider {
64-
p := &DatadogProvider{
67+
return &DatadogProvider{
6568
metadata: openfeature.Metadata{
6669
Name: "Datadog Remote Config Provider",
6770
},
71+
configReady: make(chan struct{}),
6872
}
69-
p.configChange.L = &p.mu
70-
return p
7173
}
7274

7375
// updateConfiguration updates the provider's flag configuration.
7476
// This is called by the Remote Config callback when new configuration is received.
7577
func (p *DatadogProvider) updateConfiguration(config *universalFlagsConfiguration) {
7678
p.mu.Lock()
7779
defer p.mu.Unlock()
80+
81+
isFirstConfig := p.configuration == nil
7882
p.configuration = config
79-
p.configChange.Broadcast()
83+
84+
// Close the channel on first configuration to signal readiness
85+
if isFirstConfig {
86+
p.configOnce.Do(func() {
87+
close(p.configReady)
88+
})
89+
}
8090
}
8191

8292
// getConfiguration returns the current configuration (for testing purposes).
@@ -93,30 +103,73 @@ func (p *DatadogProvider) Metadata() openfeature.Metadata {
93103

94104
// Init initializes the provider. For the Datadog provider,
95105
// this is waiting for the first configuration to be loaded.
96-
func (p *DatadogProvider) Init(openfeature.EvaluationContext) error {
97-
p.mu.Lock()
98-
defer p.mu.Unlock()
99-
for p.configuration == nil {
100-
p.configChange.Wait()
101-
}
106+
func (p *DatadogProvider) Init(evaluationContext openfeature.EvaluationContext) error {
107+
// Use a background context with a reasonable timeout for backward compatibility
108+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
109+
defer cancel()
110+
return p.InitWithContext(ctx, evaluationContext)
111+
}
102112

103-
return nil
113+
// InitWithContext initializes the provider with context support.
114+
// This method respects context cancellation and timeouts, allowing users
115+
// to cancel the initialization process if needed.
116+
func (p *DatadogProvider) InitWithContext(ctx context.Context, _ openfeature.EvaluationContext) error {
117+
// Quick check if already configured
118+
p.mu.RLock()
119+
if p.configuration != nil {
120+
p.mu.RUnlock()
121+
return nil
122+
}
123+
readyChan := p.configReady
124+
p.mu.RUnlock()
125+
126+
// Wait for either context cancellation or configuration
127+
select {
128+
case <-ctx.Done():
129+
return ctx.Err()
130+
case <-readyChan:
131+
return nil
132+
}
104133
}
105134

106135
// Shutdown shuts down the provider and stops Remote Config updates.
107136
func (p *DatadogProvider) Shutdown() {
108-
// Best effort to stop Remote Config - ignore error as we're shutting down anyway
109-
_ = stopRemoteConfig()
137+
// Use a background context with a reasonable timeout for backward compatibility
138+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
139+
defer cancel()
140+
_ = p.ShutdownWithContext(ctx)
141+
}
142+
143+
// ShutdownWithContext shuts down the provider with context support.
144+
// This method respects context cancellation and timeouts, allowing users
145+
// to control how long the shutdown process should take.
146+
func (p *DatadogProvider) ShutdownWithContext(ctx context.Context) error {
147+
// Create a channel to signal completion
148+
done := make(chan error, 1)
149+
150+
go func() {
151+
// Perform the shutdown operations
152+
err := stopRemoteConfig()
153+
done <- err
154+
}()
155+
156+
// Wait for completion or context cancellation
157+
select {
158+
case <-ctx.Done():
159+
return ctx.Err()
160+
case err := <-done:
161+
return err
162+
}
110163
}
111164

112165
// BooleanEvaluation evaluates a boolean feature flag.
113166
func (p *DatadogProvider) BooleanEvaluation(
114-
_ context.Context,
167+
ctx context.Context,
115168
flagKey string,
116169
defaultValue bool,
117170
flatCtx openfeature.FlattenedContext,
118171
) openfeature.BoolResolutionDetail {
119-
result := p.evaluate(flagKey, defaultValue, flatCtx)
172+
result := p.evaluate(ctx, flagKey, defaultValue, flatCtx)
120173

121174
// Convert result to boolean
122175
boolValue, ok := result.Value.(bool)
@@ -138,12 +191,12 @@ func (p *DatadogProvider) BooleanEvaluation(
138191

139192
// StringEvaluation evaluates a string feature flag.
140193
func (p *DatadogProvider) StringEvaluation(
141-
_ context.Context,
194+
ctx context.Context,
142195
flagKey string,
143196
defaultValue string,
144197
flatCtx openfeature.FlattenedContext,
145198
) openfeature.StringResolutionDetail {
146-
result := p.evaluate(flagKey, defaultValue, flatCtx)
199+
result := p.evaluate(ctx, flagKey, defaultValue, flatCtx)
147200

148201
// Convert result to string
149202
strValue, ok := result.Value.(string)
@@ -165,12 +218,12 @@ func (p *DatadogProvider) StringEvaluation(
165218

166219
// FloatEvaluation evaluates a numeric (float) feature flag.
167220
func (p *DatadogProvider) FloatEvaluation(
168-
_ context.Context,
221+
ctx context.Context,
169222
flagKey string,
170223
defaultValue float64,
171224
flatCtx openfeature.FlattenedContext,
172225
) openfeature.FloatResolutionDetail {
173-
result := p.evaluate(flagKey, defaultValue, flatCtx)
226+
result := p.evaluate(ctx, flagKey, defaultValue, flatCtx)
174227

175228
// Convert result to float64
176229
var floatValue float64
@@ -211,12 +264,12 @@ func (p *DatadogProvider) FloatEvaluation(
211264

212265
// IntEvaluation evaluates an integer feature flag.
213266
func (p *DatadogProvider) IntEvaluation(
214-
_ context.Context,
267+
ctx context.Context,
215268
flagKey string,
216269
defaultValue int64,
217270
flatCtx openfeature.FlattenedContext,
218271
) openfeature.IntResolutionDetail {
219-
result := p.evaluate(flagKey, defaultValue, flatCtx)
272+
result := p.evaluate(ctx, flagKey, defaultValue, flatCtx)
220273

221274
// Convert result to int64
222275
var intValue int64
@@ -264,12 +317,12 @@ func (p *DatadogProvider) IntEvaluation(
264317

265318
// ObjectEvaluation evaluates a structured (JSON) feature flag.
266319
func (p *DatadogProvider) ObjectEvaluation(
267-
_ context.Context,
320+
ctx context.Context,
268321
flagKey string,
269322
defaultValue any,
270323
flatCtx openfeature.FlattenedContext,
271324
) openfeature.InterfaceResolutionDetail {
272-
result := p.evaluate(flagKey, defaultValue, flatCtx)
325+
result := p.evaluate(ctx, flagKey, defaultValue, flatCtx)
273326

274327
return openfeature.InterfaceResolutionDetail{
275328
Value: result.Value,
@@ -289,6 +342,7 @@ func (p *DatadogProvider) Hooks() []openfeature.Hook {
289342

290343
// evaluate is the core evaluation method that all type-specific methods use.
291344
func (p *DatadogProvider) evaluate(
345+
ctx context.Context,
292346
flagKey string,
293347
defaultValue any,
294348
flatCtx openfeature.FlattenedContext,
@@ -297,6 +351,18 @@ func (p *DatadogProvider) evaluate(
297351
defer func() {
298352
log.Debug("openfeature: evaluated flag %q: value=%v, reason=%s, error=%v", flagKey, res.Value, res.Reason, res.Error)
299353
}()
354+
355+
// Check if context was cancelled before starting evaluation
356+
select {
357+
case <-ctx.Done():
358+
return evaluationResult{
359+
Value: defaultValue,
360+
Reason: openfeature.ErrorReason,
361+
Error: ctx.Err(),
362+
}
363+
default:
364+
}
365+
300366
config := p.getConfiguration()
301367

302368
// Check if configuration is loaded
@@ -318,7 +384,7 @@ func (p *DatadogProvider) evaluate(
318384
}
319385
}
320386

321-
// Evaluate the flag
387+
// Evaluate the flag (pass context for potential future use in evaluateFlag)
322388
return evaluateFlag(flag, defaultValue, flatCtx)
323389
}
324390

0 commit comments

Comments
 (0)