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 {
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
}

View File

@ -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)
}

View File

@ -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(

View File

@ -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
},
},

View File

@ -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)
}

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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)

View File

@ -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,17 +68,26 @@ 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) {
// first marshal payload

View File

@ -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}

View File

@ -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),

View File

@ -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)