fix(segments): use REPEATABLE READ isolation for segment cache queries #6525
+330
−25
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This fix was developed using Codex and Claude. Please review with extra care.
Problem
There is a race condition in segment PATCH updates that can cause incorrect flag evaluations. When a segment is updated via PATCH, the
drf_writable_nestedlibrary processes nested rules and conditions in multiple steps within the serializer. Meanwhile, the segment read path (get_segments_from_cache) uses Django'sprefetch_relatedwhich executes multiple sequential queries - one for segments, one for rules, one for conditions, etc.The Race Condition
If a PATCH request commits between the read's prefetch queries, the reader can observe an inconsistent state:
The flag engine evaluates rules with empty conditions as
True:This causes any identity to incorrectly match the segment during the race window, potentially enabling features for users who should not have them.
Reproduction
An integration test was added that reproduces this bug reliably:
api/tests/integration/environments/identities/test_integration_segment_patch_atomic.pyThe test runs two concurrent loops for ~10 seconds:
enabled: trueresponsesBefore the fix, the test fails within seconds as the poll loop catches the race condition.
Solution
Use REPEATABLE READ transaction isolation level for PostgreSQL when fetching segments with
prefetch_related. This ensures all prefetch queries see the same database snapshot, preventing the race condition.Changes
environments/models.py-Environment.get_segments_from_cache():transaction.atomic()REPEATABLE READisolation level for PostgreSQLprojects/services.py-get_project_segments_from_cache():QuerySet[Segment]tolist[Segment]to ensure queries execute within the transactiontests/unit/projects/test_unit_projects_models.py:len()instead of.count())Why This Works
With REPEATABLE READ isolation, all queries within the transaction see data as it existed at the start of the transaction. Even if a PATCH commits during the read:
PostgreSQL-Specific
The fix only applies REPEATABLE READ for PostgreSQL (
connection.vendor == "postgresql"). Other databases continue using their default isolation level. This is acceptable because:Testing
Test Plan
get_segments_from_cachepass🤖 Generated with Claude Code