Skip to content

Conversation

@aepfli
Copy link
Member

@aepfli aepfli commented Jul 29, 2025

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

@aepfli aepfli requested review from a team as code owners July 29, 2025 19:28
@aepfli aepfli force-pushed the feat/contextValues branch 2 times, most recently from fa7a42d to 30a0194 Compare July 30, 2025 06:34
gemini-code-assist[bot]

This comment was marked as outdated.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

This 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

  • Context Enrichment for In-Process Provider: Introduced a new "ContextEnricher" type and "WithContextEnricher" option to allow custom logic for modifying or adding to the evaluation context for the in-process flagd provider.
  • IService Interface Update: The "IService" interface now includes a "ContextValues()" method, enabling services to expose their current context values.
  • InProcess Service Context Storage: The "InProcess" service now stores "contextValues" derived from the "SyncContext" received during flag synchronization.
  • SyncContextHook Integration: A new "SyncContextHook" is used within the "Provider" to apply the configured "ContextEnricher" before flag evaluations.
  • Test Coverage: Added new test cases to "provider_test.go" to validate the functionality of the "WithContextEnricher" option and ensure proper context enrichment.
Changelog
  • providers/flagd/internal/mock/service_mock.go
    • Added "ContextValues()" method to "MockIService" for testing purposes.
  • providers/flagd/pkg/configuration.go
    • Introduced "ContextEnricher" field to "ProviderConfiguration" and a "WithContextEnricher" option to set it, along with a default implementation.
  • providers/flagd/pkg/iservice.go
    • Extended "IService" interface with "ContextValues()" method.
  • providers/flagd/pkg/provider.go
    • Modified "Provider" to include a slice of "hooks" and integrated "NewSyncContextHook" to apply context enrichment.
  • providers/flagd/pkg/provider_test.go
    • Added new test cases to verify the "ContextEnricher" functionality, including scenarios for adding and modifying context values.
  • providers/flagd/pkg/service/in_process/service.go
    • Implemented "contextValues" storage and retrieval within the "InProcess" service, capturing "SyncContext" data.
  • providers/flagd/pkg/service/in_process/service_grpc_test.go
    • Updated gRPC tests to include "SyncContext" in mock responses and assert its presence.
  • providers/flagd/pkg/service/rpc/service.go
    • Added a placeholder "ContextValues()" method returning "nil" for the RPC service.
  • providers/flagd/pkg/sync_context_hook.go
    • New file defining the "ContextEnricher" type and "SyncContextHook" for applying context modifications.
Activity
  • aepfli initiated a Gemini review and later requested a summary.
  • Early discussions between aepfli and toddbaert revolved around the complexity and configurability of the context enrichment approach, leading to the adoption of a configurable option.
  • gemini-code-assist[bot] provided critical feedback, identifying high-priority race conditions in test cases and the InProcess service's ContextValues implementation, suggesting fixes involving defensive copying and mutexes.
  • gemini-code-assist[bot] also recommended using reflect.DeepEqual for more robust map comparison in tests.

@aepfli
Copy link
Member Author

aepfli commented Oct 2, 2025

/gemini review

aepfli added 3 commits October 2, 2025 11:13
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]>
@aepfli aepfli force-pushed the feat/contextValues branch from 2106a06 to b6c9664 Compare October 2, 2025 09:14
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a 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.

@aepfli aepfli force-pushed the feat/contextValues branch from 81e621d to 0286738 Compare October 2, 2025 09:30
Signed-off-by: Simon Schrottner <[email protected]>
@aepfli aepfli force-pushed the feat/contextValues branch from ab45615 to 38fd36c Compare October 2, 2025 10:19
// 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()
Copy link

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?

Copy link
Member Author

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?

Copy link

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

@toddbaert toddbaert changed the title feat(flagd): add new context enrichment appraoch for in-process provider feat(flagd): add new context enrichment approach for in-process provider Oct 7, 2025
changes, _, err := i.evaluator.SetState(data)
if data.SyncContext != nil {
i.mtx.Lock()
i.contextValues = data.SyncContext.AsMap()
Copy link
Member

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.

Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants