Skip to content

Commit 4ee43eb

Browse files
erkasahidvelji
andauthored
fix: ⚠️ correct execution order for the same level hooks (#458)
* fix: correct execution order for the same level hooks Signed-off-by: Roman Dmytrenko <rdmytrenko@gmail.com> * Update openfeature/client.go Co-authored-by: Sahid Velji <sahidvelji@gmail.com> Signed-off-by: Roman Dmytrenko <rdmytrenko@gmail.com> --------- Signed-off-by: Roman Dmytrenko <rdmytrenko@gmail.com> Co-authored-by: Sahid Velji <sahidvelji@gmail.com>
1 parent 0c9d36c commit 4ee43eb

File tree

2 files changed

+28
-18
lines changed

2 files changed

+28
-18
lines changed

openfeature/client.go

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -692,9 +692,8 @@ func (c *Client) evaluate(
692692
// ensure that the same provider & hooks are used across this transaction to avoid unexpected behaviour
693693
provider, globalHooks, globalEvalCtx := c.api.ForEvaluation(c.metadata.domain)
694694

695-
evalCtx = mergeContexts(evalCtx, c.evaluationContext, TransactionContext(ctx), globalEvalCtx) // API (global) -> transaction -> client -> invocation
696-
apiClientInvocationProviderHooks := slices.Concat(globalHooks, c.hooks, options.hooks, provider.Hooks()) // API, Client, Invocation, Provider
697-
providerInvocationClientAPIHooks := slices.Concat(provider.Hooks(), options.hooks, c.hooks, globalHooks) // Provider, Invocation, Client, API
695+
evalCtx = mergeContexts(evalCtx, c.evaluationContext, TransactionContext(ctx), globalEvalCtx) // API (global) -> transaction -> client -> invocation
696+
hooks := slices.Concat(globalHooks, c.hooks, options.hooks, provider.Hooks()) // API, Client, Invocation, Provider
698697

699698
var err error
700699
hookCtx := HookContext{
@@ -707,29 +706,29 @@ func (c *Client) evaluate(
707706
}
708707

709708
defer func() {
710-
c.finallyHooks(ctx, hookCtx, providerInvocationClientAPIHooks, evalDetails, options)
709+
c.finallyHooks(ctx, hookCtx, hooks, evalDetails, options)
711710
}()
712711

713712
// bypass short-circuit logic for the Noop provider; it is essentially stateless and a "special case"
714713
if _, ok := provider.(NoopProvider); !ok {
715714
// short circuit if provider is in NOT READY state
716715
if c.State() == NotReadyState {
717-
c.errorHooks(ctx, hookCtx, providerInvocationClientAPIHooks, ProviderNotReadyError, options)
716+
c.errorHooks(ctx, hookCtx, hooks, ProviderNotReadyError, options)
718717
return evalDetails, ProviderNotReadyError
719718
}
720719

721720
// short circuit if provider is in FATAL state
722721
if c.State() == FatalState {
723-
c.errorHooks(ctx, hookCtx, providerInvocationClientAPIHooks, ProviderFatalError, options)
722+
c.errorHooks(ctx, hookCtx, hooks, ProviderFatalError, options)
724723
return evalDetails, ProviderFatalError
725724
}
726725
}
727726

728-
evalCtx, err = c.beforeHooks(ctx, hookCtx, apiClientInvocationProviderHooks, evalCtx, options)
727+
evalCtx, err = c.beforeHooks(ctx, hookCtx, hooks, evalCtx, options)
729728
hookCtx.evaluationContext = evalCtx
730729
if err != nil {
731730
err = fmt.Errorf("before hook: %w", err)
732-
c.errorHooks(ctx, hookCtx, providerInvocationClientAPIHooks, err, options)
731+
c.errorHooks(ctx, hookCtx, hooks, err, options)
733732
return evalDetails, err
734733
}
735734

@@ -763,17 +762,17 @@ func (c *Client) evaluate(
763762
err = resolution.Error()
764763
if err != nil {
765764
err = fmt.Errorf("error code: %w", err)
766-
c.errorHooks(ctx, hookCtx, providerInvocationClientAPIHooks, err, options)
765+
c.errorHooks(ctx, hookCtx, hooks, err, options)
767766
evalDetails.ResolutionDetail = resolution.ResolutionDetail()
768767
evalDetails.Reason = ErrorReason
769768
return evalDetails, err
770769
}
771770
evalDetails.Value = resolution.Value
772771
evalDetails.ResolutionDetail = resolution.ResolutionDetail()
773772

774-
if err := c.afterHooks(ctx, hookCtx, providerInvocationClientAPIHooks, evalDetails, options); err != nil {
773+
if err := c.afterHooks(ctx, hookCtx, hooks, evalDetails, options); err != nil {
775774
err = fmt.Errorf("after hook: %w", err)
776-
c.errorHooks(ctx, hookCtx, providerInvocationClientAPIHooks, err, options)
775+
c.errorHooks(ctx, hookCtx, hooks, err, options)
777776
return evalDetails, err
778777
}
779778

@@ -791,6 +790,8 @@ func flattenContext(evalCtx EvaluationContext) FlattenedContext {
791790
return flatCtx
792791
}
793792

793+
// beforeHooks executes the Before hook for each hook in the collection.
794+
// Hooks are executed in forward order: API → Client → Invocation → Provider.
794795
func (c *Client) beforeHooks(
795796
ctx context.Context, hookCtx HookContext, hooks []Hook, evalCtx EvaluationContext, options EvaluationOptions,
796797
) (EvaluationContext, error) {
@@ -807,10 +808,13 @@ func (c *Client) beforeHooks(
807808
return mergeContexts(hookCtx.evaluationContext, evalCtx), nil
808809
}
809810

811+
// afterHooks executes the After hook for each hook in the collection.
812+
// Hooks are executed in reverse order: Provider → Invocation → Client → API.
810813
func (c *Client) afterHooks(
811814
ctx context.Context, hookCtx HookContext, hooks []Hook, evalDetails InterfaceEvaluationDetails, options EvaluationOptions,
812815
) error {
813-
for _, hook := range hooks {
816+
// reverse order
817+
for _, hook := range slices.Backward(hooks) {
814818
if err := hook.After(ctx, hookCtx, evalDetails, options.hookHints); err != nil {
815819
return err
816820
}
@@ -819,14 +823,20 @@ func (c *Client) afterHooks(
819823
return nil
820824
}
821825

826+
// errorHooks executes the Error hook for each hook in the collection.
827+
// Hooks are executed in reverse order: Provider → Invocation → Client → API.
822828
func (c *Client) errorHooks(ctx context.Context, hookCtx HookContext, hooks []Hook, err error, options EvaluationOptions) {
823-
for _, hook := range hooks {
829+
// reverse order
830+
for _, hook := range slices.Backward(hooks) {
824831
hook.Error(ctx, hookCtx, err, options.hookHints)
825832
}
826833
}
827834

835+
// finallyHooks executes the Finally hook for each hook in the collection.
836+
// Hooks are executed in reverse order: Provider → Invocation → Client → API.
828837
func (c *Client) finallyHooks(ctx context.Context, hookCtx HookContext, hooks []Hook, evalDetails InterfaceEvaluationDetails, options EvaluationOptions) {
829-
for _, hook := range hooks {
838+
// reverse order
839+
for _, hook := range slices.Backward(hooks) {
830840
hook.Finally(ctx, hookCtx, evalDetails, options.hookHints)
831841
}
832842
}

openfeature/hooks_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@ func TestRequirement_4_4_2(t *testing.T) {
652652
t.Errorf("error setting up provider %v", err)
653653
}
654654

655-
mockProvider.EXPECT().Hooks().Return([]Hook{mockProviderHook}).Times(2)
655+
mockProvider.EXPECT().Hooks().Return([]Hook{mockProviderHook}).Times(1)
656656

657657
client := NewClient(t.Name())
658658
client.AddHooks(mockClientHook)
@@ -704,7 +704,7 @@ func TestRequirement_4_4_2(t *testing.T) {
704704
client := NewClient(t.Name())
705705
client.AddHooks(mockClientHook)
706706

707-
mockProvider.EXPECT().Hooks().Return([]Hook{mockProviderHook}).Times(2)
707+
mockProvider.EXPECT().Hooks().Return([]Hook{mockProviderHook}).Times(1)
708708

709709
mockAPIHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("forced"))
710710

@@ -808,8 +808,8 @@ func TestRequirement_4_4_6(t *testing.T) {
808808

809809
mockHook1.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any())
810810
mockHook2.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any())
811-
mockHook1.EXPECT().After(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("forced"))
812-
// the lack of mockHook2.EXPECT().After() asserts that remaining hooks aren't invoked after an error
811+
mockHook2.EXPECT().After(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("forced"))
812+
// the lack of mockHook1.EXPECT().After() asserts that remaining hooks aren't invoked after an error
813813
mockHook1.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
814814
mockHook1.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
815815
mockHook2.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())

0 commit comments

Comments
 (0)