Skip to content

fix: batch-load waffle flags to avoid per-flag Redis N+1 - #14

Merged
rbarbaresco merged 3 commits into
mainfrom
n+1-flag-evaluation-01
Jul 23, 2026
Merged

fix: batch-load waffle flags to avoid per-flag Redis N+1#14
rbarbaresco merged 3 commits into
mainfrom
n+1-flag-evaluation-01

Conversation

@rbarbaresco

@rbarbaresco rbarbaresco commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Motivation and context for the change

Alice GET /api/frontend-settings/ was still slow in Datadog APM (~250ms avg, ~550ms p95) with dozens of repeated Redis GETs on waffle:2.7.* keys per request.

Why PR #11 did not fix this: that change only removed the SQL N+1 on request.user.groups (one auth_user_groups query per flag). It left the evaluation loop as “list flag names, then call waffle.flag_is_active(request, name) once per name.” Each call does Flag.get(name) → one Redis GET (and on miss, a DB get). For user/group-targeted flags, waffle’s _get_user_ids / _get_group_ids can add more per-flag Redis GETs. So after #11, Postgres group lookups were bounded, but per-flag Redis round-trips remained — that is what Datadog still showed.

Why this PR fixes it: we stop resolving flags by name. We load matching flag rows in one queryset with prefetch_related("users", "groups"), then call flag.is_active(request) on those instances. That removes N× Flag.get(). We also bind _get_user_ids / _get_group_ids to the prefetched id sets so membership checks stay in memory and skip waffle’s per-flag Redis M2M caches. PR #11’s request-local user-groups cache is kept.

Review path: frontend_settings.views.settingsget_flags_request_with_cached_user_groups → batch queryset + prefetch → _evaluate_flags / _bind_prefetched_membershipflag.is_active.

A clear description of the change

  • Refactor get_flags to batch-load FRONTEND_* (configurable prefix) flags with prefetch_related("users", "groups") instead of values_list("name") + flag_is_active per name.
  • Bind prefetched users/groups membership onto each flag before is_active, so waffle evaluation does not hit Redis/DB per flag for M2M.
  • Keep the existing cached-user-groups adapter (PR fix: avoid N+1 group queries in get_flags #11 behavior), extracted as _request_with_cached_user_groups.
  • Add regression tests that assert a single primary waffle_flag SELECT, bounded M2M prefetches, and that total query count does not grow linearly with flag count.

Testing

  • The change is covered with automated tests

Testing instructions

Rollback

  • The change can be automatically rolled back

Rollback instructions

Revert this PR / release and pin consumers (e.g. Alice) back to the previous django-frontend-settings version.

Summary by CodeRabbit

  • Performance
    • Improved feature-flag evaluation efficiency by reducing database queries.
    • Prevented query counts from increasing linearly as more flags are configured.
  • Bug Fixes
    • Improved loading of feature-flag user and group memberships during evaluation.
    • Added regression coverage to ensure efficient flag evaluation behavior.

Evaluate FRONTEND_* flags from a single prefetched queryset instead of
calling flag_is_active (Flag.get) once per flag. Bind prefetched
users/groups membership so is_active skips Redis M2M lookups.

Co-authored-by: Cursor <cursoragent@cursor.com>
@rbarbaresco
rbarbaresco marked this pull request as ready for review July 23, 2026 16:20
@rbarbaresco
rbarbaresco requested a review from a team as a code owner July 23, 2026 16:20
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: loadsmart/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7e3e99c3-bdd9-432b-964f-750d1b18c6d7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

get_flags now evaluates prefetched waffle flag objects using cached request group IDs and bound membership sets. New tests verify primary and membership queries remain bounded as the number of matching flags increases.

Changes

Flag evaluation optimization

Layer / File(s) Summary
Prefetched flag evaluation
frontend_settings/utils.py
Request group caching, prefetched waffle memberships, and flag.is_active(request) evaluation replace per-flag name lookups and activation calls.
Query-bound regression coverage
tests/test_views.py
SQL-capture tests verify one primary flag query and bounded membership queries, including when the number of matching flags increases.

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states the main change: batch-loading waffle flags to eliminate per-flag Redis N+1 work.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch n+1-flag-evaluation-01

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (1)
frontend_settings/utils.py (1)

56-74: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Add a regression test for the waffle hook contract This depends on waffle’s private _get_user_ids / _get_group_ids methods; a small test will catch an upgrade that changes that contract and silently drops back to the slower path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend_settings/utils.py` around lines 56 - 74, Add a regression test
covering _bind_prefetched_membership that verifies the bound _get_user_ids and
_get_group_ids hooks return the prefetched user and group ID sets without
invoking the original lookup methods. Use a flag with prefetched memberships and
ensure the test fails if waffle removes or changes either private method
contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@frontend_settings/utils.py`:
- Around line 56-74: Add a regression test covering _bind_prefetched_membership
that verifies the bound _get_user_ids and _get_group_ids hooks return the
prefetched user and group ID sets without invoking the original lookup methods.
Use a flag with prefetched memberships and ensure the test fails if waffle
removes or changes either private method contract.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: loadsmart/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0e77fb71-a0c5-4b74-9405-13c21a62c410

📥 Commits

Reviewing files that changed from the base of the PR and between 5a6fb27 and 096bbe7.

📒 Files selected for processing (2)
  • frontend_settings/utils.py
  • tests/test_views.py

rbarbaresco and others added 2 commits July 23, 2026 13:36
Rewrite _select_queries_matching without a line-broken binary operator
so CI lint (flake8 --ignore=E501) passes.

Co-authored-by: Cursor <cursoragent@cursor.com>
Wrap the long function signature so CI black --check passes.

Co-authored-by: Cursor <cursoragent@cursor.com>
@rbarbaresco
rbarbaresco merged commit 204a20d into main Jul 23, 2026
11 checks passed
@rbarbaresco
rbarbaresco deleted the n+1-flag-evaluation-01 branch July 23, 2026 17:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants