Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: flagSetMetadata in OFREP/ResolveAll, core refactors #1540

Merged
merged 4 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion config/samples/example_flags.flagd.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
{
"$schema": "https://flagd.dev/schema/v0/flags.json",
"metadata": {
"flagSetId": "example",
"version": "v1"
},
"flags": {
"myBoolFlag": {
"state": "ENABLED",
"variants": {
"on": true,
"off": false
},
"defaultVariant": "on"
"defaultVariant": "on",
"metadata": {
"version": "v2"
}
},
"myStringFlag": {
"state": "ENABLED",
Expand Down
3 changes: 3 additions & 0 deletions config/samples/example_flags_secondary.flagd.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"$schema": "https://flagd.dev/schema/v0/flags.json",
"metadata": {
"version": "v2"
},
"flags": {
"myBoolFlag": {
"state": "ENABLED",
Expand Down
4 changes: 0 additions & 4 deletions core/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -317,10 +317,6 @@ golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1m
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/exp v0.0.0-20250128144449-3edf0e91c1ae h1:COZdc9Ut6wLq7MO9GIYxfZl4n4ScmgqQLoHocKXrxco=
golang.org/x/exp v0.0.0-20250128144449-3edf0e91c1ae/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
Expand Down
2 changes: 1 addition & 1 deletion core/pkg/evaluator/fractional_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,7 @@ func BenchmarkFractionalEvaluation(b *testing.B) {
for name, tt := range tests {
b.Run(name, func(b *testing.B) {
log := logger.NewLogger(nil, false)
je := NewJSON(log, &store.Flags{Flags: tt.flags.Flags})
je := NewJSON(log, &store.State{Flags: tt.flags.Flags})
for i := 0; i < b.N; i++ {
value, variant, reason, _, err := resolve[string](
ctx, reqID, tt.flagKey, tt.context, je.evaluateVariant)
Expand Down
19 changes: 10 additions & 9 deletions core/pkg/evaluator/ievaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package evaluator
import (
"context"

"github.com/open-feature/flagd/core/pkg/model"
"github.com/open-feature/flagd/core/pkg/sync"
)

Expand All @@ -11,12 +12,12 @@ type AnyValue struct {
Variant string
Reason string
FlagKey string
Metadata map[string]interface{}
Metadata model.Metadata
Error error
}

func NewAnyValue(
value interface{}, variant string, reason string, flagKey string, metadata map[string]interface{},
value interface{}, variant string, reason string, flagKey string, metadata model.Metadata,
err error,
) AnyValue {
return AnyValue{
Expand All @@ -34,7 +35,7 @@ IEvaluator is an extension of IResolver, allowing storage updates and retrievals
*/
type IEvaluator interface {
GetState() (string, error)
SetState(payload sync.DataSync) (map[string]interface{}, bool, error)
SetState(payload sync.DataSync) (model.Metadata, bool, error)
IResolver
}

Expand All @@ -44,31 +45,31 @@ type IResolver interface {
ctx context.Context,
reqID string,
flagKey string,
context map[string]any) (value bool, variant string, reason string, metadata map[string]interface{}, err error)
context map[string]any) (value bool, variant string, reason string, metadata model.Metadata, err error)
ResolveStringValue(
ctx context.Context,
reqID string,
flagKey string,
context map[string]any) (
value string, variant string, reason string, metadata map[string]interface{}, err error)
value string, variant string, reason string, metadata model.Metadata, err error)
ResolveIntValue(
ctx context.Context,
reqID string,
flagKey string,
context map[string]any) (
value int64, variant string, reason string, metadata map[string]interface{}, err error)
value int64, variant string, reason string, metadata model.Metadata, err error)
ResolveFloatValue(
ctx context.Context,
reqID string,
flagKey string,
context map[string]any) (
value float64, variant string, reason string, metadata map[string]interface{}, err error)
value float64, variant string, reason string, metadata model.Metadata, err error)
ResolveObjectValue(
ctx context.Context,
reqID string,
flagKey string,
context map[string]any) (
value map[string]any, variant string, reason string, metadata map[string]interface{}, err error)
value map[string]any, variant string, reason string, metadata model.Metadata, err error)
ResolveAsAnyValue(
ctx context.Context,
reqID string,
Expand All @@ -77,5 +78,5 @@ type IResolver interface {
ResolveAllValues(
ctx context.Context,
reqID string,
context map[string]any) (values []AnyValue, err error)
context map[string]any) (resolutions []AnyValue, metadata model.Metadata, err error)
}
58 changes: 21 additions & 37 deletions core/pkg/evaluator/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,13 @@ func WithEvaluator(name string, evalFunc func(interface{}, interface{}) interfac

// JSON evaluator
type JSON struct {
store *store.Flags
store *store.State
Logger *logger.Logger
jsonEvalTracer trace.Tracer
Resolver
}

func NewJSON(logger *logger.Logger, s *store.Flags, opts ...JSONEvaluatorOption) *JSON {
func NewJSON(logger *logger.Logger, s *store.State, opts ...JSONEvaluatorOption) *JSON {
logger = logger.WithFields(
zap.String("component", "evaluator"),
zap.String("evaluator", "json"),
Expand Down Expand Up @@ -107,9 +107,9 @@ func (je *JSON) SetState(payload sync.DataSync) (map[string]interface{}, bool, e
trace.WithAttributes(attribute.String("feature_flag.sync_type", payload.Type.String())))
defer span.End()

var newFlags Flags
var definition Definition

err := configToFlags(je.Logger, payload.FlagData, &newFlags)
err := configToFlagDefinition(je.Logger, payload.FlagData, &definition)
if err != nil {
span.SetStatus(codes.Error, "flagSync error")
span.RecordError(err)
Expand All @@ -119,15 +119,16 @@ func (je *JSON) SetState(payload sync.DataSync) (map[string]interface{}, bool, e
var events map[string]interface{}
var reSync bool

// TODO: We do not handle metadata in ADD/UPDATE operations. These are only relevant for grpc sync implementations.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Q] did you miss this TODO or should it rather be a normal comment? reading your comment below the TODO would be to deprecate these cases (ADD/UPDATE/DELETE) for sync impls, or did I misunderstand it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are understanding correctly. I left it as a TODO because I wanted to open an issue to discuss the deprecation of these, which I still intend to do.

switch payload.Type {
case sync.ALL:
events, reSync = je.store.Merge(je.Logger, payload.Source, payload.Selector, newFlags.Flags)
events, reSync = je.store.Merge(je.Logger, payload.Source, payload.Selector, definition.Flags, definition.Metadata)
case sync.ADD:
events = je.store.Add(je.Logger, payload.Source, payload.Selector, newFlags.Flags)
events = je.store.Add(je.Logger, payload.Source, payload.Selector, definition.Flags)
case sync.UPDATE:
events = je.store.Update(je.Logger, payload.Source, payload.Selector, newFlags.Flags)
events = je.store.Update(je.Logger, payload.Source, payload.Selector, definition.Flags)
case sync.DELETE:
events = je.store.DeleteFlags(je.Logger, payload.Source, newFlags.Flags)
events = je.store.DeleteFlags(je.Logger, payload.Source, definition.Flags)
Comment on lines +122 to +131
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ADD/UPDATE are only relevant to hypothetical gRPC server implementations, and were a premature optimization to allow gRPC servers to tell flagd "just add these flags, or just change this flag" instead of sending a whole payload. I have never seen this implemented, and it's not relevant to any of the other sync types.

I think we should cease to enhance them and deprecate them in the protobuf.

default:
return nil, false, fmt.Errorf("unsupported sync type: %d", payload.Type)
}
Expand Down Expand Up @@ -156,14 +157,16 @@ 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, error) {
func (je *Resolver) ResolveAllValues(ctx context.Context, reqID string, context map[string]any) ([]AnyValue,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Q] is this the function that relates to this proto command (from evaluation.proto):
rpc ResolveAll(ResolveAllRequest) returns (ResolveAllResponse) {}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is the inner "domain layer" function satisfying that rpc. More directly, this function is the actual rpc endpoint.

model.Metadata, error,
) {
_, span := je.tracer.Start(ctx, "resolveAll")
defer span.End()

var err error
allFlags, err := je.store.GetAll(ctx)
allFlags, flagSetMetadata, err := je.store.GetAll(ctx)
if err != nil {
return nil, fmt.Errorf("error retreiving flags from the store: %w", err)
return nil, flagSetMetadata, fmt.Errorf("error retreiving flags from the store: %w", err)
}

values := []AnyValue{}
Expand Down Expand Up @@ -195,7 +198,7 @@ func (je *Resolver) ResolveAllValues(ctx context.Context, reqID string, context
values = append(values, NewAnyValue(value, variant, reason, flagKey, metadata, err))
}

return values, nil
return values, flagSetMetadata, nil
}

func (je *Resolver) ResolveBooleanValue(
Expand Down Expand Up @@ -312,9 +315,7 @@ func resolve[T constraints](ctx context.Context, reqID string, key string, conte
func (je *Resolver) evaluateVariant(ctx context.Context, reqID string, flagKey string, evalCtx map[string]any) (
variant string, variants map[string]interface{}, reason string, metadata map[string]interface{}, err error,
) {
metadata = map[string]interface{}{}

flag, ok := je.store.Get(ctx, flagKey)
flag, metadata, ok := je.store.Get(ctx, flagKey)
if !ok {
// flag not found
je.Logger.DebugWithID(reqID, fmt.Sprintf("requested flag could not be found: %s", flagKey))
Expand Down Expand Up @@ -447,8 +448,8 @@ func loadAndCompileSchema(log *logger.Logger) *gojsonschema.Schema {
return compiledSchema
}

// configToFlags convert string configurations to flags and store them to pointer newFlags
func configToFlags(log *logger.Logger, config string, newFlags *Flags) error {
// configToFlagDefinition convert string configurations to flags and store them to pointer newFlags
func configToFlagDefinition(log *logger.Logger, config string, definition *Definition) error {
compiledSchema := loadAndCompileSchema(log)

flagStringLoader := gojsonschema.NewStringLoader(config)
Expand All @@ -467,33 +468,16 @@ func configToFlags(log *logger.Logger, config string, newFlags *Flags) error {
return fmt.Errorf("transposing evaluators: %w", err)
}

var configData ConfigWithMetadata
err = json.Unmarshal([]byte(transposedConfig), &configData)
err = json.Unmarshal([]byte(transposedConfig), &definition)
if err != nil {
return fmt.Errorf("unmarshalling provided configurations: %w", err)
}

// Assign the flags from the unmarshalled config to the newFlags struct
newFlags.Flags = configData.Flags

// Assign metadata as a map to each flag's metadata
for key, flag := range newFlags.Flags {
if flag.Metadata == nil {
flag.Metadata = make(map[string]interface{})
}
for metaKey, metaValue := range configData.Metadata {
if _, exists := flag.Metadata[metaKey]; !exists {
flag.Metadata[metaKey] = metaValue
}
}
newFlags.Flags[key] = flag
}

return validateDefaultVariants(newFlags)
return validateDefaultVariants(definition)
}

// validateDefaultVariants returns an error if any of the default variants aren't valid
func validateDefaultVariants(flags *Flags) error {
func validateDefaultVariants(flags *Definition) error {
for name, flag := range flags.Flags {
if _, ok := flag.Variants[flag.DefaultVariant]; !ok {
return fmt.Errorf(
Expand Down
2 changes: 1 addition & 1 deletion core/pkg/evaluator/json_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type Evaluators struct {
Evaluators map[string]json.RawMessage `json:"$evaluators"`
}

type ConfigWithMetadata struct {
type Definition struct {
Flags map[string]model.Flag `json:"flags"`
Metadata map[string]interface{} `json:"metadata"`
}
Expand Down
Loading
Loading