fix(query-orchestrator): collect QueryOrchestrator.test.js and fix the two bugs it had stopped catching - #11481
Conversation
…e two bugs it had stopped catching
|
Claude finished @igorlukanin's task in 4m 47s —— View job Review: solid, well-diagnosed fix — 0 blocking, 2 medium, 3 low (5 inline comments)Full review
VerdictThe diagnosis holds up under reading. I traced both fixes independently: The cache fix is real and correctly targeted. The queue fix is real and the
I could not run the suites: Findings
Also raised as a question, not a defect (inline on Praise
No security concerns: no new input paths, no user-controlled data reaching a sink; the added md5 is a cache discriminator, not a security primitive. |
…ueryHash round-trip, and bound retained results
|
Claude finished @igorlukanin's task in 2m 46s —— View job Follow-up review: all 5 prior findings addressed — 0 blocking, 1 low (1 inline comment)Full review
Verdict
All five of my prior threads were already resolved by the author, so there was nothing stale to clean up and nothing to re-affirm. Findings
One non-finding, for the record: the I could not run the suites: Praise
|
|
Claude finished @igorlukanin's task in 1m 6s —— View job Follow-up review of
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #11481 +/- ##
==========================================
+ Coverage 59.10% 61.21% +2.10%
==========================================
Files 223 223
Lines 17891 17918 +27
Branches 3636 3640 +4
==========================================
+ Hits 10575 10968 +393
+ Misses 6795 6427 -368
- Partials 521 523 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
packages/cubejs-query-orchestrator/test/unit/QueryOrchestrator.test.js— 51 tests over pre-aggregation partitioning, lambda partitions, streaming and index handling — has not run since October 2025.The package adopted
jest.base-ts.config.jsin #10037, which setstestMatch: ['<rootDir>/test/**/*.test.ts']..tsonly, so the.jssuite silently stopped being collected, andyarn unit(and thereforeyarn lerna run unit) reported success having run only the.tssuites. The file kept being edited since — most recently in #11050 — by people reasonably assuming it ran.Confirm on master with
npx jest --listTestsin the package: the.jsfile is absent.What had broken behind it
Collecting the suite gives 47 passed, 4 failed. All four are pre-existing and reproduce deterministically, and they turned out to be two real bugs, not stale expectations. Both landed after collection stopped, which is exactly why nothing caught them.
1. A cached result could be served for a different query (
QueryCache)Fixes 2 of the 4 (
index is part of query key,lambda partitions).#10489 added an
isSameRequestshort-circuit so that amust-revalidatequery whoserefreshKeymoves mid-flight returns the cached result and refreshes in the background, instead of looping on continue-wait. It decides "is this my own continue-wait cycle?" fromrequestIdalone.But the result-cache key (
QueryCache.queryCacheKey) is[query, values, preAggregations.map(p => p.loadSql)]. It deliberately omitsindexesSqland the matched time-dimension range — the very things that change which physical pre-aggregation table gets resolved. So two queries in one request flow that differ only outside that key collide on one cache entry, and the second is served the first's result, including its embedded table names. The branch'sfetchNew()is fire-and-forget, so the correct result is computed, cached, and its return value dropped.This is not staleness within a refresh interval: the pre-aggregation layer resolves and executes the correct freshly-built table, and the cache then returns a result computed against a different one. Reachable whenever a rollup gains or loses an index, or the same query is re-issued with a different matched time-dimension range, under
must-revalidatewith a fast-movingrefreshKey.Fix: stamp the entry with a
queryHashand require it to match.requestIdestablishes the request flow; it says nothing about which query — the hash supplies the half of the predicate the branch already intended. #10489's behaviour is preserved exactly, since a genuine continue-wait retry re-runs the same query and still takes the branch; when the query differs, control falls through to the pre-existing renewal branch, which underwaitForRenewblocks and returns the correct result. Entries written before the field existed carry no hash and are accepted, so deploying this cannot trigger a revalidation storm.#10489's own regression tests pass either way, because they hand-construct entries and never exercise two different resolved queries under one
requestId— that gap is why this shipped. Addedsame request + different query: must block on fetchNewto close it.2. A lost wakeup in the in-memory queue driver (
LocalQueueDriver)Fixes the other 2 (
range partitions,empty partitions with externalRefresh), which threwContinueWaitError.Requests coalesce onto one queue key, but a request that joins an already-
activekey takes theadded = 0path inaddToQueueand registers no interest of its own. It only callsgetResultBlockinglater — by which point the first waiter has consumed the result and deleted the promise, andsetResultAndRemoveQueryhas deletedqueryDef. Both maps are empty, so the straggler hits the early-null branch and getsContinueWaitErrorinstead of a result that was computed successfully.Fix: retain the completed result briefly (15s, capped at 1000 entries) in a map kept separate from
resultPromises, and read from it on that early-null path. The separation matters:resultPromisesdoubles as "a query is in flight on this key", so holding a resolved promise there would hand the next query on the same key an instantly-resolved stale result — there's a regression test for exactly that. A cancelled or orphaned query clears its retained result, so a cancel can't serve one either.Scope:
CubeStoreQueueDriveris not affected — it keysRESULT_BLOCKINGon the server-sidequeueId, and a duplicateaddToQueuereturns the existing job's id, so a late waiter still blocks on the real job. Production defaults to CubeStore (detectQueueAndCacheDriver), andContinueWaitErroris normal handled control flow that the client retries. So the practical impact is dev-mode /CUBEJS_CACHE_AND_QUEUE_DRIVER=memory, where a high-partition-count pre-aggregation causes avoidable continue-wait churn — a latency defect, not a correctness one. Fixing it also removes a dev/prod behavioural divergence.Keeping it collected
test/unit/TestCollection.test.tsreadsjest.config'stestMatch, walkstest/for*.test.[tj]s, and asserts nothing on disk is uncollected — the actual invariant, which generalises past this one file. A count assertion would rot on every added test, and a--listTests | grepshell step is easy to render vacuously green. It also fails loudly on an unsupported glob construct and on a walk that finds nothing, rather than passing vacuously.Notes
testMatchwidening is in the package config, not the sharedjest.base-ts.config.js. Widening the shared base would pull.jssuites into every package extending it, a much larger blast radius than this change needs.tsconfig.jsonalready hadallowJs: trueandinclude: ["src", "test"], sotschas been compiling this file all along; only jest wasn't running it. That's why it kept type-checking clean while never executing.cubejs-api-gatewayhas a different shape of the same class of problem: itsunitscript isjest ... dist/test, so CI targets built output exclusively and its two source.jstests never run (and don't parse). Left alone here; tracked separately.Testing
test/unitin this package: 7 suites, 124 tests, all green (from 6 suites / 120 tests with 6 failing).QueryOrchestrator.test.js51/51, verified stable over 5 consecutive runs.tsc -p tsconfig.jsonclean;eslint src/* test/*clean (0 errors, and 2 fewer warnings than master).