Skip to content

fix(Segment membership): Zero out (segment, env) pairs that stopped matching#7600

Open
khvn26 wants to merge 4 commits into
mainfrom
fix/segment-membership-stale-counts
Open

fix(Segment membership): Zero out (segment, env) pairs that stopped matching#7600
khvn26 wants to merge 4 commits into
mainfrom
fix/segment-membership-stale-counts

Conversation

@khvn26
Copy link
Copy Markdown
Member

@khvn26 khvn26 commented May 26, 2026

Thanks for submitting a PR! Please check the boxes below:

  • I have read the Contributing Guide.
  • I have added information to docs/ if required so people know about the feature.
  • I have filled in the "Changes" section below.
  • I have filled in the "How did you test this code" section below.

Changes

Contributes to #5663.

refresh_project_segment_counts only upserts pairs returned by compute (count > 0), so a pair that previously matched but no longer does keeps its stale non-zero count forever. Append explicit zero rows for every (segment, env) pair already in the table that didn't appear in this run; pairs that have never matched stay absent.

How did you test this code?

Added test_refresh_project_segment_counts__previously_matching_pair_drops_to_zero__row_zeroed and test_refresh_project_segment_counts__never_matched_pair__no_row_written.

…atching

`refresh_project_segment_counts` builds the upsert list from
`compute_segment_counts_for_project`, which only returns pairs with at
least one match. Combined with `bulk_create(update_conflicts=True)` --
which only touches rows it's given -- a pair that previously matched but
no longer does keeps the stale non-zero count forever.

Saw this in practice on staging: editing a segment rule so that a
previously-matching env dropped to zero left the old count visible in
the UI through the next refresh cycle.

Append explicit zero-count rows for every (segment, env) pair the
project already has in the table but didn't appear in this run's
result. The next bulk_create then drives those down to zero. Pairs that
have never matched stay absent (no row pre-population).

Log the zeroed-row count alongside the existing membership_counts__count
on `refresh.project.completed` so this path is observable.

beep boop
@khvn26 khvn26 requested review from a team as code owners May 26, 2026 14:07
@khvn26 khvn26 requested review from emyller and removed request for a team May 26, 2026 14:07
@vercel
Copy link
Copy Markdown

vercel Bot commented May 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment May 26, 2026 4:01pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
flagsmith-frontend-preview Ignored Ignored Preview May 26, 2026 4:01pm
flagsmith-frontend-staging Ignored Ignored Preview May 26, 2026 4:01pm

Request Review

Copy link
Copy Markdown

@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 updates the segment membership count refresh task to explicitly zero out stale membership counts for segment-environment pairs that no longer match, preventing stale counts from lingering. It also adds corresponding unit tests and updates the observability documentation. The reviewer suggested a performance optimization to filter the existing pairs by count__gt=0 to avoid redundant database updates for rows that have already been zeroed out.

Comment thread api/segment_membership/tasks.py Outdated
Comment on lines +171 to +173
existing_pairs = SegmentMembershipCount.objects.filter(
segment__project=project,
).values_list("segment_id", "environment_id")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Currently, existing_pairs fetches all existing SegmentMembershipCount records for the project, regardless of their current count. This means that any pair that has already been zeroed out in a previous run will continue to be fetched, included in zeroed_counts, and updated via bulk_create on every subsequent run.

At scale, this results in a large number of redundant database writes (updates) for steady-state zero counts.

By filtering existing_pairs to only include records where count > 0, we ensure that we only zero out pairs that actually transitioned from a positive count to zero, avoiding redundant updates for already-zeroed rows.

        existing_pairs = SegmentMembershipCount.objects.filter(
            segment__project=project,
            count__gt=0,
        ).values_list("segment_id", "environment_id")

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agree, good bot. b60fd51.

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.

Sorry, probably lacking context, but is there a benefit to adding zero count records instead of doing something like:

SegmentMembershipCount.objects.exclude(...).delete()

Maybe we're planning on being able to show a history?

Copy link
Copy Markdown
Member Author

@khvn26 khvn26 May 26, 2026

Choose a reason for hiding this comment

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

Switched to delete in 8d259ad — same end state for the UI without keeping zero rows around.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

IDK if we should invest in history given we'll move from polling to CDC fairly soon.

Filter `existing_pairs` by `count__gt=0` so steady-state refreshes don't
re-write zeros over zeros. Per gemini-code-assist on #7600.

beep boop
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 26, 2026

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  44.6 seconds
commit  44983b7
info  🔄 Run: #17003 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  33.1 seconds
commit  b60fd51
info  🔄 Run: #17005 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  2 passed

Details

stats  2 tests across 2 suites
duration  41.3 seconds
commit  b60fd51
info  🔄 Run: #17005 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  39.2 seconds
commit  44983b7
info  🔄 Run: #17003 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  42.6 seconds
commit  8d259ad
info  🔄 Run: #17006 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  39.1 seconds
commit  8d259ad
info  🔄 Run: #17006 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  2 passed

Details

stats  2 tests across 2 suites
duration  1 minute, 11 seconds
commit  8d259ad
info  🔄 Run: #17006 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

passed  2 passed

Details

stats  2 tests across 2 suites
duration  32.1 seconds
commit  8d259ad
info  🔄 Run: #17006 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  32.1 seconds
commit  5da46f1
info  🔄 Run: #17010 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  43.6 seconds
commit  5da46f1
info  🔄 Run: #17010 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  3 passed

Details

stats  3 tests across 3 suites
duration  58.3 seconds
commit  5da46f1
info  🔄 Run: #17010 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  58.9 seconds
commit  5da46f1
info  🔄 Run: #17010 (attempt 1)

Comment thread api/segment_membership/tasks.py Outdated
Comment on lines +171 to +172
# `count__gt=0` keeps steady-state refreshes off rows that are already
# zeroed -- writing zero over zero on every run is just churn.
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.

Nit: this comment feels slop-y to me. It could just be:

Ignore records that already have `count=0` to avoid unnecessary writes. 

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Replaced with delete in 8d259ad, the slop comment is gone.

Comment thread api/segment_membership/tasks.py Outdated
Comment on lines +171 to +173
existing_pairs = SegmentMembershipCount.objects.filter(
segment__project=project,
).values_list("segment_id", "environment_id")
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.

Sorry, probably lacking context, but is there a benefit to adding zero count records instead of doing something like:

SegmentMembershipCount.objects.exclude(...).delete()

Maybe we're planning on being able to show a history?

@codecov
Copy link
Copy Markdown

codecov Bot commented May 26, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.51%. Comparing base (e39f9ae) to head (5da46f1).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #7600   +/-   ##
=======================================
  Coverage   98.51%   98.51%           
=======================================
  Files        1436     1436           
  Lines       54363    54405   +42     
=======================================
+ Hits        53553    53595   +42     
  Misses        810      810           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Per matthewelwell on #7600: zero-out rows just for the next refresh to
read them isn't useful — we're not surfacing history anywhere — so drop
them outright. The frontend already renders no chip when a membership
row is absent, which is the same UX the explicit zero was reaching for.

beep boop
@github-actions github-actions Bot added api Issue related to the REST API docs Documentation updates labels May 26, 2026
@github-actions github-actions Bot added fix and removed docs Documentation updates labels May 26, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 26, 2026

Docker builds report

Image Build Status Security report
ghcr.io/flagsmith/flagsmith-e2e:pr-7600 Finished ✅ Skipped
ghcr.io/flagsmith/flagsmith-api-test:pr-7600 Finished ✅ Skipped
ghcr.io/flagsmith/flagsmith-frontend:pr-7600 Finished ✅ Results
ghcr.io/flagsmith/flagsmith-api:pr-7600 Finished ✅ Results
ghcr.io/flagsmith/flagsmith:pr-7600 Finished ✅ Results
ghcr.io/flagsmith/flagsmith-private-cloud:pr-7600 Finished ✅ Results

@khvn26 khvn26 requested a review from matthewelwell May 26, 2026 14:52
Zaimwa9
Zaimwa9 previously approved these changes May 26, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 26, 2026

Visual Regression

19 screenshots compared. See report for details.
View full report

Comment thread api/segment_membership/tasks.py Outdated
Comment on lines +165 to +166
# Drop pairs that stopped matching this run so the next refresh
# doesn't carry stale counts forward.
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.

This feels slop-y again? I think it's referencing old context about 'carrying stale counts forward'

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Dropped the comment in 5da46f1 — the code is self-evident now.

Comment thread api/segment_membership/tasks.py Outdated
Comment on lines +177 to +181
stale_deleted, _ = (
SegmentMembershipCount.objects.filter(id__in=stale_ids).delete()
if stale_ids
else (0, {})
)
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.

I suspect this would work the same, no? Surely 0, {} is the default response from .delete() and we're already working with a list, not None.

Suggested change
stale_deleted, _ = (
SegmentMembershipCount.objects.filter(id__in=stale_ids).delete()
if stale_ids
else (0, {})
)
stale_deleted, _ = SegmentMembershipCount.objects.filter(id__in=stale_ids).delete()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Right — .delete() on an empty id__in=[] filter already returns (0, {}) without issuing SQL. Adopted the suggestion in 5da46f1.

Per matthewelwell on #7600: comment was carrying old context, and the
`if stale_ids else (0, {})` guard is unnecessary since `.delete()` on
an empty `id__in` filter already returns `(0, {})` without issuing
SQL.

beep boop
@github-actions github-actions Bot added docs Documentation updates and removed fix docs Documentation updates labels May 26, 2026
@github-actions github-actions Bot added the fix label May 26, 2026
@khvn26 khvn26 requested a review from matthewelwell May 26, 2026 16:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Issue related to the REST API fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants