diff --git a/providers/launchdarkly/pkg/provider.go b/providers/launchdarkly/pkg/provider.go index 68dc44f13..bbf9176e7 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,14 +30,16 @@ 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) // 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. @@ -52,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 @@ -372,3 +385,15 @@ 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 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 efe5b424c..8ee0a13aa 100644 --- a/providers/launchdarkly/pkg/provider_test.go +++ b/providers/launchdarkly/pkg/provider_test.go @@ -280,3 +280,39 @@ 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 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") + }) +}