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:
parent
f82c094f5c
commit
b9c099cb7f
|
|
@ -40,7 +40,7 @@ func NewFractional(logger *logger.Logger) *Fractional {
|
|||
func (fe *Fractional) Evaluate(values, data any) any {
|
||||
valueToDistribute, feDistributions, err := parseFractionalEvaluationData(values, data)
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -77,5 +77,5 @@ type IResolver interface {
|
|||
ResolveAllValues(
|
||||
ctx context.Context,
|
||||
reqID string,
|
||||
context map[string]any) (values []AnyValue)
|
||||
context map[string]any) (values []AnyValue, err error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,17 +156,22 @@ func NewResolver(store store.IStore, logger *logger.Logger, jsonEvalTracer trace
|
|||
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")
|
||||
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{}
|
||||
var value interface{}
|
||||
var variant string
|
||||
var reason string
|
||||
var metadata map[string]interface{}
|
||||
var err error
|
||||
allFlags := je.store.GetAll(ctx)
|
||||
|
||||
for flagKey, flag := range allFlags {
|
||||
if flag.State == Disabled {
|
||||
// ignore evaluation of disabled flag
|
||||
|
|
@ -176,44 +181,21 @@ func (je *Resolver) ResolveAllValues(ctx context.Context, reqID string, context
|
|||
defaultValue := flag.Variants[flag.DefaultVariant]
|
||||
switch defaultValue.(type) {
|
||||
case bool:
|
||||
value, variant, reason, metadata, err = resolve[bool](
|
||||
ctx,
|
||||
reqID,
|
||||
flagKey,
|
||||
context,
|
||||
je.evaluateVariant,
|
||||
)
|
||||
value, variant, reason, metadata, err = resolve[bool](ctx, reqID, flagKey, context, je.evaluateVariant)
|
||||
case string:
|
||||
value, variant, reason, metadata, err = resolve[string](
|
||||
ctx,
|
||||
reqID,
|
||||
flagKey,
|
||||
context,
|
||||
je.evaluateVariant,
|
||||
)
|
||||
value, variant, reason, metadata, err = resolve[string](ctx, reqID, flagKey, context, je.evaluateVariant)
|
||||
case float64:
|
||||
value, variant, reason, metadata, err = resolve[float64](
|
||||
ctx,
|
||||
reqID,
|
||||
flagKey,
|
||||
context,
|
||||
je.evaluateVariant,
|
||||
)
|
||||
value, variant, reason, metadata, err = resolve[float64](ctx, reqID, flagKey, context, je.evaluateVariant)
|
||||
case map[string]any:
|
||||
value, variant, reason, metadata, err = resolve[map[string]any](
|
||||
ctx,
|
||||
reqID,
|
||||
flagKey,
|
||||
context,
|
||||
je.evaluateVariant,
|
||||
)
|
||||
value, variant, reason, metadata, err = resolve[map[string]any](ctx, reqID, flagKey, context, je.evaluateVariant)
|
||||
}
|
||||
if err != nil {
|
||||
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))
|
||||
}
|
||||
return values
|
||||
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (je *Resolver) ResolveBooleanValue(
|
||||
|
|
|
|||
|
|
@ -335,7 +335,11 @@ func TestResolveAllValues(t *testing.T) {
|
|||
}
|
||||
const reqID = "default"
|
||||
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 {
|
||||
// disabled flag must be ignored from bulk evaluation
|
||||
if val.FlagKey == DisabledFlag {
|
||||
|
|
@ -1234,21 +1238,30 @@ func TestFlagStateSafeForConcurrentReadWrites(t *testing.T) {
|
|||
"Add_ResolveAllValues": {
|
||||
dataSyncType: sync.ADD,
|
||||
flagResolution: func(evaluator *evaluator.JSON) error {
|
||||
evaluator.ResolveAllValues(context.TODO(), "", nil)
|
||||
_, err := evaluator.ResolveAllValues(context.TODO(), "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
"Update_ResolveAllValues": {
|
||||
dataSyncType: sync.UPDATE,
|
||||
flagResolution: func(evaluator *evaluator.JSON) error {
|
||||
evaluator.ResolveAllValues(context.TODO(), "", nil)
|
||||
_, err := evaluator.ResolveAllValues(context.TODO(), "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
"Delete_ResolveAllValues": {
|
||||
dataSyncType: sync.DELETE,
|
||||
flagResolution: func(evaluator *evaluator.JSON) error {
|
||||
evaluator.ResolveAllValues(context.TODO(), "", nil)
|
||||
_, err := evaluator.ResolveAllValues(context.TODO(), "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -57,11 +57,12 @@ func (mr *MockIEvaluatorMockRecorder) GetState() *gomock.Call {
|
|||
}
|
||||
|
||||
// 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()
|
||||
ret := m.ctrl.Call(m, "ResolveAllValues", ctx, reqID, context)
|
||||
ret0, _ := ret[0].([]evaluator.AnyValue)
|
||||
return ret0
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ResolveAllValues indicates an expected call of ResolveAllValues.
|
||||
|
|
@ -189,3 +190,145 @@ func (mr *MockIEvaluatorMockRecorder) SetState(payload any) *gomock.Call {
|
|||
mr.mock.ctrl.T.Helper()
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,13 +73,20 @@ func ContextErrorResponseFrom(key string) EvaluationError {
|
|||
}
|
||||
}
|
||||
|
||||
func BulkEvaluationContextErrorFrom() BulkEvaluationError {
|
||||
func BulkEvaluationContextError() BulkEvaluationError {
|
||||
return BulkEvaluationError{
|
||||
ErrorCode: model.InvalidContextCode,
|
||||
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) {
|
||||
payload := EvaluationError{
|
||||
Key: result.FlagKey,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import (
|
|||
)
|
||||
|
||||
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)
|
||||
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)
|
||||
func (f *Flags) GetAll(_ context.Context) map[string]model.Flag {
|
||||
func (f *Flags) GetAll(_ context.Context) (map[string]model.Flag, error) {
|
||||
f.mx.RLock()
|
||||
defer f.mx.RUnlock()
|
||||
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
|
||||
}
|
||||
|
||||
return state
|
||||
return state, nil
|
||||
}
|
||||
|
||||
// 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{}{}
|
||||
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 {
|
||||
if flag.Source != source {
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -69,7 +69,13 @@ func (s *OldFlagEvaluationService) ResolveAll(
|
|||
if e := req.Msg.GetContext(); e != nil {
|
||||
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)))
|
||||
for _, value := range values {
|
||||
// register the impression and reason for each flag evaluated
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ func TestConnectService_ResolveAll(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
eval := mock.NewMockIEvaluator(ctrl)
|
||||
eval.EXPECT().ResolveAllValues(gomock.Any(), gomock.Any(), gomock.Any()).Return(
|
||||
tt.evalRes,
|
||||
tt.evalRes, nil,
|
||||
).AnyTimes()
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewOldFlagEvaluationService(
|
||||
|
|
|
|||
|
|
@ -68,7 +68,12 @@ func (s *FlagEvaluationService) ResolveAll(
|
|||
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)))
|
||||
for _, value := range values {
|
||||
// register the impression and reason for each flag evaluated
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ func TestConnectServiceV2_ResolveAll(t *testing.T) {
|
|||
tests := map[string]struct {
|
||||
req *evalV1.ResolveAllRequest
|
||||
evalRes []evaluator.AnyValue
|
||||
wantErr error
|
||||
evalErr error
|
||||
wantErr bool
|
||||
wantRes *evalV1.ResolveAllResponse
|
||||
}{
|
||||
"happy-path": {
|
||||
|
|
@ -52,7 +53,6 @@ func TestConnectServiceV2_ResolveAll(t *testing.T) {
|
|||
FlagKey: "object",
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
wantRes: &evalV1.ResolveAllResponse{
|
||||
Flags: map[string]*evalV1.AnyFlag{
|
||||
"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)
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// given
|
||||
eval := mock.NewMockIEvaluator(ctrl)
|
||||
eval.EXPECT().ResolveAllValues(gomock.Any(), gomock.Any(), gomock.Any()).Return(
|
||||
tt.evalRes,
|
||||
tt.evalRes, tt.evalErr,
|
||||
).AnyTimes()
|
||||
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewFlagEvaluationService(
|
||||
logger.NewLogger(nil, false),
|
||||
eval,
|
||||
&eventingConfiguration{},
|
||||
metrics,
|
||||
)
|
||||
s := NewFlagEvaluationService(logger.NewLogger(nil, false), eval, &eventingConfiguration{}, metrics)
|
||||
|
||||
// when
|
||||
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
|
||||
}
|
||||
|
||||
var data metricdata.ResourceMetrics
|
||||
err = exp.Collect(context.TODO(), &data)
|
||||
require.Nil(t, err)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/gorilla/mux"
|
||||
"github.com/open-feature/flagd/core/pkg/evaluator"
|
||||
"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/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) {
|
||||
requestID := xid.New().String()
|
||||
defer h.Logger.ClearFields(requestID)
|
||||
|
||||
// obtain flag key
|
||||
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) {
|
||||
requestID := xid.New().String()
|
||||
defer h.Logger.ClearFields(requestID)
|
||||
|
||||
request, err := extractOfrepRequest(r)
|
||||
if err != nil {
|
||||
h.writeJSONToResponse(http.StatusBadRequest, ofrep.BulkEvaluationContextErrorFrom(), w)
|
||||
h.writeJSONToResponse(http.StatusBadRequest, ofrep.BulkEvaluationContextError(), w)
|
||||
return
|
||||
}
|
||||
|
||||
context := flagdContext(h.Logger, requestID, request)
|
||||
evaluations := h.evaluator.ResolveAllValues(r.Context(), requestID, context)
|
||||
evaluations, err := h.evaluator.ResolveAllValues(r.Context(), requestID, context)
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ func Test_handler_HandleBulkEvaluation(t *testing.T) {
|
|||
method string
|
||||
input *bytes.Reader
|
||||
mockAnyResponse []evaluator.AnyValue
|
||||
mockAnyError error
|
||||
|
||||
expectedStatus int
|
||||
}{
|
||||
|
|
@ -179,6 +180,14 @@ func Test_handler_HandleBulkEvaluation(t *testing.T) {
|
|||
mockAnyResponse: []evaluator.AnyValue{genericErrorValue, flagNotFoundValue},
|
||||
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",
|
||||
method: http.MethodPost,
|
||||
|
|
@ -197,11 +206,8 @@ func Test_handler_HandleBulkEvaluation(t *testing.T) {
|
|||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
eval := mock.NewMockIEvaluator(gomock.NewController(t))
|
||||
if test.mockAnyResponse != nil {
|
||||
eval.EXPECT().
|
||||
ResolveAllValues(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(test.mockAnyResponse)
|
||||
}
|
||||
eval.EXPECT().ResolveAllValues(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(test.mockAnyResponse, test.mockAnyError).MinTimes(0)
|
||||
|
||||
h := handler{Logger: log, evaluator: eval}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ func Test_OfrepServiceStartStop(t *testing.T) {
|
|||
port := 18282
|
||||
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{
|
||||
Logger: logger.NewLogger(nil, false),
|
||||
Port: uint16(port),
|
||||
|
|
|
|||
|
|
@ -163,7 +163,11 @@ func (r *Multiplexer) SourcesAsMetadata() string {
|
|||
func (r *Multiplexer) reFill() error {
|
||||
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})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error from marshallin: %w", err)
|
||||
|
|
|
|||
Loading…
Reference in New Issue