-
Notifications
You must be signed in to change notification settings - Fork 66
feat(flagd): add new context enrichment approach for in-process provider #730
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
fa7a42d to
30a0194
Compare
Summary of ChangesThis pull request enhances the in-process flagd provider by introducing a flexible context enrichment mechanism. It allows users to define custom functions to modify or extend the evaluation context dynamically, ensuring that flag evaluations can incorporate additional, derived, or external data. This is achieved through a new "ContextEnricher" configuration option and its integration via a "SyncContextHook", providing a powerful way to customize flag evaluation behavior. Highlights
Changelog
Activity
|
|
/gemini review |
Signed-off-by: Simon Schrottner <[email protected]> diff --git c/providers/flagd/pkg/provider.go i/providers/flagd/pkg/provider.go index 2b0084c..cf52f01 100644 --- c/providers/flagd/pkg/provider.go +++ i/providers/flagd/pkg/provider.go @@ -22,8 +22,8 @@ type Provider struct { service IService status of.State mtx parallel.RWMutex - - eventStream chan of.Event + hooks []of.Hook + eventStream chan of.Event } func NewProvider(opts ...ProviderOption) (*Provider, error) { @@ -38,6 +38,7 @@ func NewProvider(opts ...ProviderOption) (*Provider, error) { eventStream: make(chan of.Event), providerConfiguration: providerConfiguration, status: of.NotReadyState, + hooks: []of.Hook{}, } cacheService := cache.NewCacheService( @@ -60,7 +61,7 @@ func NewProvider(opts ...ProviderOption) (*Provider, error) { provider.providerConfiguration.log, provider.providerConfiguration.EventStreamConnectionMaxAttempts) } else if provider.providerConfiguration.Resolver == inProcess { - service = process.NewInProcessService(process.Configuration{ + inprocess_service := process.NewInProcessService(process.Configuration{ Host: provider.providerConfiguration.Host, Port: provider.providerConfiguration.Port, ProviderID: provider.providerConfiguration.ProviderId, @@ -73,6 +74,13 @@ func NewProvider(opts ...ProviderOption) (*Provider, error) { CustomSyncProviderUri: provider.providerConfiguration.CustomSyncProviderUri, GrpcDialOptionsOverride: provider.providerConfiguration.GrpcDialOptionsOverride, }) + provider.hooks = append(provider.hooks, NewSyncContextHook( + func() *of.EvaluationContext { + evaluationContext := of.NewTargetlessEvaluationContext(inprocess_service.ContextValues) + return &evaluationContext + })) + service = inprocess_service + } else { service = process.NewInProcessService(process.Configuration{ OfflineFlagSource: provider.providerConfiguration.OfflineFlagSourcePath, @@ -145,7 +153,7 @@ func (p *Provider) EventChannel() <-chan of.Event { // Hooks flagd provider does not have any hooks, returns empty slice func (p *Provider) Hooks() []of.Hook { - return []of.Hook{} + return p.hooks } // Metadata returns value of Metadata (name of current service, exposed to openfeature sdk) diff --git c/providers/flagd/pkg/service/in_process/service.go i/providers/flagd/pkg/service/in_process/service.go index 468b1c6..4f1c2ee 100644 --- c/providers/flagd/pkg/service/in_process/service.go +++ i/providers/flagd/pkg/service/in_process/service.go @@ -3,7 +3,6 @@ package process import ( "context" "fmt" - "regexp" parallel "sync" @@ -33,6 +32,7 @@ type InProcess struct { sync sync.ISync syncEnd context.CancelFunc wg parallel.WaitGroup + ContextValues map[string]any } type Configuration struct { @@ -113,6 +113,10 @@ func (i *InProcess) Init() error { case data := <-syncChan: // re-syncs are ignored as we only support single flag sync source changes, _, err := i.evaluator.SetState(data) + if data.SyncContext != nil { + i.ContextValues = data.SyncContext.AsMap() + } + if err != nil { i.events <- of.Event{ ProviderName: "flagd", EventType: of.ProviderError, diff --git c/providers/flagd/pkg/service/in_process/service_grpc_test.go i/providers/flagd/pkg/service/in_process/service_grpc_test.go index a1e7c1c..c271161 100644 --- c/providers/flagd/pkg/service/in_process/service_grpc_test.go +++ i/providers/flagd/pkg/service/in_process/service_grpc_test.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/open-feature/go-sdk/openfeature" "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/structpb" "log" "net" "testing" @@ -38,11 +39,20 @@ func TestInProcessProviderEvaluation(t *testing.T) { t.Fatal(err) } + m := make(map[string]any) + + m["context"] = "set" + syncContext, err := structpb.NewStruct(m) + if err != nil { + t.Fatal(err) + } + bufServ := &bufferedServer{ listener: listen, mockResponses: []*v1.SyncFlagsResponse{ { FlagConfiguration: flagRsp, + SyncContext: syncContext, }, }, fetchAllFlagsResponse: nil, @@ -112,6 +122,10 @@ func TestInProcessProviderEvaluation(t *testing.T) { if scope != detail.FlagMetadata["scope"] { t.Fatalf("Wrong scope value. Expected %s, but got %s", scope, detail.FlagMetadata["scope"]) } + + if len(inProcessService.ContextValues) == 0 { + t.Fatal("Expected context_values to be present, but got none") + } } // custom name resolver @@ -138,7 +152,7 @@ func TestInProcessProviderEvaluationEnvoy(t *testing.T) { } inProcessService := NewInProcessService(Configuration{ - TargetUri: "envoy://localhost:9211/foo.service", + TargetUri: "envoy://localhost:9211/foo.service", Selector: scope, TLSEnabled: false, }) @@ -201,7 +215,6 @@ func TestInProcessProviderEvaluationEnvoy(t *testing.T) { } } - // bufferedServer - a mock grpc service backed by buffered connection type bufferedServer struct { listener net.Listener diff --git c/providers/flagd/pkg/sync_context_hook.go i/providers/flagd/pkg/sync_context_hook.go new file mode 100644 index 0000000..ebb6590 --- /dev/null +++ i/providers/flagd/pkg/sync_context_hook.go @@ -0,0 +1,21 @@ +package flagd + +import ( + "context" + "github.com/open-feature/go-sdk/openfeature" +) + +type Supplier func() *openfeature.EvaluationContext + +type SyncContextHook struct { + openfeature.UnimplementedHook + contextSupplier Supplier +} + +func NewSyncContextHook(contextSupplier Supplier) SyncContextHook { + return SyncContextHook{contextSupplier: contextSupplier} +} + +func (hook SyncContextHook) Before(ctx context.Context, hookContext openfeature.HookContext, hookHints openfeature.HookHints) (*openfeature.EvaluationContext, error) { + return hook.contextSupplier(), nil +}
Signed-off-by: Simon Schrottner <[email protected]>
Signed-off-by: Simon Schrottner <[email protected]>
2106a06 to
b6c9664
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces a context enrichment feature for the in-process provider, allowing static context from a sync source to be merged into the evaluation context. The implementation correctly uses OpenFeature hooks. However, I've found a potential race condition in the in-process service that needs to be addressed. I also have a few suggestions to improve code clarity and stick to Go idioms.
Signed-off-by: Simon Schrottner <[email protected]>
81e621d to
0286738
Compare
Signed-off-by: Simon Schrottner <[email protected]>
ab45615 to
38fd36c
Compare
| // re-syncs are ignored as we only support single flag sync source | ||
| changes, _, err := i.evaluator.SetState(data) | ||
| if data.SyncContext != nil { | ||
| i.mtx.Lock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to add a defer i.mtx.Unlock() here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where do you see the benefit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly for consistency. But you are right, it's just the Java dev in me that sees lock/unlock outside of a try/finally context and becomes uneasy
| changes, _, err := i.evaluator.SetState(data) | ||
| if data.SyncContext != nil { | ||
| i.mtx.Lock() | ||
| i.contextValues = data.SyncContext.AsMap() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In java we have an optional lambda to manipulate this... I think it should be simple to add it here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I think you added this in the hook itself.
In the Java impl, we only run this transform function when we actually get the sync data. I think there's a reason to do it in the hook so many times, when we can do it before we store the context.
trying to add context value enrichment approach for in-process mode.
Long time no golang, so this might be a little bit of sub optimal approach open for feedback/insights