From d75ef1c3ff77db7aaba85ac0323064ed9f965a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gra=C3=B1a?= Date: Tue, 21 Oct 2025 15:47:42 -0300 Subject: [PATCH 1/3] feat(launchdarkly): Implement openfeature.StateHandler interface for clean shutdowns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Graña --- providers/launchdarkly/pkg/provider.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/providers/launchdarkly/pkg/provider.go b/providers/launchdarkly/pkg/provider.go index 68dc44f13..c8fb114fa 100644 --- a/providers/launchdarkly/pkg/provider.go +++ b/providers/launchdarkly/pkg/provider.go @@ -19,6 +19,9 @@ var errKeyMissing = errors.New("key and targetingKey attributes are missing, at // Scream at compile time if Provider does not implement FeatureProvider var _ openfeature.FeatureProvider = (*Provider)(nil) +// Scream at compile time if Provider does not implement StateHandler +var _ openfeature.StateHandler = (*Provider)(nil) + // LDClient is the narrowed local interface for the parts of the // `*ld.LDClient` LaunchDarkly client used by the provider. type LDClient interface { @@ -27,6 +30,7 @@ type LDClient interface { Float64VariationDetail(key string, context ldcontext.Context, defaultVal float64) (float64, ldreason.EvaluationDetail, error) StringVariationDetail(key string, context ldcontext.Context, defaultVal string) (string, ldreason.EvaluationDetail, error) JSONVariationDetail(key string, context ldcontext.Context, defaultVal ldvalue.Value) (ldvalue.Value, ldreason.EvaluationDetail, error) + Close() error } type Option func(*options) @@ -372,3 +376,13 @@ func (p *Provider) ObjectEvaluation(ctx context.Context, flagKey string, default func (p *Provider) Hooks() []openfeature.Hook { return []openfeature.Hook{} } + +func (p *Provider) Init(evaluationContext openfeature.EvaluationContext) error { + return nil +} + +func (p *Provider) Shutdown() { + if err := p.client.Close(); err != nil { + p.l.Error("error during LaunchDarkly client shutdown: %s", err) + } +} From ad095425e500d26f8f2a881e77602a0b27089893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gra=C3=B1a?= Date: Tue, 21 Oct 2025 17:34:31 -0300 Subject: [PATCH 2/3] add test case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Graña --- providers/launchdarkly/pkg/provider_test.go | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/providers/launchdarkly/pkg/provider_test.go b/providers/launchdarkly/pkg/provider_test.go index efe5b424c..5a86fc84e 100644 --- a/providers/launchdarkly/pkg/provider_test.go +++ b/providers/launchdarkly/pkg/provider_test.go @@ -280,3 +280,28 @@ func TestContextCancellation(t *testing.T) { _, err = client.ObjectValue(ctx, "rate_limit_config", nil, evalCtx) assert.Equals(t, errors.New("GENERAL: context canceled"), errors.Unwrap(err)) } + +// mockLDClient can be a struct that implements the LDClient interface for testing. +type mockLDClient struct { + ld.LDClient // Embedding the real client can be useful for mocking only specific methods + closeCalled bool + closeErr error +} + +func (c *mockLDClient) Close() error { + c.closeCalled = true + return c.closeErr +} + +func TestShutdown(t *testing.T) { + t.Run("should call client close on shutdown", func(t *testing.T) { + mockClient := &mockLDClient{} + provider := NewProvider(mockClient) + + err := openfeature.SetProvider(provider) + assert.Ok(t, err) + + openfeature.Shutdown() + assert.Cond(t, mockClient.closeCalled, "expected client.Close() to be called") + }) +} From c37bac0ce220c0c00685dc10438653727c78abd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gra=C3=B1a?= Date: Fri, 24 Oct 2025 17:53:15 -0300 Subject: [PATCH 3/3] Add WithCloseOnShutdown option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Graña --- providers/launchdarkly/pkg/provider.go | 19 +++++++++++++++---- providers/launchdarkly/pkg/provider_test.go | 13 ++++++++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/providers/launchdarkly/pkg/provider.go b/providers/launchdarkly/pkg/provider.go index c8fb114fa..bbf9176e7 100644 --- a/providers/launchdarkly/pkg/provider.go +++ b/providers/launchdarkly/pkg/provider.go @@ -37,8 +37,9 @@ type Option func(*options) // options contains all the optional arguments supported by Provider. type options struct { - kindAttr string - l Logger + kindAttr string + l Logger + closeOnShutdown bool } // WithLogger sets a logger implementation. By default a noop logger is used. @@ -56,6 +57,14 @@ func WithKindAttr(name string) Option { } } +// WithCloseOnShutdown sets whether the LaunchDarkly client should be closed +// when the provider is shut down. By default, this is false. +func WithCloseOnShutdown(close bool) Option { + return func(o *options) { + o.closeOnShutdown = close + } +} + // Provider implements the FeatureProvider interface for LaunchDarkly. type Provider struct { options @@ -382,7 +391,9 @@ func (p *Provider) Init(evaluationContext openfeature.EvaluationContext) error { } func (p *Provider) Shutdown() { - if err := p.client.Close(); err != nil { - p.l.Error("error during LaunchDarkly client shutdown: %s", err) + if p.closeOnShutdown { + if err := p.client.Close(); err != nil { + p.l.Error("error during LaunchDarkly client shutdown: %s", err) + } } } diff --git a/providers/launchdarkly/pkg/provider_test.go b/providers/launchdarkly/pkg/provider_test.go index 5a86fc84e..8ee0a13aa 100644 --- a/providers/launchdarkly/pkg/provider_test.go +++ b/providers/launchdarkly/pkg/provider_test.go @@ -294,13 +294,24 @@ func (c *mockLDClient) Close() error { } func TestShutdown(t *testing.T) { - t.Run("should call client close on shutdown", func(t *testing.T) { + t.Run("should not call client close on shutdown", func(t *testing.T) { mockClient := &mockLDClient{} provider := NewProvider(mockClient) err := openfeature.SetProvider(provider) assert.Ok(t, err) + openfeature.Shutdown() + assert.Cond(t, !mockClient.closeCalled, "expected client.Close() to be called") + }) + + t.Run("should call client close on shutdown", func(t *testing.T) { + mockClient := &mockLDClient{} + provider := NewProvider(mockClient, WithCloseOnShutdown(true)) + + err := openfeature.SetProvider(provider) + assert.Ok(t, err) + openfeature.Shutdown() assert.Cond(t, mockClient.closeCalled, "expected client.Close() to be called") })