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 <kavindudodanduwa@gmail.com>
Co-authored-by: Todd Baert <todd.baert@dynatrace.com>
This commit is contained in:
Kavindu Dodanduwa 2024-06-27 11:09:16 -07:00 committed by GitHub
parent f82c094f5c
commit b9c099cb7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 264 additions and 69 deletions

View File

@ -40,7 +40,7 @@ func NewFractional(logger *logger.Logger) *Fractional {
func (fe *Fractional) Evaluate(values, data any) any { func (fe *Fractional) Evaluate(values, data any) any {
valueToDistribute, feDistributions, err := parseFractionalEvaluationData(values, data) valueToDistribute, feDistributions, err := parseFractionalEvaluationData(values, data)
if err != nil { if err != nil {
fe.Logger.Error(fmt.Sprintf("parse fractional evaluation data: %v", err)) fe.Logger.Warn(fmt.Sprintf("parse fractional evaluation data: %v", err))
return nil return nil
} }

View File

@ -77,5 +77,5 @@ type IResolver interface {
ResolveAllValues( ResolveAllValues(
ctx context.Context, ctx context.Context,
reqID string, reqID string,
context map[string]any) (values []AnyValue) context map[string]any) (values []AnyValue, err error)
} }

View File

@ -156,17 +156,22 @@ func NewResolver(store store.IStore, logger *logger.Logger, jsonEvalTracer trace
return Resolver{store: store, Logger: logger, tracer: jsonEvalTracer} return Resolver{store: store, Logger: logger, tracer: jsonEvalTracer}
} }
func (je *Resolver) ResolveAllValues(ctx context.Context, reqID string, context map[string]any) []AnyValue { func (je *Resolver) ResolveAllValues(ctx context.Context, reqID string, context map[string]any) ([]AnyValue, error) {
_, span := je.tracer.Start(ctx, "resolveAll") _, span := je.tracer.Start(ctx, "resolveAll")
defer span.End() defer span.End()
var err error
allFlags, err := je.store.GetAll(ctx)
if err != nil {
return nil, fmt.Errorf("error retreiving flags from the store: %w", err)
}
values := []AnyValue{} values := []AnyValue{}
var value interface{} var value interface{}
var variant string var variant string
var reason string var reason string
var metadata map[string]interface{} var metadata map[string]interface{}
var err error
allFlags := je.store.GetAll(ctx)
for flagKey, flag := range allFlags { for flagKey, flag := range allFlags {
if flag.State == Disabled { if flag.State == Disabled {
// ignore evaluation of disabled flag // ignore evaluation of disabled flag
@ -176,44 +181,21 @@ func (je *Resolver) ResolveAllValues(ctx context.Context, reqID string, context
defaultValue := flag.Variants[flag.DefaultVariant] defaultValue := flag.Variants[flag.DefaultVariant]
switch defaultValue.(type) { switch defaultValue.(type) {
case bool: case bool:
value, variant, reason, metadata, err = resolve[bool]( value, variant, reason, metadata, err = resolve[bool](ctx, reqID, flagKey, context, je.evaluateVariant)
ctx,
reqID,
flagKey,
context,
je.evaluateVariant,
)
case string: case string:
value, variant, reason, metadata, err = resolve[string]( value, variant, reason, metadata, err = resolve[string](ctx, reqID, flagKey, context, je.evaluateVariant)
ctx,
reqID,
flagKey,
context,
je.evaluateVariant,
)
case float64: case float64:
value, variant, reason, metadata, err = resolve[float64]( value, variant, reason, metadata, err = resolve[float64](ctx, reqID, flagKey, context, je.evaluateVariant)
ctx,
reqID,
flagKey,
context,
je.evaluateVariant,
)
case map[string]any: case map[string]any:
value, variant, reason, metadata, err = resolve[map[string]any]( value, variant, reason, metadata, err = resolve[map[string]any](ctx, reqID, flagKey, context, je.evaluateVariant)
ctx,
reqID,
flagKey,
context,
je.evaluateVariant,
)
} }
if err != nil { if err != nil {
je.Logger.ErrorWithID(reqID, fmt.Sprintf("bulk evaluation: key: %s returned error: %s", flagKey, err.Error())) je.Logger.ErrorWithID(reqID, fmt.Sprintf("bulk evaluation: key: %s returned error: %s", flagKey, err.Error()))
} }
values = append(values, NewAnyValue(value, variant, reason, flagKey, metadata, err)) values = append(values, NewAnyValue(value, variant, reason, flagKey, metadata, err))
} }
return values
return values, nil
} }
func (je *Resolver) ResolveBooleanValue( func (je *Resolver) ResolveBooleanValue(

View File

@ -335,7 +335,11 @@ func TestResolveAllValues(t *testing.T) {
} }
const reqID = "default" const reqID = "default"
for _, test := range tests { for _, test := range tests {
vals := evaluator.ResolveAllValues(context.TODO(), reqID, test.context) vals, err := evaluator.ResolveAllValues(context.TODO(), reqID, test.context)
if err != nil {
t.Error("error from resolver", err)
}
for _, val := range vals { for _, val := range vals {
// disabled flag must be ignored from bulk evaluation // disabled flag must be ignored from bulk evaluation
if val.FlagKey == DisabledFlag { if val.FlagKey == DisabledFlag {
@ -1234,21 +1238,30 @@ func TestFlagStateSafeForConcurrentReadWrites(t *testing.T) {
"Add_ResolveAllValues": { "Add_ResolveAllValues": {
dataSyncType: sync.ADD, dataSyncType: sync.ADD,
flagResolution: func(evaluator *evaluator.JSON) error { flagResolution: func(evaluator *evaluator.JSON) error {
evaluator.ResolveAllValues(context.TODO(), "", nil) _, err := evaluator.ResolveAllValues(context.TODO(), "", nil)
if err != nil {
return err
}
return nil return nil
}, },
}, },
"Update_ResolveAllValues": { "Update_ResolveAllValues": {
dataSyncType: sync.UPDATE, dataSyncType: sync.UPDATE,
flagResolution: func(evaluator *evaluator.JSON) error { flagResolution: func(evaluator *evaluator.JSON) error {
evaluator.ResolveAllValues(context.TODO(), "", nil) _, err := evaluator.ResolveAllValues(context.TODO(), "", nil)
if err != nil {
return err
}
return nil return nil
}, },
}, },
"Delete_ResolveAllValues": { "Delete_ResolveAllValues": {
dataSyncType: sync.DELETE, dataSyncType: sync.DELETE,
flagResolution: func(evaluator *evaluator.JSON) error { flagResolution: func(evaluator *evaluator.JSON) error {
evaluator.ResolveAllValues(context.TODO(), "", nil) _, err := evaluator.ResolveAllValues(context.TODO(), "", nil)
if err != nil {
return err
}
return nil return nil
}, },
}, },

View File

@ -57,11 +57,12 @@ func (mr *MockIEvaluatorMockRecorder) GetState() *gomock.Call {
} }
// ResolveAllValues mocks base method. // ResolveAllValues mocks base method.
func (m *MockIEvaluator) ResolveAllValues(ctx context.Context, reqID string, context map[string]any) []evaluator.AnyValue { func (m *MockIEvaluator) ResolveAllValues(ctx context.Context, reqID string, context map[string]any) ([]evaluator.AnyValue, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ResolveAllValues", ctx, reqID, context) ret := m.ctrl.Call(m, "ResolveAllValues", ctx, reqID, context)
ret0, _ := ret[0].([]evaluator.AnyValue) ret0, _ := ret[0].([]evaluator.AnyValue)
return ret0 ret1, _ := ret[1].(error)
return ret0, ret1
} }
// ResolveAllValues indicates an expected call of ResolveAllValues. // ResolveAllValues indicates an expected call of ResolveAllValues.
@ -189,3 +190,145 @@ func (mr *MockIEvaluatorMockRecorder) SetState(payload any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetState", reflect.TypeOf((*MockIEvaluator)(nil).SetState), payload) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetState", reflect.TypeOf((*MockIEvaluator)(nil).SetState), payload)
} }
// MockIResolver is a mock of IResolver interface.
type MockIResolver struct {
ctrl *gomock.Controller
recorder *MockIResolverMockRecorder
}
// MockIResolverMockRecorder is the mock recorder for MockIResolver.
type MockIResolverMockRecorder struct {
mock *MockIResolver
}
// NewMockIResolver creates a new mock instance.
func NewMockIResolver(ctrl *gomock.Controller) *MockIResolver {
mock := &MockIResolver{ctrl: ctrl}
mock.recorder = &MockIResolverMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockIResolver) EXPECT() *MockIResolverMockRecorder {
return m.recorder
}
// ResolveAllValues mocks base method.
func (m *MockIResolver) ResolveAllValues(ctx context.Context, reqID string, context map[string]any) ([]evaluator.AnyValue, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ResolveAllValues", ctx, reqID, context)
ret0, _ := ret[0].([]evaluator.AnyValue)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ResolveAllValues indicates an expected call of ResolveAllValues.
func (mr *MockIResolverMockRecorder) ResolveAllValues(ctx, reqID, context any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResolveAllValues", reflect.TypeOf((*MockIResolver)(nil).ResolveAllValues), ctx, reqID, context)
}
// ResolveAsAnyValue mocks base method.
func (m *MockIResolver) ResolveAsAnyValue(ctx context.Context, reqID, flagKey string, context map[string]any) evaluator.AnyValue {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ResolveAsAnyValue", ctx, reqID, flagKey, context)
ret0, _ := ret[0].(evaluator.AnyValue)
return ret0
}
// ResolveAsAnyValue indicates an expected call of ResolveAsAnyValue.
func (mr *MockIResolverMockRecorder) ResolveAsAnyValue(ctx, reqID, flagKey, context any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResolveAsAnyValue", reflect.TypeOf((*MockIResolver)(nil).ResolveAsAnyValue), ctx, reqID, flagKey, context)
}
// ResolveBooleanValue mocks base method.
func (m *MockIResolver) ResolveBooleanValue(ctx context.Context, reqID, flagKey string, context map[string]any) (bool, string, string, map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ResolveBooleanValue", ctx, reqID, flagKey, context)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(string)
ret2, _ := ret[2].(string)
ret3, _ := ret[3].(map[string]any)
ret4, _ := ret[4].(error)
return ret0, ret1, ret2, ret3, ret4
}
// ResolveBooleanValue indicates an expected call of ResolveBooleanValue.
func (mr *MockIResolverMockRecorder) ResolveBooleanValue(ctx, reqID, flagKey, context any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResolveBooleanValue", reflect.TypeOf((*MockIResolver)(nil).ResolveBooleanValue), ctx, reqID, flagKey, context)
}
// ResolveFloatValue mocks base method.
func (m *MockIResolver) ResolveFloatValue(ctx context.Context, reqID, flagKey string, context map[string]any) (float64, string, string, map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ResolveFloatValue", ctx, reqID, flagKey, context)
ret0, _ := ret[0].(float64)
ret1, _ := ret[1].(string)
ret2, _ := ret[2].(string)
ret3, _ := ret[3].(map[string]any)
ret4, _ := ret[4].(error)
return ret0, ret1, ret2, ret3, ret4
}
// ResolveFloatValue indicates an expected call of ResolveFloatValue.
func (mr *MockIResolverMockRecorder) ResolveFloatValue(ctx, reqID, flagKey, context any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResolveFloatValue", reflect.TypeOf((*MockIResolver)(nil).ResolveFloatValue), ctx, reqID, flagKey, context)
}
// ResolveIntValue mocks base method.
func (m *MockIResolver) ResolveIntValue(ctx context.Context, reqID, flagKey string, context map[string]any) (int64, string, string, map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ResolveIntValue", ctx, reqID, flagKey, context)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(string)
ret2, _ := ret[2].(string)
ret3, _ := ret[3].(map[string]any)
ret4, _ := ret[4].(error)
return ret0, ret1, ret2, ret3, ret4
}
// ResolveIntValue indicates an expected call of ResolveIntValue.
func (mr *MockIResolverMockRecorder) ResolveIntValue(ctx, reqID, flagKey, context any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResolveIntValue", reflect.TypeOf((*MockIResolver)(nil).ResolveIntValue), ctx, reqID, flagKey, context)
}
// ResolveObjectValue mocks base method.
func (m *MockIResolver) ResolveObjectValue(ctx context.Context, reqID, flagKey string, context map[string]any) (map[string]any, string, string, map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ResolveObjectValue", ctx, reqID, flagKey, context)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(string)
ret2, _ := ret[2].(string)
ret3, _ := ret[3].(map[string]any)
ret4, _ := ret[4].(error)
return ret0, ret1, ret2, ret3, ret4
}
// ResolveObjectValue indicates an expected call of ResolveObjectValue.
func (mr *MockIResolverMockRecorder) ResolveObjectValue(ctx, reqID, flagKey, context any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResolveObjectValue", reflect.TypeOf((*MockIResolver)(nil).ResolveObjectValue), ctx, reqID, flagKey, context)
}
// ResolveStringValue mocks base method.
func (m *MockIResolver) ResolveStringValue(ctx context.Context, reqID, flagKey string, context map[string]any) (string, string, string, map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ResolveStringValue", ctx, reqID, flagKey, context)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(string)
ret2, _ := ret[2].(string)
ret3, _ := ret[3].(map[string]any)
ret4, _ := ret[4].(error)
return ret0, ret1, ret2, ret3, ret4
}
// ResolveStringValue indicates an expected call of ResolveStringValue.
func (mr *MockIResolverMockRecorder) ResolveStringValue(ctx, reqID, flagKey, context any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResolveStringValue", reflect.TypeOf((*MockIResolver)(nil).ResolveStringValue), ctx, reqID, flagKey, context)
}

View File

@ -73,13 +73,20 @@ func ContextErrorResponseFrom(key string) EvaluationError {
} }
} }
func BulkEvaluationContextErrorFrom() BulkEvaluationError { func BulkEvaluationContextError() BulkEvaluationError {
return BulkEvaluationError{ return BulkEvaluationError{
ErrorCode: model.InvalidContextCode, ErrorCode: model.InvalidContextCode,
ErrorDetails: "Provider context is not valid", ErrorDetails: "Provider context is not valid",
} }
} }
func BulkEvaluationContextErrorFrom(code string, details string) BulkEvaluationError {
return BulkEvaluationError{
ErrorCode: code,
ErrorDetails: details,
}
}
func EvaluationErrorResponseFrom(result evaluator.AnyValue) (int, EvaluationError) { func EvaluationErrorResponseFrom(result evaluator.AnyValue) (int, EvaluationError) {
payload := EvaluationError{ payload := EvaluationError{
Key: result.FlagKey, Key: result.FlagKey,

View File

@ -12,7 +12,7 @@ import (
) )
type IStore interface { type IStore interface {
GetAll(ctx context.Context) map[string]model.Flag GetAll(ctx context.Context) (map[string]model.Flag, error)
Get(ctx context.Context, key string) (model.Flag, bool) Get(ctx context.Context, key string) (model.Flag, bool)
SelectorForFlag(ctx context.Context, flag model.Flag) string SelectorForFlag(ctx context.Context, flag model.Flag) string
} }
@ -90,7 +90,7 @@ func (f *Flags) String() (string, error) {
} }
// GetAll returns a copy of the store's state (copy in order to be concurrency safe) // GetAll returns a copy of the store's state (copy in order to be concurrency safe)
func (f *Flags) GetAll(_ context.Context) map[string]model.Flag { func (f *Flags) GetAll(_ context.Context) (map[string]model.Flag, error) {
f.mx.RLock() f.mx.RLock()
defer f.mx.RUnlock() defer f.mx.RUnlock()
state := make(map[string]model.Flag, len(f.Flags)) state := make(map[string]model.Flag, len(f.Flags))
@ -99,7 +99,7 @@ func (f *Flags) GetAll(_ context.Context) map[string]model.Flag {
state[key] = flag state[key] = flag
} }
return state return state, nil
} }
// Add new flags from source. // Add new flags from source.
@ -187,7 +187,12 @@ func (f *Flags) DeleteFlags(logger *logger.Logger, source string, flags map[stri
notifications := map[string]interface{}{} notifications := map[string]interface{}{}
if len(flags) == 0 { if len(flags) == 0 {
allFlags := f.GetAll(ctx) allFlags, err := f.GetAll(ctx)
if err != nil {
logger.Error(fmt.Sprintf("error while retrieving flags from the store: %v", err))
return notifications
}
for key, flag := range allFlags { for key, flag := range allFlags {
if flag.Source != source { if flag.Source != source {
continue continue

View File

@ -69,7 +69,13 @@ func (s *OldFlagEvaluationService) ResolveAll(
if e := req.Msg.GetContext(); e != nil { if e := req.Msg.GetContext(); e != nil {
evalCtx = e.AsMap() evalCtx = e.AsMap()
} }
values := s.eval.ResolveAllValues(sCtx, reqID, evalCtx)
values, err := s.eval.ResolveAllValues(sCtx, reqID, evalCtx)
if err != nil {
s.logger.WarnWithID(reqID, fmt.Sprintf("error resolving all flags: %v", err))
return nil, fmt.Errorf("error resolving flags. Tracking ID: %s", reqID)
}
span.SetAttributes(attribute.Int("feature_flag.count", len(values))) span.SetAttributes(attribute.Int("feature_flag.count", len(values)))
for _, value := range values { for _, value := range values {
// register the impression and reason for each flag evaluated // register the impression and reason for each flag evaluated

View File

@ -118,7 +118,7 @@ func TestConnectService_ResolveAll(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
eval := mock.NewMockIEvaluator(ctrl) eval := mock.NewMockIEvaluator(ctrl)
eval.EXPECT().ResolveAllValues(gomock.Any(), gomock.Any(), gomock.Any()).Return( eval.EXPECT().ResolveAllValues(gomock.Any(), gomock.Any(), gomock.Any()).Return(
tt.evalRes, tt.evalRes, nil,
).AnyTimes() ).AnyTimes()
metrics, exp := getMetricReader() metrics, exp := getMetricReader()
s := NewOldFlagEvaluationService( s := NewOldFlagEvaluationService(

View File

@ -68,7 +68,12 @@ func (s *FlagEvaluationService) ResolveAll(
evalCtx = e.AsMap() evalCtx = e.AsMap()
} }
values := s.eval.ResolveAllValues(sCtx, reqID, evalCtx) values, err := s.eval.ResolveAllValues(sCtx, reqID, evalCtx)
if err != nil {
s.logger.WarnWithID(reqID, fmt.Sprintf("error resolving all flags: %v", err))
return nil, fmt.Errorf("error resolving flags. Tracking ID: %s", reqID)
}
span.SetAttributes(attribute.Int("feature_flag.count", len(values))) span.SetAttributes(attribute.Int("feature_flag.count", len(values)))
for _, value := range values { for _, value := range values {
// register the impression and reason for each flag evaluated // register the impression and reason for each flag evaluated

View File

@ -21,7 +21,8 @@ func TestConnectServiceV2_ResolveAll(t *testing.T) {
tests := map[string]struct { tests := map[string]struct {
req *evalV1.ResolveAllRequest req *evalV1.ResolveAllRequest
evalRes []evaluator.AnyValue evalRes []evaluator.AnyValue
wantErr error evalErr error
wantErr bool
wantRes *evalV1.ResolveAllResponse wantRes *evalV1.ResolveAllResponse
}{ }{
"happy-path": { "happy-path": {
@ -52,7 +53,6 @@ func TestConnectServiceV2_ResolveAll(t *testing.T) {
FlagKey: "object", FlagKey: "object",
}, },
}, },
wantErr: nil,
wantRes: &evalV1.ResolveAllResponse{ wantRes: &evalV1.ResolveAllResponse{
Flags: map[string]*evalV1.AnyFlag{ Flags: map[string]*evalV1.AnyFlag{
"bool": { "bool": {
@ -76,26 +76,37 @@ func TestConnectServiceV2_ResolveAll(t *testing.T) {
}, },
}, },
}, },
"resolver error": {
req: &evalV1.ResolveAllRequest{},
evalRes: []evaluator.AnyValue{},
evalErr: errors.New("some error from internal evaluator"),
wantErr: true,
},
} }
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
for name, tt := range tests { for name, tt := range tests {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
// given
eval := mock.NewMockIEvaluator(ctrl) eval := mock.NewMockIEvaluator(ctrl)
eval.EXPECT().ResolveAllValues(gomock.Any(), gomock.Any(), gomock.Any()).Return( eval.EXPECT().ResolveAllValues(gomock.Any(), gomock.Any(), gomock.Any()).Return(
tt.evalRes, tt.evalRes, tt.evalErr,
).AnyTimes() ).AnyTimes()
metrics, exp := getMetricReader() metrics, exp := getMetricReader()
s := NewFlagEvaluationService( s := NewFlagEvaluationService(logger.NewLogger(nil, false), eval, &eventingConfiguration{}, metrics)
logger.NewLogger(nil, false),
eval, // when
&eventingConfiguration{},
metrics,
)
got, err := s.ResolveAll(context.Background(), connect.NewRequest(tt.req)) got, err := s.ResolveAll(context.Background(), connect.NewRequest(tt.req))
if err != nil && !errors.Is(err, tt.wantErr) {
t.Errorf("ConnectService.ResolveAll() error = %v, wantErr %v", err.Error(), tt.wantErr.Error()) // then
if tt.wantErr {
if err == nil {
t.Error("expected error but git none")
}
return return
} }
var data metricdata.ResourceMetrics var data metricdata.ResourceMetrics
err = exp.Collect(context.TODO(), &data) err = exp.Collect(context.TODO(), &data)
require.Nil(t, err) require.Nil(t, err)

View File

@ -8,6 +8,7 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/open-feature/flagd/core/pkg/evaluator" "github.com/open-feature/flagd/core/pkg/evaluator"
"github.com/open-feature/flagd/core/pkg/logger" "github.com/open-feature/flagd/core/pkg/logger"
"github.com/open-feature/flagd/core/pkg/model"
"github.com/open-feature/flagd/core/pkg/service/ofrep" "github.com/open-feature/flagd/core/pkg/service/ofrep"
"github.com/rs/xid" "github.com/rs/xid"
) )
@ -37,6 +38,7 @@ func NewOfrepHandler(logger *logger.Logger, evaluator evaluator.IEvaluator) http
func (h *handler) HandleFlagEvaluation(w http.ResponseWriter, r *http.Request) { func (h *handler) HandleFlagEvaluation(w http.ResponseWriter, r *http.Request) {
requestID := xid.New().String() requestID := xid.New().String()
defer h.Logger.ClearFields(requestID)
// obtain flag key // obtain flag key
vars := mux.Vars(r) vars := mux.Vars(r)
@ -66,16 +68,25 @@ func (h *handler) HandleFlagEvaluation(w http.ResponseWriter, r *http.Request) {
func (h *handler) HandleBulkEvaluation(w http.ResponseWriter, r *http.Request) { func (h *handler) HandleBulkEvaluation(w http.ResponseWriter, r *http.Request) {
requestID := xid.New().String() requestID := xid.New().String()
defer h.Logger.ClearFields(requestID)
request, err := extractOfrepRequest(r) request, err := extractOfrepRequest(r)
if err != nil { if err != nil {
h.writeJSONToResponse(http.StatusBadRequest, ofrep.BulkEvaluationContextErrorFrom(), w) h.writeJSONToResponse(http.StatusBadRequest, ofrep.BulkEvaluationContextError(), w)
return return
} }
context := flagdContext(h.Logger, requestID, request) context := flagdContext(h.Logger, requestID, request)
evaluations := h.evaluator.ResolveAllValues(r.Context(), requestID, context) evaluations, err := h.evaluator.ResolveAllValues(r.Context(), requestID, context)
h.writeJSONToResponse(http.StatusOK, ofrep.BulkEvaluationResponseFrom(evaluations), w) if err != nil {
h.Logger.WarnWithID(requestID, fmt.Sprintf("error from resolver: %v", err))
res := ofrep.BulkEvaluationContextErrorFrom(model.GeneralErrorCode,
fmt.Sprintf("Bulk evaluation failed. Tracking ID: %s", requestID))
h.writeJSONToResponse(http.StatusInternalServerError, res, w)
} else {
h.writeJSONToResponse(http.StatusOK, ofrep.BulkEvaluationResponseFrom(evaluations), w)
}
} }
func (h *handler) writeJSONToResponse(status int, payload interface{}, w http.ResponseWriter) { func (h *handler) writeJSONToResponse(status int, payload interface{}, w http.ResponseWriter) {

View File

@ -155,6 +155,7 @@ func Test_handler_HandleBulkEvaluation(t *testing.T) {
method string method string
input *bytes.Reader input *bytes.Reader
mockAnyResponse []evaluator.AnyValue mockAnyResponse []evaluator.AnyValue
mockAnyError error
expectedStatus int expectedStatus int
}{ }{
@ -179,6 +180,14 @@ func Test_handler_HandleBulkEvaluation(t *testing.T) {
mockAnyResponse: []evaluator.AnyValue{genericErrorValue, flagNotFoundValue}, mockAnyResponse: []evaluator.AnyValue{genericErrorValue, flagNotFoundValue},
expectedStatus: http.StatusOK, expectedStatus: http.StatusOK,
}, },
{
name: "handles internal errors and yield 500",
method: "http.MethodPost",
input: bytes.NewReader([]byte{}),
mockAnyResponse: []evaluator.AnyValue{},
mockAnyError: errors.New("some internal error from evaluator"),
expectedStatus: http.StatusInternalServerError,
},
{ {
name: "valid context payload", name: "valid context payload",
method: http.MethodPost, method: http.MethodPost,
@ -197,11 +206,8 @@ func Test_handler_HandleBulkEvaluation(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
eval := mock.NewMockIEvaluator(gomock.NewController(t)) eval := mock.NewMockIEvaluator(gomock.NewController(t))
if test.mockAnyResponse != nil { eval.EXPECT().ResolveAllValues(gomock.Any(), gomock.Any(), gomock.Any()).
eval.EXPECT(). Return(test.mockAnyResponse, test.mockAnyError).MinTimes(0)
ResolveAllValues(gomock.Any(), gomock.Any(), gomock.Any()).
Return(test.mockAnyResponse)
}
h := handler{Logger: log, evaluator: eval} h := handler{Logger: log, evaluator: eval}

View File

@ -19,7 +19,9 @@ func Test_OfrepServiceStartStop(t *testing.T) {
port := 18282 port := 18282
eval := mock.NewMockIEvaluator(gomock.NewController(t)) eval := mock.NewMockIEvaluator(gomock.NewController(t))
eval.EXPECT().ResolveAllValues(gomock.Any(), gomock.Any(), gomock.Any()).Return([]evaluator.AnyValue{}) eval.EXPECT().ResolveAllValues(gomock.Any(), gomock.Any(), gomock.Any()).
Return([]evaluator.AnyValue{}, nil)
cfg := SvcConfiguration{ cfg := SvcConfiguration{
Logger: logger.NewLogger(nil, false), Logger: logger.NewLogger(nil, false),
Port: uint16(port), Port: uint16(port),

View File

@ -163,7 +163,11 @@ func (r *Multiplexer) SourcesAsMetadata() string {
func (r *Multiplexer) reFill() error { func (r *Multiplexer) reFill() error {
clear(r.selectorFlags) clear(r.selectorFlags)
all := r.store.GetAll(context.Background()) all, err := r.store.GetAll(context.Background())
if err != nil {
return fmt.Errorf("error retrieving flags from the store: %w", err)
}
bytes, err := json.Marshal(map[string]interface{}{"flags": all}) bytes, err := json.Marshal(map[string]interface{}{"flags": all})
if err != nil { if err != nil {
return fmt.Errorf("error from marshallin: %w", err) return fmt.Errorf("error from marshallin: %w", err)