Skip to content
Open
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
4 changes: 2 additions & 2 deletions core/pkg/evaluator/fractional_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
expectedReason string
expectedErrorCode string
}{
"rachel@faas.com": {

Check failure on line 90 in core/pkg/evaluator/fractional_test.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "rachel@faas.com" 3 times.

See more on https://sonarcloud.io/project/issues?id=open-feature_flagd&issues=AZ1tOSGdia_1p5aQ3055&open=AZ1tOSGdia_1p5aQ3055&pullRequest=1922
flags: commonFlags,
flagKey: "headerColor",
context: map[string]any{
Expand All @@ -97,34 +97,34 @@
expectedValue: "#FFFF00",
expectedReason: model.TargetingMatchReason,
},
"monica@faas.com": {

Check failure on line 100 in core/pkg/evaluator/fractional_test.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "monica@faas.com" 3 times.

See more on https://sonarcloud.io/project/issues?id=open-feature_flagd&issues=AZ1tOSGdia_1p5aQ3054&open=AZ1tOSGdia_1p5aQ3054&pullRequest=1922
flags: commonFlags,
flagKey: "headerColor",
context: map[string]any{
"email": "monica@faas.com",
},
expectedVariant: "blue",
expectedValue: "#0000FF",

Check failure on line 107 in core/pkg/evaluator/fractional_test.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "#0000FF" 4 times.

See more on https://sonarcloud.io/project/issues?id=open-feature_flagd&issues=AZ1tOSGdia_1p5aQ3057&open=AZ1tOSGdia_1p5aQ3057&pullRequest=1922
expectedReason: model.TargetingMatchReason,
},
"joey@faas.com": {

Check failure on line 110 in core/pkg/evaluator/fractional_test.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "joey@faas.com" 3 times.

See more on https://sonarcloud.io/project/issues?id=open-feature_flagd&issues=AZ1tOSGdia_1p5aQ3053&open=AZ1tOSGdia_1p5aQ3053&pullRequest=1922
flags: commonFlags,
flagKey: "headerColor",
context: map[string]any{
"email": "joey@faas.com",
},
expectedVariant: "red",
expectedValue: "#FF0000",

Check failure on line 117 in core/pkg/evaluator/fractional_test.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "#FF0000" 9 times.

See more on https://sonarcloud.io/project/issues?id=open-feature_flagd&issues=AZ1tOSGdia_1p5aQ3056&open=AZ1tOSGdia_1p5aQ3056&pullRequest=1922
expectedReason: model.TargetingMatchReason,
},
"ross@faas.com": {

Check failure on line 120 in core/pkg/evaluator/fractional_test.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "ross@faas.com" 4 times.

See more on https://sonarcloud.io/project/issues?id=open-feature_flagd&issues=AZ1tOSGdia_1p5aQ3052&open=AZ1tOSGdia_1p5aQ3052&pullRequest=1922
flags: commonFlags,
flagKey: "headerColor",
context: map[string]any{
"email": "ross@faas.com",
},
expectedVariant: "green",
expectedValue: "#00FF00",

Check failure on line 127 in core/pkg/evaluator/fractional_test.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "#00FF00" 5 times.

See more on https://sonarcloud.io/project/issues?id=open-feature_flagd&issues=AZ1tOSGdia_1p5aQ3050&open=AZ1tOSGdia_1p5aQ3050&pullRequest=1922
expectedReason: model.TargetingMatchReason,
},
"rachel@faas.com with custom seed": {
Expand Down Expand Up @@ -313,7 +313,7 @@
},
flagKey: "headerColor",
context: map[string]any{
"email": "foo@foo.com",

Check failure on line 316 in core/pkg/evaluator/fractional_test.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "foo@foo.com" 4 times.

See more on https://sonarcloud.io/project/issues?id=open-feature_flagd&issues=AZ1tOSGdia_1p5aQ3051&open=AZ1tOSGdia_1p5aQ3051&pullRequest=1922
},
expectedVariant: "red",
expectedValue: "#FF0000",
Expand Down Expand Up @@ -409,7 +409,7 @@
}

je := NewJSON(log, s)
je.store.Update(source, tt.flags, model.Metadata{})
je.store.Update(source, tt.flags, model.Metadata{}, false)

value, variant, reason, _, err := resolve[string](ctx, reqID, tt.flagKey, tt.context, je.evaluateVariant)

Expand Down Expand Up @@ -537,7 +537,7 @@
b.Fatalf("NewStore failed: %v", err)
}
je := NewJSON(log, s)
je.store.Update(source, tt.flags, model.Metadata{})
je.store.Update(source, tt.flags, model.Metadata{}, false)

for i := 0; i < b.N; i++ {
value, variant, reason, _, err := resolve[string](
Expand Down
2 changes: 1 addition & 1 deletion core/pkg/evaluator/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func (je *JSON) SetState(payload sync.DataSync) error {
return err
}

je.store.Update(payload.Source, definition.Flags, definition.Metadata)
je.store.Update(payload.Source, definition.Flags, definition.Metadata, payload.IncrementalUpdates)

return nil
}
Expand Down
2 changes: 1 addition & 1 deletion core/pkg/evaluator/semver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,7 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) {
t.Fatalf("NewStore failed: %v", err)
}
je := NewJSON(log, s)
je.store.Update(source, tt.flags, model.Metadata{})
je.store.Update(source, tt.flags, model.Metadata{}, false)

value, variant, reason, _, err := resolve[string](ctx, reqID, tt.flagKey, tt.context, je.evaluateVariant)

Expand Down
4 changes: 2 additions & 2 deletions core/pkg/evaluator/string_comparison_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ func TestJSONEvaluator_startsWithEvaluation(t *testing.T) {
t.Fatalf("NewStore failed: %v", err)
}
je := NewJSON(log, s)
je.store.Update(source, tt.flags, model.Metadata{})
je.store.Update(source, tt.flags, model.Metadata{}, false)

value, variant, reason, _, err := resolve[string](ctx, reqID, tt.flagKey, tt.context, je.evaluateVariant)

Expand Down Expand Up @@ -325,7 +325,7 @@ func TestJSONEvaluator_endsWithEvaluation(t *testing.T) {
t.Fatalf("NewStore failed: %v", err)
}
je := NewJSON(log, s)
je.store.Update(source, tt.flags, model.Metadata{})
je.store.Update(source, tt.flags, model.Metadata{}, false)

value, variant, reason, _, err := resolve[string](ctx, reqID, tt.flagKey, tt.context, je.evaluateVariant)

Expand Down
17 changes: 12 additions & 5 deletions core/pkg/store/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,17 @@ func (s *Selector) ToMetadata() model.Metadata {
}

func (s *Selector) ToLogString() string {
if s != nil && len(s.indexMap) == 1 {
for k, v := range s.indexMap {
return fmt.Sprintf("'%s=%s'", k, v)
}
if s == nil || len(s.indexMap) == 0 {
return "<none>"
}
return "<none>"
keys := make([]string, 0, len(s.indexMap))
for k := range s.indexMap {
keys = append(keys, k)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, k := range keys {
parts = append(parts, fmt.Sprintf("%s=%s", k, s.indexMap[k]))
}
return "'" + strings.Join(parts, ",") + "'"
}
43 changes: 39 additions & 4 deletions core/pkg/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type IStore interface {
Get(ctx context.Context, key string, selector *Selector) (model.Flag, model.Metadata, error)
GetAll(ctx context.Context, selector *Selector) ([]model.Flag, model.Metadata, error)
Watch(ctx context.Context, selector *Selector, watcher chan<- FlagQueryResult)
Update(source string, flags []model.Flag, metadata model.Metadata)
Update(source string, flags []model.Flag, metadata model.Metadata, incrementalUpdate bool)
}

var _ IStore = (*Store)(nil)
Expand Down Expand Up @@ -214,10 +214,15 @@ type flagIdentifier struct {
}

// Update the flag state with the provided flags.
// When incrementalUpdate is true, deletion is scoped to only the flagSetIds present in
// this payload (from metadata and flag-level overrides), allowing flags from other
// flagSetIds to accumulate across updates. When false, all flags for the source are
// replaced (the default full-snapshot behavior).
func (s *Store) Update(
source string,
flags []model.Flag,
metadata model.Metadata,
incrementalUpdate bool,
) {
if source == "" {
panic("source cannot be empty")
Expand Down Expand Up @@ -254,9 +259,39 @@ func (s *Store) Update(
txn := s.db.Txn(true)
defer txn.Abort()

// get all flags for the source we are updating
selector := NewSelector(sourceIndex + "=" + source)
oldFlags, _, _ := s.GetAll(context.Background(), &selector)
// When incrementalUpdate is enabled, scope deletion to only the flagSetIds touched
// by this payload (metadata-level + flag-level overrides). This allows per-flagSetId
// updates (e.g., from per-project stream messages) to accumulate without deleting
// flags from unrelated flagSetIds. Otherwise, replace all flags for the source.
var oldFlags []model.Flag
if incrementalUpdate {
seenFlagSetIds := make(map[string]struct{})
if fsi, ok := metadata["flagSetId"].(string); ok && fsi != "" {
seenFlagSetIds[fsi] = struct{}{}
}
for id := range newFlags {
seenFlagSetIds[id.flagSetId] = struct{}{}
}
for fsi := range seenFlagSetIds {
sel := NewSelector(flagSetIdIndex + "=" + fsi).WithIndex(sourceIndex, source)
indexId, constraints := sel.ToQuery()
it, err := txn.Get(flagsTable, indexId, constraints...)
if err != nil {
s.logger.Error(fmt.Sprintf("unable to query flags for flagSetId %s: %v", fsi, err))
continue
}
oldFlags = append(oldFlags, s.collect(it)...)
}
} else {
sel := NewSelector(sourceIndex + "=" + source)
indexId, constraints := sel.ToQuery()
it, err := txn.Get(flagsTable, indexId, constraints...)
if err != nil {
s.logger.Error(fmt.Sprintf("unable to query flags for source %s: %v", source, err))
} else {
oldFlags = s.collect(it)
}
}

for _, oldFlag := range oldFlags {
if _, ok := newFlags[flagIdentifier{flagSetId: oldFlag.FlagSetId, key: oldFlag.Key}]; !ok {
Expand Down
Loading
Loading