Skip to content

Commit 171f8c8

Browse files
refactor(go): Move sticky resolve handling logic to provider level
Address review feedback by moving the sticky resolve response handling logic from SwapWasmResolverApi to LocalResolverProvider. Changes: - Update WasmResolverApi.ResolveWithSticky to return ResolveWithStickyResponse instead of ResolveFlagsResponse - Move response handling logic (Success/MissingMaterializations) from SwapWasmResolverApi to LocalResolverProvider - Add handleStickyResponse, storeUpdates, and handleMissingMaterializations methods to LocalResolverProvider - Remove sticky strategy parameter from NewSwapWasmResolverApi - Update all tests to work with the new interface - Remove obsolete sticky_resolve_strategy_test.go (logic now tested at provider level) This separates concerns: SwapWasmResolverApi is now only responsible for managing WASM instances, while LocalResolverProvider handles the business logic for sticky resolve strategies. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 80cce20 commit 171f8c8

File tree

8 files changed

+231
-536
lines changed

8 files changed

+231
-536
lines changed

openfeature-provider/go/confidence/integration_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ func createProviderWithTestState(
191191
runtime := wazero.NewRuntimeWithConfig(ctx, runtimeConfig)
192192

193193
// Create SwapWasmResolverApi without initial state (lazy initialization)
194-
resolverAPI, err := NewSwapWasmResolverApi(ctx, runtime, defaultWasmBytes, logger, slog.New(slog.NewTextHandler(os.Stderr, nil)), nil)
194+
resolverAPI, err := NewSwapWasmResolverApi(ctx, runtime, defaultWasmBytes, logger, slog.New(slog.NewTextHandler(os.Stderr, nil)))
195195
if err != nil {
196196
return nil, err
197197
}

openfeature-provider/go/confidence/local_resolver_provider.go

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ func (p *LocalResolverProvider) ObjectEvaluation(
280280
}
281281

282282
// Resolve flags with sticky support
283-
response, err := p.resolverAPI.ResolveWithSticky(ctx, stickyRequest)
283+
stickyResponse, err := p.resolverAPI.ResolveWithSticky(ctx, stickyRequest)
284284
if err != nil {
285285
p.logger.Error("Failed to resolve flag", "flag", flagPath, "error", err)
286286
return openfeature.InterfaceResolutionDetail{
@@ -292,6 +292,19 @@ func (p *LocalResolverProvider) ObjectEvaluation(
292292
}
293293
}
294294

295+
// Handle the sticky response and extract the actual resolve response
296+
response, err := p.handleStickyResponse(ctx, stickyRequest, stickyResponse)
297+
if err != nil {
298+
p.logger.Error("Failed to handle sticky response", "flag", flagPath, "error", err)
299+
return openfeature.InterfaceResolutionDetail{
300+
Value: defaultValue,
301+
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
302+
Reason: openfeature.ErrorReason,
303+
ResolutionError: openfeature.NewGeneralResolutionError(fmt.Sprintf("resolve failed: %v", err)),
304+
},
305+
}
306+
}
307+
295308
// Check if flag was found
296309
if len(response.ResolvedFlags) == 0 {
297310
p.logger.Info("No active flag was found", "flag", flagPath)
@@ -448,6 +461,145 @@ func (p *LocalResolverProvider) Shutdown() {
448461
}
449462
}
450463

464+
// handleStickyResponse processes the sticky response and returns the actual resolve response
465+
func (p *LocalResolverProvider) handleStickyResponse(
466+
ctx context.Context,
467+
request *resolver.ResolveWithStickyRequest,
468+
stickyResponse *resolver.ResolveWithStickyResponse,
469+
) (*resolver.ResolveFlagsResponse, error) {
470+
// Handle the response based on result type
471+
switch result := stickyResponse.ResolveResult.(type) {
472+
case *resolver.ResolveWithStickyResponse_Success_:
473+
success := result.Success
474+
// Store updates if present and we have a MaterializationRepository
475+
if len(success.GetUpdates()) > 0 {
476+
p.storeUpdates(ctx, success.GetUpdates())
477+
}
478+
return success.Response, nil
479+
480+
case *resolver.ResolveWithStickyResponse_MissingMaterializations_:
481+
missingMaterializations := result.MissingMaterializations
482+
483+
// Check for ResolverFallback first - return early if so
484+
if fallback, ok := p.stickyResolveStrategy.(ResolverFallback); ok {
485+
return fallback.Resolve(ctx, request.GetResolveRequest())
486+
}
487+
488+
// Handle MaterializationRepository case
489+
if repo, ok := p.stickyResolveStrategy.(MaterializationRepository); ok {
490+
updatedRequest, err := p.handleMissingMaterializations(ctx, request, missingMaterializations.GetItems(), repo)
491+
if err != nil {
492+
return nil, fmt.Errorf("failed to handle missing materializations: %w", err)
493+
}
494+
// Retry with the updated request
495+
retryResponse, err := p.resolverAPI.ResolveWithSticky(ctx, updatedRequest)
496+
if err != nil {
497+
return nil, err
498+
}
499+
// Recursively handle the response (in case there are more missing materializations)
500+
return p.handleStickyResponse(ctx, updatedRequest, retryResponse)
501+
}
502+
503+
// If no strategy is configured, return an error
504+
if p.stickyResolveStrategy == nil {
505+
return nil, fmt.Errorf("missing materializations and no sticky resolve strategy configured")
506+
}
507+
508+
return nil, fmt.Errorf("unknown sticky resolve strategy type: %T", p.stickyResolveStrategy)
509+
510+
default:
511+
return nil, fmt.Errorf("unexpected resolve result type: %T", stickyResponse.ResolveResult)
512+
}
513+
}
514+
515+
// storeUpdates stores materialization updates asynchronously if we have a MaterializationRepository
516+
func (p *LocalResolverProvider) storeUpdates(ctx context.Context, updates []*resolver.ResolveWithStickyResponse_MaterializationUpdate) {
517+
repo, ok := p.stickyResolveStrategy.(MaterializationRepository)
518+
if !ok {
519+
return
520+
}
521+
522+
// Store updates asynchronously
523+
go func() {
524+
// Group updates by unit
525+
updatesByUnit := make(map[string][]*resolver.ResolveWithStickyResponse_MaterializationUpdate)
526+
for _, update := range updates {
527+
updatesByUnit[update.GetUnit()] = append(updatesByUnit[update.GetUnit()], update)
528+
}
529+
530+
// Store assignments for each unit
531+
for unit, unitUpdates := range updatesByUnit {
532+
assignments := make(map[string]*MaterializationInfo)
533+
for _, update := range unitUpdates {
534+
ruleToVariant := map[string]string{update.GetRule(): update.GetVariant()}
535+
assignments[update.GetWriteMaterialization()] = &MaterializationInfo{
536+
UnitInMaterialization: true,
537+
RuleToVariant: ruleToVariant,
538+
}
539+
}
540+
541+
if err := repo.StoreAssignment(ctx, unit, assignments); err != nil {
542+
p.logger.Error("Failed to store materialization updates",
543+
"unit", unit,
544+
"error", err)
545+
}
546+
}
547+
}()
548+
}
549+
550+
// handleMissingMaterializations loads missing materializations from the repository
551+
// and returns an updated request with the materializations added
552+
func (p *LocalResolverProvider) handleMissingMaterializations(
553+
ctx context.Context,
554+
request *resolver.ResolveWithStickyRequest,
555+
missingItems []*resolver.ResolveWithStickyResponse_MissingMaterializationItem,
556+
repo MaterializationRepository,
557+
) (*resolver.ResolveWithStickyRequest, error) {
558+
// Group missing items by unit for efficient loading
559+
missingByUnit := make(map[string][]*resolver.ResolveWithStickyResponse_MissingMaterializationItem)
560+
for _, item := range missingItems {
561+
missingByUnit[item.GetUnit()] = append(missingByUnit[item.GetUnit()], item)
562+
}
563+
564+
// Create the materializations per unit map
565+
materializationsPerUnit := make(map[string]*resolver.MaterializationMap)
566+
567+
// Copy existing materializations
568+
for k, v := range request.GetMaterializationsPerUnit() {
569+
materializationsPerUnit[k] = v
570+
}
571+
572+
// Load materialized assignments for all missing units
573+
for unit, items := range missingByUnit {
574+
for _, item := range items {
575+
loadedAssignments, err := repo.LoadMaterializedAssignmentsForUnit(ctx, unit, item.GetReadMaterialization())
576+
if err != nil {
577+
return nil, fmt.Errorf("failed to load materializations for unit %s: %w", unit, err)
578+
}
579+
580+
// Ensure the map exists for this unit
581+
if materializationsPerUnit[unit] == nil {
582+
materializationsPerUnit[unit] = &resolver.MaterializationMap{
583+
InfoMap: make(map[string]*resolver.MaterializationInfo),
584+
}
585+
}
586+
587+
// Add loaded assignments to the materialization map
588+
for name, info := range loadedAssignments {
589+
materializationsPerUnit[unit].InfoMap[name] = info.ToProto()
590+
}
591+
}
592+
}
593+
594+
// Create a new request with the updated materializations
595+
return &resolver.ResolveWithStickyRequest{
596+
ResolveRequest: request.GetResolveRequest(),
597+
MaterializationsPerUnit: materializationsPerUnit,
598+
FailFastOnSticky: request.GetFailFastOnSticky(),
599+
NotProcessSticky: request.GetNotProcessSticky(),
600+
}, nil
601+
}
602+
451603
// startScheduledTasks starts the background tasks for state fetching and log polling
452604
func (p *LocalResolverProvider) startScheduledTasks(parentCtx context.Context) {
453605
ctx, cancel := context.WithCancel(parentCtx)

openfeature-provider/go/confidence/local_resolver_provider_resolve_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func TestLocalResolverProvider_ReturnsDefaultOnError(t *testing.T) {
3434
stateBytes, _ := proto.Marshal(state)
3535

3636
flagLogger := NewNoOpWasmFlagLogger()
37-
swap, err := NewSwapWasmResolverApi(ctx, runtime, defaultWasmBytes, flagLogger, slog.New(slog.NewTextHandler(os.Stderr, nil)), nil)
37+
swap, err := NewSwapWasmResolverApi(ctx, runtime, defaultWasmBytes, flagLogger, slog.New(slog.NewTextHandler(os.Stderr, nil)))
3838
if err != nil {
3939
t.Fatalf("Failed to create SwapWasmResolverApi: %v", err)
4040
}
@@ -85,7 +85,7 @@ func TestLocalResolverProvider_ReturnsCorrectValue(t *testing.T) {
8585
testAcctID := loadTestAccountID(t)
8686

8787
flagLogger := NewNoOpWasmFlagLogger()
88-
swap, err := NewSwapWasmResolverApi(ctx, runtime, defaultWasmBytes, flagLogger, slog.New(slog.NewTextHandler(os.Stderr, nil)), nil)
88+
swap, err := NewSwapWasmResolverApi(ctx, runtime, defaultWasmBytes, flagLogger, slog.New(slog.NewTextHandler(os.Stderr, nil)))
8989
if err != nil {
9090
t.Fatalf("Failed to create SwapWasmResolverApi: %v", err)
9191
}
@@ -172,7 +172,7 @@ func TestLocalResolverProvider_MissingMaterializations(t *testing.T) {
172172
testAcctID := loadTestAccountID(t)
173173

174174
flagLogger := NewNoOpWasmFlagLogger()
175-
swap, err := NewSwapWasmResolverApi(ctx, runtime, defaultWasmBytes, flagLogger, slog.New(slog.NewTextHandler(os.Stderr, nil)), nil)
175+
swap, err := NewSwapWasmResolverApi(ctx, runtime, defaultWasmBytes, flagLogger, slog.New(slog.NewTextHandler(os.Stderr, nil)))
176176
if err != nil {
177177
t.Fatalf("Failed to create SwapWasmResolverApi: %v", err)
178178
}
@@ -217,7 +217,7 @@ func TestLocalResolverProvider_MissingMaterializations(t *testing.T) {
217217
accountId := "test-account"
218218

219219
flagLogger := NewNoOpWasmFlagLogger()
220-
swap, err := NewSwapWasmResolverApi(ctx, runtime, defaultWasmBytes, flagLogger, slog.New(slog.NewTextHandler(os.Stderr, nil)), nil)
220+
swap, err := NewSwapWasmResolverApi(ctx, runtime, defaultWasmBytes, flagLogger, slog.New(slog.NewTextHandler(os.Stderr, nil)))
221221
if err != nil {
222222
t.Fatalf("Failed to create SwapWasmResolverApi: %v", err)
223223
}

openfeature-provider/go/confidence/local_resolver_provider_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ func (m *mockStateProviderForInit) Provide(ctx context.Context) ([]byte, string,
437437
type mockResolverAPIForInit struct {
438438
updateStateFunc func(state []byte, accountID string) error
439439
closeFunc func(ctx context.Context)
440-
resolveWithSticky func(ctx context.Context, request *resolver.ResolveWithStickyRequest) (*resolver.ResolveFlagsResponse, error)
440+
resolveWithSticky func(ctx context.Context, request *resolver.ResolveWithStickyRequest) (*resolver.ResolveWithStickyResponse, error)
441441
}
442442

443443
func (m *mockResolverAPIForInit) UpdateStateAndFlushLogs(state []byte, accountID string) error {
@@ -453,7 +453,7 @@ func (m *mockResolverAPIForInit) Close(ctx context.Context) {
453453
}
454454
}
455455

456-
func (m *mockResolverAPIForInit) ResolveWithSticky(ctx context.Context, request *resolver.ResolveWithStickyRequest) (*resolver.ResolveFlagsResponse, error) {
456+
func (m *mockResolverAPIForInit) ResolveWithSticky(ctx context.Context, request *resolver.ResolveWithStickyRequest) (*resolver.ResolveWithStickyResponse, error) {
457457
if m.resolveWithSticky != nil {
458458
return m.resolveWithSticky(ctx, request)
459459
}

openfeature-provider/go/confidence/provider_builder.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func NewProvider(ctx context.Context, config ProviderConfig) (*LocalResolverProv
9797

9898
// Create SwapWasmResolverApi without initial state (lazy initialization)
9999
// State will be set during Provider.Init()
100-
resolverAPI, err := NewSwapWasmResolverApi(ctx, wasmRuntime, defaultWasmBytes, flagLogger, logger, stickyStrategy)
100+
resolverAPI, err := NewSwapWasmResolverApi(ctx, wasmRuntime, defaultWasmBytes, flagLogger, logger)
101101
if err != nil {
102102
wasmRuntime.Close(ctx)
103103
return nil, fmt.Errorf("failed to create resolver API: %w", err)
@@ -132,7 +132,7 @@ func NewProviderForTest(ctx context.Context, config ProviderTestConfig) (*LocalR
132132

133133
// Create SwapWasmResolverApi without initial state (lazy initialization)
134134
// State will be set during Provider.Init()
135-
resolverAPI, err := NewSwapWasmResolverApi(ctx, wasmRuntime, defaultWasmBytes, config.FlagLogger, logger, stickyStrategy)
135+
resolverAPI, err := NewSwapWasmResolverApi(ctx, wasmRuntime, defaultWasmBytes, config.FlagLogger, logger)
136136
if err != nil {
137137
wasmRuntime.Close(ctx)
138138
return nil, fmt.Errorf("failed to create resolver API: %w", err)

0 commit comments

Comments
 (0)