Skip to content

Commit b9c099c

Browse files
feat!: support emitting errors from the bulk evaluator (#1338)
Fixes #1328 ### Improvement flagd's core components are intended to be reused. This PR change the`IStore` interface by allowing an error to be returned from `GetAll`. This error is then propagated through `ResolveAllValues`. This change enables custom `IStore` implementations to return errors and propagate them through the resolver layer. With this change, I have upgrade OFREP bulk evaluator and flagd RPC `ResolveAll` with error propagation. OFREP - Log warning with resolver error and return HTTP 500 with a tracking reference RPC - Log warning with resolver error and return an error with a tracking reference Signed-off-by: Kavindu Dodanduwa <[email protected]> Co-authored-by: Todd Baert <[email protected]>
1 parent f82c094 commit b9c099c

File tree

15 files changed

+264
-69
lines changed

15 files changed

+264
-69
lines changed

core/pkg/evaluator/fractional.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func NewFractional(logger *logger.Logger) *Fractional {
4040
func (fe *Fractional) Evaluate(values, data any) any {
4141
valueToDistribute, feDistributions, err := parseFractionalEvaluationData(values, data)
4242
if err != nil {
43-
fe.Logger.Error(fmt.Sprintf("parse fractional evaluation data: %v", err))
43+
fe.Logger.Warn(fmt.Sprintf("parse fractional evaluation data: %v", err))
4444
return nil
4545
}
4646

core/pkg/evaluator/ievaluator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,5 @@ type IResolver interface {
7777
ResolveAllValues(
7878
ctx context.Context,
7979
reqID string,
80-
context map[string]any) (values []AnyValue)
80+
context map[string]any) (values []AnyValue, err error)
8181
}

core/pkg/evaluator/json.go

Lines changed: 14 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -156,17 +156,22 @@ func NewResolver(store store.IStore, logger *logger.Logger, jsonEvalTracer trace
156156
return Resolver{store: store, Logger: logger, tracer: jsonEvalTracer}
157157
}
158158

159-
func (je *Resolver) ResolveAllValues(ctx context.Context, reqID string, context map[string]any) []AnyValue {
159+
func (je *Resolver) ResolveAllValues(ctx context.Context, reqID string, context map[string]any) ([]AnyValue, error) {
160160
_, span := je.tracer.Start(ctx, "resolveAll")
161161
defer span.End()
162162

163+
var err error
164+
allFlags, err := je.store.GetAll(ctx)
165+
if err != nil {
166+
return nil, fmt.Errorf("error retreiving flags from the store: %w", err)
167+
}
168+
163169
values := []AnyValue{}
164170
var value interface{}
165171
var variant string
166172
var reason string
167173
var metadata map[string]interface{}
168-
var err error
169-
allFlags := je.store.GetAll(ctx)
174+
170175
for flagKey, flag := range allFlags {
171176
if flag.State == Disabled {
172177
// ignore evaluation of disabled flag
@@ -176,44 +181,21 @@ func (je *Resolver) ResolveAllValues(ctx context.Context, reqID string, context
176181
defaultValue := flag.Variants[flag.DefaultVariant]
177182
switch defaultValue.(type) {
178183
case bool:
179-
value, variant, reason, metadata, err = resolve[bool](
180-
ctx,
181-
reqID,
182-
flagKey,
183-
context,
184-
je.evaluateVariant,
185-
)
184+
value, variant, reason, metadata, err = resolve[bool](ctx, reqID, flagKey, context, je.evaluateVariant)
186185
case string:
187-
value, variant, reason, metadata, err = resolve[string](
188-
ctx,
189-
reqID,
190-
flagKey,
191-
context,
192-
je.evaluateVariant,
193-
)
186+
value, variant, reason, metadata, err = resolve[string](ctx, reqID, flagKey, context, je.evaluateVariant)
194187
case float64:
195-
value, variant, reason, metadata, err = resolve[float64](
196-
ctx,
197-
reqID,
198-
flagKey,
199-
context,
200-
je.evaluateVariant,
201-
)
188+
value, variant, reason, metadata, err = resolve[float64](ctx, reqID, flagKey, context, je.evaluateVariant)
202189
case map[string]any:
203-
value, variant, reason, metadata, err = resolve[map[string]any](
204-
ctx,
205-
reqID,
206-
flagKey,
207-
context,
208-
je.evaluateVariant,
209-
)
190+
value, variant, reason, metadata, err = resolve[map[string]any](ctx, reqID, flagKey, context, je.evaluateVariant)
210191
}
211192
if err != nil {
212193
je.Logger.ErrorWithID(reqID, fmt.Sprintf("bulk evaluation: key: %s returned error: %s", flagKey, err.Error()))
213194
}
214195
values = append(values, NewAnyValue(value, variant, reason, flagKey, metadata, err))
215196
}
216-
return values
197+
198+
return values, nil
217199
}
218200

219201
func (je *Resolver) ResolveBooleanValue(

core/pkg/evaluator/json_test.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,11 @@ func TestResolveAllValues(t *testing.T) {
335335
}
336336
const reqID = "default"
337337
for _, test := range tests {
338-
vals := evaluator.ResolveAllValues(context.TODO(), reqID, test.context)
338+
vals, err := evaluator.ResolveAllValues(context.TODO(), reqID, test.context)
339+
if err != nil {
340+
t.Error("error from resolver", err)
341+
}
342+
339343
for _, val := range vals {
340344
// disabled flag must be ignored from bulk evaluation
341345
if val.FlagKey == DisabledFlag {
@@ -1234,21 +1238,30 @@ func TestFlagStateSafeForConcurrentReadWrites(t *testing.T) {
12341238
"Add_ResolveAllValues": {
12351239
dataSyncType: sync.ADD,
12361240
flagResolution: func(evaluator *evaluator.JSON) error {
1237-
evaluator.ResolveAllValues(context.TODO(), "", nil)
1241+
_, err := evaluator.ResolveAllValues(context.TODO(), "", nil)
1242+
if err != nil {
1243+
return err
1244+
}
12381245
return nil
12391246
},
12401247
},
12411248
"Update_ResolveAllValues": {
12421249
dataSyncType: sync.UPDATE,
12431250
flagResolution: func(evaluator *evaluator.JSON) error {
1244-
evaluator.ResolveAllValues(context.TODO(), "", nil)
1251+
_, err := evaluator.ResolveAllValues(context.TODO(), "", nil)
1252+
if err != nil {
1253+
return err
1254+
}
12451255
return nil
12461256
},
12471257
},
12481258
"Delete_ResolveAllValues": {
12491259
dataSyncType: sync.DELETE,
12501260
flagResolution: func(evaluator *evaluator.JSON) error {
1251-
evaluator.ResolveAllValues(context.TODO(), "", nil)
1261+
_, err := evaluator.ResolveAllValues(context.TODO(), "", nil)
1262+
if err != nil {
1263+
return err
1264+
}
12521265
return nil
12531266
},
12541267
},

core/pkg/evaluator/mock/ievaluator.go

Lines changed: 145 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/pkg/service/ofrep/models.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,20 @@ func ContextErrorResponseFrom(key string) EvaluationError {
7373
}
7474
}
7575

76-
func BulkEvaluationContextErrorFrom() BulkEvaluationError {
76+
func BulkEvaluationContextError() BulkEvaluationError {
7777
return BulkEvaluationError{
7878
ErrorCode: model.InvalidContextCode,
7979
ErrorDetails: "Provider context is not valid",
8080
}
8181
}
8282

83+
func BulkEvaluationContextErrorFrom(code string, details string) BulkEvaluationError {
84+
return BulkEvaluationError{
85+
ErrorCode: code,
86+
ErrorDetails: details,
87+
}
88+
}
89+
8390
func EvaluationErrorResponseFrom(result evaluator.AnyValue) (int, EvaluationError) {
8491
payload := EvaluationError{
8592
Key: result.FlagKey,

core/pkg/store/flags.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
)
1313

1414
type IStore interface {
15-
GetAll(ctx context.Context) map[string]model.Flag
15+
GetAll(ctx context.Context) (map[string]model.Flag, error)
1616
Get(ctx context.Context, key string) (model.Flag, bool)
1717
SelectorForFlag(ctx context.Context, flag model.Flag) string
1818
}
@@ -90,7 +90,7 @@ func (f *Flags) String() (string, error) {
9090
}
9191

9292
// GetAll returns a copy of the store's state (copy in order to be concurrency safe)
93-
func (f *Flags) GetAll(_ context.Context) map[string]model.Flag {
93+
func (f *Flags) GetAll(_ context.Context) (map[string]model.Flag, error) {
9494
f.mx.RLock()
9595
defer f.mx.RUnlock()
9696
state := make(map[string]model.Flag, len(f.Flags))
@@ -99,7 +99,7 @@ func (f *Flags) GetAll(_ context.Context) map[string]model.Flag {
9999
state[key] = flag
100100
}
101101

102-
return state
102+
return state, nil
103103
}
104104

105105
// Add new flags from source.
@@ -187,7 +187,12 @@ func (f *Flags) DeleteFlags(logger *logger.Logger, source string, flags map[stri
187187

188188
notifications := map[string]interface{}{}
189189
if len(flags) == 0 {
190-
allFlags := f.GetAll(ctx)
190+
allFlags, err := f.GetAll(ctx)
191+
if err != nil {
192+
logger.Error(fmt.Sprintf("error while retrieving flags from the store: %v", err))
193+
return notifications
194+
}
195+
191196
for key, flag := range allFlags {
192197
if flag.Source != source {
193198
continue

flagd/pkg/service/flag-evaluation/flag_evaluator.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,13 @@ func (s *OldFlagEvaluationService) ResolveAll(
6969
if e := req.Msg.GetContext(); e != nil {
7070
evalCtx = e.AsMap()
7171
}
72-
values := s.eval.ResolveAllValues(sCtx, reqID, evalCtx)
72+
73+
values, err := s.eval.ResolveAllValues(sCtx, reqID, evalCtx)
74+
if err != nil {
75+
s.logger.WarnWithID(reqID, fmt.Sprintf("error resolving all flags: %v", err))
76+
return nil, fmt.Errorf("error resolving flags. Tracking ID: %s", reqID)
77+
}
78+
7379
span.SetAttributes(attribute.Int("feature_flag.count", len(values)))
7480
for _, value := range values {
7581
// register the impression and reason for each flag evaluated

flagd/pkg/service/flag-evaluation/flag_evaluator_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ func TestConnectService_ResolveAll(t *testing.T) {
118118
t.Run(name, func(t *testing.T) {
119119
eval := mock.NewMockIEvaluator(ctrl)
120120
eval.EXPECT().ResolveAllValues(gomock.Any(), gomock.Any(), gomock.Any()).Return(
121-
tt.evalRes,
121+
tt.evalRes, nil,
122122
).AnyTimes()
123123
metrics, exp := getMetricReader()
124124
s := NewOldFlagEvaluationService(

0 commit comments

Comments
 (0)