Add single-flight dedup to $withCache#5862
Conversation
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
829d076 to
e982b66
Compare
|
Heads up for reviewers: the 2 workflows ( Commit is GPG-signed and verified. Branch is up to date with |
|
Nice work on the Cache-level placement - putting One behavior worth documenting: if Do you want to add a test that verifies error propagation to concurrent waiters, or document the put-error semantics in the PR body? |
|
@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. |
7fec249 to
7281d19
Compare
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