Skip to content

fix: require transactional query context for createOrGetExistingAsync utility method#628

Draft
wschurman wants to merge 1 commit into
mainfrom
05-28-fix_require_transactional_query_context_for_createorgetexistingasync_utility_method
Draft

fix: require transactional query context for createOrGetExistingAsync utility method#628
wschurman wants to merge 1 commit into
mainfrom
05-28-fix_require_transactional_query_context_for_createorgetexistingasync_utility_method

Conversation

@wschurman
Copy link
Copy Markdown
Member

@wschurman wschurman commented May 28, 2026

Why

One class of issue in entity is concurrent calls to load/create methods caching stale data in dataloaders/redis when done outside a transaction (otherwise dataloaders are scoped to transaction and cache adapter is skipped). This can happen when multiple calls to blocks of code like createOrGetExistingAsync happen in parallel without an explicit transactional query context, within which it would negatively cache in one call and then incorrectly retrieve that negative cache from another call depending on race condition ordering.

Concretely, one such issue with createOrGetExistingAsync is reproducible by calling it with the same set of args:

const [a, b] = await Promise.all([
  createOrGetExistingAsync(...args),
  createOrGetExistingAsync(...args),
]);

This will throw due to the ordering: both calls will miss initially, both will create, one will hit unique constraint error (handled), that one will try to re-load, but will see the negatively-cached result in dataloader.

How

While the class of bug can happen with really any non-transactional sequence of loads/mutations, entity as a library should do its best to prevent it from occurring in blessed utility methods. The only way to make this deterministic is via requiring a transactional query context be supplied to createOrGetExistingAsync.

Reasoning for why this is likely the right call path. Most creations in applications should already be within transactions, and those that aren't automatically start a transaction in the mutator to run the triggers and create within the same transaction. This change makes the semantics of this method more explicit without much loss of usability and protects against the race that is likely.

Test Plan

ToDo

Copy link
Copy Markdown
Member Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (dd8c710) to head (aea0513).

Additional details and impacted files
@@            Coverage Diff            @@
##              main      #628   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          109       109           
  Lines        17743     17733   -10     
  Branches      1539      1522   -17     
=========================================
- Hits         17743     17733   -10     
Flag Coverage Δ
integration 28.63% <0.00%> (+0.01%) ⬆️
unittest 94.52% <100.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ 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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant