Skip to content

fix: scope flag deletion by flagSetId to support per-selector sync#1922

Open
erenatas wants to merge 1 commit intoopen-feature:mainfrom
erenatas:erenatas/support-per-selector-sync
Open

fix: scope flag deletion by flagSetId to support per-selector sync#1922
erenatas wants to merge 1 commit intoopen-feature:mainfrom
erenatas:erenatas/support-per-selector-sync

Conversation

@erenatas
Copy link
Copy Markdown

@erenatas erenatas commented Apr 1, 2026

This PR

When an upstream provider sends per-selector stream messages (each with a flagSetId in metadata),

  • store.Update now only deletes stale flags within the same flagSetId(s), preserving flags from other selector.
  • Updated ToLogString function since the old ToLogString only handled single-key selectors and returned <none> for compound ones. With the scoped deletion, selectors now have two keys (flagSetId+source), so debug logs would show <none> instead of the actual query — making it impossible to troubleshoot store operations.

Related Issues

This change should fix using flagd with a provider that has multiple selectors

@erenatas erenatas requested review from a team as code owners April 1, 2026 16:58
@netlify
Copy link
Copy Markdown

netlify bot commented Apr 1, 2026

Deploy Preview for polite-licorice-3db33c canceled.

Name Link
🔨 Latest commit 5bf055b
🔍 Latest deploy log https://app.netlify.com/projects/polite-licorice-3db33c/deploys/69cd51f698caae0008e87578

@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Apr 1, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces flagSetId-based scoping for flag updates, allowing for more granular control over flag deletions by targeting specific flag sets rather than the entire source. It also improves the selector's log string generation by supporting multiple keys and ensuring deterministic output through sorting. Feedback was provided to refine the scoping logic to correctly handle empty flagSetId strings and to optimize performance by utilizing the existing write transaction for queries instead of creating new read transactions.

Comment on lines +263 to +276
if fsi, ok := metadata["flagSetId"].(string); ok && fsi != "" {
seenFlagSetIds := map[string]struct{}{fsi: {}}
for id := range newFlags {
seenFlagSetIds[id.flagSetId] = struct{}{}
}
for fsi := range seenFlagSetIds {
sel := NewSelector(flagSetIdIndex + "=" + fsi).WithIndex(sourceIndex, source)
partial, _, _ := s.GetAll(context.Background(), &sel)
oldFlags = append(oldFlags, partial...)
}
} else {
sel := NewSelector(sourceIndex + "=" + source)
oldFlags, _, _ = s.GetAll(context.Background(), &sel)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The current implementation has two areas for improvement:

  1. Scoping for Default Selector: The check fsi != "" prevents scoping when the flagSetId is an empty string. Since flagd normalizes empty or missing flagSetIds to a default internal UUID (nilFlagSetId), skipping scoping for "" causes the "default" selector to fall back to source-scoped deletion. This may unintentionally delete flags from other selectors. Removing fsi != "" and relying on ok ensures consistent scoping for all selectors.
  2. Transaction Efficiency: Calling s.GetAll inside Update is suboptimal because it initiates new read transactions while a write transaction (txn) is already active. It is more efficient to use the existing txn directly. This also allows for proper error handling during the query phase.
	if fsi, ok := metadata["flagSetId"].(string); ok {
		seenFlagSetIds := map[string]struct{}{fsi: {}}
		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)
		}
	}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Flags with an empty-string flagSetId are stored as "", but NewSelector silently rewrites "" to nilFlagSetId, so removing the guard would make the query look for the wrong value and never find those flags.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Transaction Efficiency: Calling s.GetAll inside Update is suboptimal because it initiates new read transactions while a write transaction (txn) is already active. It is more efficient to use the existing txn directly. This also allows for proper error handling during the query phase.

Updated

@erenatas erenatas force-pushed the erenatas/support-per-selector-sync branch from 8937fd1 to 62080d5 Compare April 1, 2026 17:03
…sync

  When an upstream provider sends per-selector stream messages (each with
  a flagSetId in metadata), store.Update now only deletes stale flags
  within the same flagSetId(s), preserving flags from other selector.

Signed-off-by: Eren Atas <eren_atas@hotmail.com>
@erenatas erenatas force-pushed the erenatas/support-per-selector-sync branch from 62080d5 to 5bf055b Compare April 1, 2026 17:12
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 1, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant