Skip to content

Add single-flight dedup to $withCache#5862

Open
mohamedramadan14 wants to merge 2 commits into
drizzle-team:mainfrom
mohamedramadan14:fix/cache-single-flight
Open

Add single-flight dedup to $withCache#5862
mohamedramadan14 wants to merge 2 commits into
drizzle-team:mainfrom
mohamedramadan14:fix/cache-single-flight

Conversation

@mohamedramadan14

Copy link
Copy Markdown

On a cold cache, concurrent SELECTs sharing the same key all miss and each fire their own DB query. The number of queries scales with concurrency, not cache misses. Most cache backends used with $withCache do not promise single-flight, so the dedup has to live in the ORM.

Fix: add an inFlightQueries map on the Cache instance. On miss, the first caller runs query() and put() inside a shared promise; concurrent callers for the same key return that same promise. The entry is removed on settle, so a later call after the cache is populated behaves as before. put() stays inside the in-flight chain so every caller observes the cached value once it is written.

Scope: same change applied to pg-core, mysql-core, sqlite-core, singlestore-core, gel-core.

Test: a concurrent stampede test added to each dialect cache suite. 20 parallel $withCache calls on a cold cache assert exactly one put() and 20 get()s.

Fixes #5841

On a cold cache, concurrent SELECTs sharing the same key all miss and each fire their own DB query, multiplying load by concurrency. Cache backends do not guarantee single-flight, so the dedup lives on the Cache instance.

Cache now owns an inFlightQueries map keyed by cache key. On miss, the first caller runs query() and put() inside a shared promise. Concurrent callers for the same key return that same promise. The entry is removed on settle, so a later call after the cache is populated behaves as before. put() stays inside the in-flight chain so all callers observe the cached value once it is written.

A concurrent stampede test is added to each dialect cache suite and asserts exactly one put() across N parallel  calls.

Fixes drizzle-team#5841
@mohamedramadan14 mohamedramadan14 force-pushed the fix/cache-single-flight branch from 829d076 to e982b66 Compare June 8, 2026 11:50
@mohamedramadan14

Copy link
Copy Markdown
Author

Heads up for reviewers: the 2 workflows (release (drizzle-orm), release (drizzle-zod)) show as "Required / Waiting for status to be reported" because they need a maintainer click to approve first-time contributor runs. Not specific to this PR — same gate is showing on #5861 (ClankOS) and any other external PR. The "not authorized to push" line is the standard branch protection on main, also expected for first-time contributors.

Commit is GPG-signed and verified. Branch is up to date with main. Ready for review whenever a maintainer has a cycle.

@Nexory

Nexory commented Jun 8, 2026

Copy link
Copy Markdown

Nice work on the Cache-level placement - putting inFlightQueries on the base class rather than per-PreparedQuery is the right scope; it deduplicates across all PreparedQuery instances sharing the same db.$cache, which is the actual stampede scenario.

One behavior worth documenting: if cache.put() throws inside the IIFE, the IIFE rejects and all N concurrent waiters receive the put error even though query() already succeeded. The DB round-trip is wasted for every waiter. Pre-fix, a put failure only affected the single caller that triggered it. This matches the pattern suggested in #5841, so it is not a regression introduced here - but it is a silent behavioral change under a code path that could be surprising in production.

Do you want to add a test that verifies error propagation to concurrent waiters, or document the put-error semantics in the PR body?

@mohamedramadan14

Copy link
Copy Markdown
Author

@Nexory I think you are right. If put fails, the promise rejects and every waiter gets that error — even though the query itself worked fine. The DB work is wasted for everyone in that scenario.

Added a test that covers this: mock put to reject once, fire 5 concurrent calls, assert all 5 receive the same error object. This documents the behavior and makes sure future changes don't accidentally break it.

It's a tradeoff of single-flight dedup — you get the stampede protection but lose the ability to return partial results when put fails. Matches the pattern suggested in #5841, so it's not a regression, just an edge case worth being explicit about.

@mohamedramadan14 mohamedramadan14 force-pushed the fix/cache-single-flight branch from 7fec249 to 7281d19 Compare June 9, 2026 03:44
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.

Enhancement request: add single-flight deduplication to $withCache to avoid cache-stampede under concurrent load

2 participants