perf(core): make wait_wal_table(), sleep() functions lightweight and not block IO threads - #7031
Conversation
…m many connections
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…wait-wal-table-func
…wait-wal-table-func
…wait-wal-table-func
Built from win64svc commit: f809829 Build timestamp: 2026-05-06 11:47:51 UTC Toolchain: MinGW-w64 on Windows Server 2022 GitHub Action: https://github.com/questdb/questdb/actions/runs/25433270391
enable logging paranoia for CI
…wait-wal-table-func Conflicts: core/src/main/java/io/questdb/cairo/CairoEngine.java
bluestreak01
left a comment
There was a problem hiding this comment.
@ideoma — review from the review-pr skill at --level 3 (independent pass with per-finding source verification against the current branch state).
The load-bearing surface (continuation runtime, CarrierLocal/CarrierIdentity + Rust FFI, the Worker driver + job rotation, SeqTxnTracker, wait_wal_table/sleep) is unusually well-engineered and documented. The CAS state machines, the size==0 lost-wakeup fast path, the Rust FFI panic/ABI safety, and the CarrierLocal id-recycle ordering are all correct. But the job-rotation recycle machinery is inert during normal operation, which turns the advertised feature into an unbounded native-memory leak on the REST path.
Critical
C1. Unbounded selector + HTTP-handler leak on the REST path — the rotation recycle pool is never replenished except at pool halt (in-diff)
Locations:
core/src/main/java/io/questdb/mp/Worker.java:236,267—recycleJobList(cont)is reachable only from the twocont.isDone()branches.core/src/main/java/io/questdb/mp/Worker.java:326-420—loopBodyhas noreturn/breakexcept thewhile (lifecycle.get() == RUNNING)guard, so a continuation reachesisDone()only at pool halt.core/src/main/java/io/questdb/mp/Worker.java:443-461—mintNextGencold-allocates and appends every stateful clone toownedJobClones(line 459).core/src/main/java/io/questdb/mp/WorkerPool.java:209—ownedJobClonesis freed only byhalt().core/src/main/java/io/questdb/cutlass/http/HttpServer.java:435-470, 524-526, 610-614—HttpRequestJob.cloneInstance()→selectorFactory.acquire()→create()mints a freshHttpRequestProcessorSelectorImplpopulated with the entire REST handler set (factory.newInstance()per registered URL:JsonQueryProcessor, import/export processors, etc.).recycleInstance()→selectorFactory.release()is the only thing that refillsrecyclePool.core/src/main/java/io/questdb/griffin/engine/functions/date/SleepFunctionFactory.java:198-214—sleepre-suspends once perwakeIntervalMillis(default 1000ms);WaitWalFunction.getBoolre-parks the same way.
Trace.
- A continuation wraps the worker's entire
loopBodyloop, not a single query.loopBodyonly returns when the worker isHALTED, socont.isDone()is never true duringRUNNING. recycleJobList()— the sole feeder ofWorker.snapshotPooland the sole caller ofJob.recycleInstance()— runs only from theisDone()branches. Therefore duringRUNNINGit never runs:snapshotPoolstays empty andHttpRequestProcessorSelectorFactory.recyclePoolis never replenished.- Every time a body deep-parks (a
wait_wal_table/sleepsuspend()), the outer driver takes thehandoff == nullpath and callsmintNextGen. WithsnapshotPoolempty,mintNextGenalways cold-allocates andcloneInstance()→acquire()→create()builds a fresh selector + full handler set, thenownedJobClones.add(clone)pins it (strong ref) until halt. - Magnitude is per-wake, not per-call:
sleep(3600)re-parks ~3600 times (one per 1s wake), so a single hour-long RESTsleepleaks ~3600 complete HTTP handler sets.wait_wal_tableagainst a lagging writer behaves identically.
The HttpRequestProcessorSelectorFactory comment ("steady-state rotation amortises to 'pop a selector from the queue' instead of allocating a fresh selector + handler set per suspend") and DESIGN_NOTES.md ("pool size converges to the workload's concurrent-suspend count") are false: the pool is write-only at halt, so steady state always allocates. Tests miss it because everything is freed at halt, before assertMemoryLeak's native check, and result assertions are insensitive to heap growth.
Impact. Unbounded native + heap growth on the HTTP/REST interface, proportional to the number of wait_wal_table/sleep suspend episodes, reclaimed only on server shutdown — i.e. triggered by exactly the long-poll/monitoring workloads these functions exist for. PGWire is unaffected (its dispatcher Job uses the default cloneInstance() returning this, so clones are not pinned), and the shared/ILP pools never run suspending functions, so the leak is HTTP-specific — but the REST path is a primary QuestDB interface.
Suggested fix. The recycle pool must be replenished during RUNNING, not only at halt. Options: (a) when a deep-parked body is resumed and completes its current job iteration, return the abandoned generation's stateful clones to the pool at that point rather than waiting for cont.isDone(); (b) decouple selector lifetime from cont/gen lifetime — acquire a selector lazily per run() and release it back to recyclePool at the end of each non-suspending iteration; (c) at minimum, bound ownedJobClones. Until fixed, the steady-state-amortisation claims in HttpServer and DESIGN_NOTES.md are incorrect and should not ship.
Moderate
M2. Shutdown is not fenced behind a guaranteed ContinuationQueue drain (in-diff)
ServerMain.close() calls engine.signalClose() then immediately workerPoolManager.halt(). signalClose() → timerShards.shutdown() only fires parked entries (cont.scheduleResume() enqueues into the per-pool ContinuationQueue) and returns; it does not wait for any cont to remount and unwind. The resume queue is drained only inside Worker.loopBody's tryDequeue, gated by lifecycle == RUNNING. If halt() flips workers to HALTED before they dequeue a just-fired cont, those conts are stranded in ContinuationQueue and the in-flight connection/query native resources they own (socket fd, native buffers, any open TableReader) are never released. Bounded and shutdown-only (harmless at process exit; OS reclaims), but it matters for embedded/repeated-engine-restart use and would surface as an intermittent native leak in leak-sensitive tests. The CairoEngine.close() assert (closing || timerShards.size() == 0) covers the timer heap but not the resume queue. Consider a bounded "drain ContinuationQueue until empty (or deadline) while workers RUNNING" step before halt(), or document the residual as accepted-at-process-exit.
M3. TimerShards.shutdown()/halt() ordering can silently skip the drain (in-diff)
TimerShards.halt() never drains; shutdown() early-returns without draining when !running. So if halt() is ever called before shutdown() — both are documented as idempotent and order-independent — the entry-drain (entry.shutdown() on each parked waiter) is silently skipped. Production is safe today because CairoEngine always drives shutdown() (via signalClose) before halt() (via close), and the partial-init path is guarded by the assert in CairoEngine.close(). But the class Javadoc advertises an order-independence the implementation doesn't honor. Make shutdown() always drain (the toArray/clear path is heap-safe regardless of daemon state), or have CairoEngine.close() call shutdown() before halt().
M4. Job.run() Javadoc contradicts the implementation about workerId identity (in-diff)
Job.java:107-117 states "workerId is the calling carrier's globally-unique id (see CarrierIdentity)… The Worker loop body fills this argument with CarrierIdentity.current() at every call site." The implementation passes the pool-local id: Worker.java:337 reads currentWorkerId = WORKER_ID.get() (the pool-local CarrierLocal<Integer>) and passes that to myJobs.get(i).run(currentWorkerId, …). Pool-local is the correct choice (it's what PerWorkerLocks expects), but the Javadoc asserts the opposite and a non-existent uniqueness guarantee. This is the exact misunderstanding that enables M5.
M5. Latent cross-carrier workerId aliasing (in-diff design risk; not demonstrably reachable today)
Because currentWorkerId is captured in the frozen frame, when a body suspends mid-iteration on carrier A (pool-local id a) and resumes on carrier B, the remainder of that iteration runs on B's thread with workerId == a, while A's fresh continuation also runs workerId == a. Two carriers then hold the same pool-local workerId concurrently. The mint-new-generation policy isolates per-job-instance state, but workerId-indexed global state (PerWorkerLocks, per-worker scratch arrays, RingQueue single-producer slots) is not isolated. I could not construct a reachable corruption with the current suspending functions — wait_wal_table/sleep are runtime-constant/table functions evaluated on the IO worker that do not touch pool-local-workerId-indexed shared scratch — so this is not a live bug. But it is a real hazard for any future suspending function, and M4's incorrect Javadoc makes it likely someone trips it. The design docs acknowledge migration but do not enumerate which Jobs are safe under cross-carrier remount; that enumeration should exist.
M6. TxnWaiter.cancel() is an unconditional write but its contract describes a CAS (in-diff)
TxnWaiter.cancel() (:121-123) is state = STATE_CANCELLED; (void, no CAS), yet its own Javadoc (:100-120) and DESIGN_NOTES.md describe a tryCancel that "CASes PENDING → CANCELLED… on iteration 2+ tryCancel's CAS fails harmlessly." There is no CAS; on the happy path it clobbers FIRED → CANCELLED. The current behavior is benign (after the finally the body is already unwinding and never re-suspends, so no resume is lost), but the safety argument the design leans on is false and one refactor away from breaking. Make it a real CAS matching the doc, or rewrite the doc + DESIGN_NOTES to justify an unconditional terminal write.
M7. Per-iteration FFI downcall added to the hot worker loop, including legacy pools (in-diff, perf)
Worker.loopBody:337 reads WORKER_ID.get() every loop iteration — a CarrierLocal.get() → CarrierIdentity.current() critical FFI downcall plus an open-addressed map lookup — replacing master's plain workerId field read. This runs on every pool, including legacy ILP TCP IO/writer pools that never migrate and gain nothing. It is a measurable per-iteration regression on the busy-spin/cheap-job path. On legacy pools (continuationQueue == null) use the stable workerId field directly; only non-legacy pools need the carrier-aware read.
M8. Test coverage gaps in the new concurrency machinery (in-diff)
- The carrier-pinned / suspend-refused fallback (
suspend()returnsfalse→markParkRefused/consumeParkRefusedphantom-dequeue protocol) is entirely untested — no test forcessuspend()to returnfalse. This is the trickiest path and a regression there (peer busy-spin or lost/duplicated resume) would slip through. - The
Jobrecycle-on-completion path (attachJobs/takeJobs/recycleInstance) has no focused test — and given C1, a test that asserts the recycle pool actually refills during operation would have caught the leak. ServerMainSleepTest.testFuzzConcurrentSleepscomputes its anti-serialization metric (sumSleptMillisvsbusyWallMillis) but never asserts it. Add the assertion or soften the comment.
Minor
ColumnTypeConverterhas no registeredCarrierLocalcleaner despiteDESIGN_NOTES.md:225-238naming it as the example requiring one. Harmless today (every conversion eagerly unmaps viadetachFdClose()infinally), but the doc tells future authors to rely on a cleaner that doesn't exist. Register a cleaner or correct the note.WaitWalTableSeqTxnFunctionFactoryis missing fromfunction_list.txt. Harmless — that file is only an ordering map; discovery is by classpath/module scan, and the 2-arg form has no overload-ordering conflict. Cosmetic inconsistency only (the PR did addSleepFunctionFactory).AsyncLogRecord.getDejaVu()is dead code (zero callers) with a comment claiming the writer thread clearsdejaVu— it never does. TheAsyncLogRecord(Clock, name)nameparameter is also dead. Delete both.- Side-effecting assertion
assert recyclePool.pop() == nullinHttpServer.java:607—pop()mutates under-ea. Invariant holds, but prefer a non-destructive peek. SeqTxnTracker.initTxns()advanceswriterTxnwithoutfireWaiters()(unlikeupdateWriterTxns). Bounded extra latency (≤ wake interval), not a hang — worth a comment.CarrierLocal.set(null)no longer re-runs the factory (diverges from the oldio.questdb.std.ThreadLocal, matchesjava.lang.ThreadLocal). Dormant today, but a trap for future call sites.- Test nits:
SeqTxnTrackerTestimports not alphabetized;TimerShardsTest.testShardDistributiondead empty loop +terminalCount.get() == 1 ? 1 : 0assertion that hides the real value;ServerMainSleepTestcomments contradict the actual config.
Downgraded (false positives / already resolved on the branch)
@TestOnlyonSeqTxnTracker.getWriterTxn(): already fixed —@TestOnlyis ongetWaiterRegistrationCount();getWriterTxn()is unannotated..returnsOnce(...)on a deterministic query: not present on the current branch; the only.returns(...)use is correct.- Shared
IODispatcherrunning concurrently on two carriers: verified safe — dispatch is via anMCSequence(multi-consumer, by design), the epoll loop is aSynchronizedJob, and per-handler scratch is isolated by per-generation selector cloning. DelayHeap.take()infinitewait(0,0): unreachable —delay <= 0is handled separately, so whenmillis == 0thennanos > 0.CairoExceptionallocating per throw: not a regression — matches prior-eabehavior, no caller depends on instance reuse/identity, and it removes a real catch-rethrow aliasing hazard.fireWaiterssize==0vsregisterWaiterlost wakeup, double-resume / concurrent two-worker mount,CarrierLocalid-recycle window, Rust FFI panic/ABI safety: all verified correct.
Summary
Verdict: request changes — one confirmed Critical (C1) blocks merge.
C1 is an unbounded native + heap leak on the HTTP/REST path: the job-rotation recycle pool is replenished only at pool halt, so every wait_wal_table/sleep suspend (and each wake re-park) mints and pins a full HTTP handler set until shutdown. It directly contradicts the PR's own amortisation claims and is reachable by the workloads the feature targets. The remaining issues are non-blocking but several touch the design's stated correctness story (M3, M4, M6) and are worth addressing alongside C1.
Draft findings verified: ~11 confirmed (1 Critical, 7 Moderate, plus Minors), ~7 dropped as false positives/already-resolved. All confirmed findings are in-diff; the only out-of-diff-reachable concern (the IODispatcher sharing) was verified safe. Nice, unusually well-documented change otherwise.
…e-func' into feat-productionise-wait-wal-table-func
…on every Worker hot loop iteration. Address other comments
|
Pushed two commits on top of the review:
Per-finding status below. C1 — Unbounded selector + HTTP-handler leak on the REST path — fixed (
|
Code review — OSS #7031 (level 3, tandem with questdb-enterprise#1006)Mission-critical adversarial review of the worker-continuation framework. 11 review passes across the new primitives ( The novel core was traced to ground truth and is sound: the C2-hoist defense, carrier-id recycle ordering, job-clone rotation (no double-close), the timer state machine (single-CAS, no double-fire), and the WAL-waiter register/fire happens-before all verified. CriticalNone. No data race, lost-wakeup-forever, use-after-free, double-free, native leak, or unclosed Moderate1. Per-iteration FFI cost on the shared reduce pool — 2. Reader-pool supervisor now belongs to Design intent (author): the previous Note on guarding it: a (Positive: the PR also fixes a real pre-existing reader-leak-on-cleanup bug — the old 3. Unreachable dead code in Minor
Downgraded (false positives)
Summary
🤖 Level-3 adversarial review via Claude Code ( |
|
/azp run macwin |
|
Azure Pipelines successfully started running 1 pipeline(s). |
bluestreak01
left a comment
There was a problem hiding this comment.
Approving after an independent level-3 review pass (full continuation runtime, carrier-identity FFI, worker loop, both SQL functions, WAL notification path, Job rotation, reader-pool supervisor relocation, and HTTP/PG wiring).
Validation: compiled the core module clean across all 272 changed files; ran the focused new suites — DelayHeapTest, TimerShardsTest, TimerContTest, WorkerContinuationTest, CarrierLocalTest, JobRotationTest, SeqTxnTrackerTest, SleepFunctionFactoryTest, WaitWalTableFunctionFactoryTest, ReaderLeakTest, ServerMainSleepTest, ServerMainWaitWalTableTest — all green, including timeout/cancel/shutdown-while-parked scenarios.
No blocking defect found in the current head (60e139d). The two Criticals from the earlier review were written against an older revision and are now resolved: workerId aliasing is fixed by the carrierId lazy-pull redesign (the <=N-concurrent-distinct-id invariant is preserved), and the timer-drain ordering is documented + asserted.
Non-blocking follow-ups worth addressing (none gate the merge):
- TimerShards.shutdown() early-returns on !running; make it always drain and mark halt() synchronized, so a close() not preceded by signalClose() cannot skip the drain.
- Register an assignThreadLocalCleaner for ColumnTypeConverter's native-backed CarrierLocal slots (the case DESIGN_NOTES.md already calls out). Pre-existing, equal severity to master, but worth fixing here.
- Reader-pool supervisor on SqlExecutionContext relies on the unenforced single-thread-per-context invariant; consider an ownership assert.
- Surface the bundled execute() retry cap (infinite-retry -> throw after 1000) and the standing tradeoffs (always-on TimerShards thread per shard, per-suspend native stack chunk, ~1-3 ns per CarrierLocal.get()) in the PR description.
- Minor: add WaitWalTableSeqTxnFunctionFactory to function_list.txt for consistency; non-destructive check instead of the destructive assert recyclePool.pop() == null in bind(); add Performance/SQL/New feature/rust labels.
|
/azp run macwin |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
[PR Coverage check]😍 pass : 1457 / 1695 (85.96%) file detail
|
Resolve the single content conflict in QueryRegistry.java's import block. Master's #7031 switched tlQueryPool from ThreadLocal to CarrierLocal so wait_wal_table()/sleep() no longer block IO threads; this branch had added Os and Unsafe imports next to the old ThreadLocal import for the query-lifecycle CAS guard. The resolution keeps the Os and Unsafe imports, adopts CarrierLocal, and drops the now-unused ThreadLocal, sorting CarrierLocal into its alphabetical position. The tlQueryPool field and constructor were changed only by master and auto-merged to CarrierLocal. The lifecycle guard (beginCancel/retire/ activate) is independent of the pool's thread-affinity model, so the spurious-cancellation fix remains correct. Advances the java-questdb-client submodule pointer to 809d4534b6. Verified: core test-compile is clean; QueryRegistryLifecycleTest (4) and QueryActivityFunctionFactoryTest (7) pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge origin/master (e-3.0.3-708, 97e8a6a) into the feature branch, bringing in the table-level parquet format (#7107), the memory-budgeted parquet random-access cache (#7230), the LIMIT/ORDER BY decode-skipping work (#7215/#7222) and the lightweight wait_wal_table/sleep change (#7031). Conflict resolutions: - parquet_read/parquet_meta_decode.rs, parquet_write/encoders/plain/ {mod,primitive}.rs: union of both sides' imports/re-exports (VarcharSliceBufGuard, TimestampValues, encode_designated_timestamp_ strided from master; encode_boolean_nullable, encode_primitive_def_ levels, row_groups conversion helpers from the feature branch). - parquet_write/update.rs: kept both sides' additive tests. - parquet_read/row_groups.rs: clean-merge break - master added the page_buffers_size field to ColumnChunkBuffers; set it to 0 in the two feature-branch post_convert test initializers. - O3ParquetMergeContext.java / O3PartitionJob.java: merged both sides' members; copyO3ToRowGroup takes master's ctx-based body and feeds the parquet colId from getOriginalWriterIndex so ALTER COLUMN TYPE field ids survive into master's refactored fresh-O3/append path. - PageFrameMemoryPool.java / PageFrameMemoryRecord.java: integrated the byte-budgeted LRU cache with the lazy col-type conversion mapping; openParquet() refreshes sourceColumnTypes/hasTypeCasts on every navigation incl. a full cache hit, now wrapped in clear-on-throw to match the decode branches. - GroupByHistogram.java: took master's complete Unsafe shorthand refactor. - WalWriterFuzzTest.java: kept testConvertPartitionToParquet's feature probability tweaks; dropped the three POSTING-index regression tests that #7107 deleted on master. Rebuilt libquestdbr.dylib (darwin-aarch64, release) from the merged Rust source. Validated: cargo check/clippy/fmt clean, 1032 lib tests pass; mvn -pl core test-compile BUILD SUCCESS on JDK 25.
The master merge (a877001) brought in the removal of io.questdb.std.ThreadLocal, which PR #7031 replaced with io.questdb.std.CarrierLocal to make per-thread state safe under continuation-based workers. Two live-view files still imported and used the deleted type, so the branch no longer compiled: - CairoEngine.tlInvalidateSink - LiveViewStateStoreImpl.taskHolder Both fields only call get(), and CarrierLocal exposes the same ObjectFactory<T> constructor and get()/set()/remove() surface, so the swap is mechanical. CairoEngine already imported CarrierLocal for tlMatViewRefreshTask; LiveViewStateStoreImpl updates its import. Verified with: mvn -pl core compile -P local-client. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CopyExportException recycles a CarrierLocal flyweight, but its instance() factories reset only phase/message/errno, never the inherited base flags. The parquet exporters set cancellation/interruption on that flyweight, so a later non-cancel error on the same carrier thread inherited a stale cancellation and could surface as STATUS_CANCELLED. Route both factories through the inherited clear(errno) so every base flag starts clean. Correct the comments in CairoException.clear(), ReadParquetRecordCursor, and CairoExceptionTest. PR #7031 abolished the CairoException ThreadLocal flyweight: instance() now allocates a fresh object every call, and clear() resets cancellation. The comments still described a reused flyweight whose clear() left flags sticky, which the current code contradicts. Reframe them: clear() is a full reset that stays load-bearing for pooled subclasses (LineProtocolException), and the parquet catch keys on isInterruption() because a timeout sets interruption but not cancellation.
Tandem https://github.com/questdb/questdb-enterprise/pull/1006
Summary
Productionises
wait_wal_table(table[, seq_txn])and addssleep(seconds)as the first two callers of a new worker-continuation framework. While these functions are parked, the worker carrier is released to run other queries, so the number of concurrent calls is no longer bounded by the worker pool size.Introduces an internal continuation runtime in
io.questdb.mp.continuationbuilt onjdk.internal.vm.Continuation. A SQL function body can yield the carrier, register a wakeup condition (target seqTxn reached, table state change, timer pop), and resume on any worker. WAL commit notification fires waiters as soon as the target txn lands; aTimerShardsdaemon pool drives periodic wakeups so the body can probe the circuit breaker, cancel flag and SQL timeout.Replaces JDK
ThreadLocalwith a carrier-keyedCarrierLocalalong the continuation-critical paths. C2 hoists the JIT-cached thread identity across yield/resume boundaries, which means a stale value can survive a carrier migration.CarrierLocalroutes reads through an opaque carrier-identity intrinsic to defeat the hoist. The analysis is inCARRIER_LOCAL.md.SQL functions
wait_wal_table(table_name [, seq_txn])Blocks the current query until the WAL writer for
table_namehas applied transactions up to a targetseq_txn. ReturnsBOOLEAN(trueon success). Useful when a client needs to confirm that a prior write has been materialised before issuing a follow-up read.Arguments:
table_nameSTRING-- name of a WAL-enabled table. For non-WAL tables the call returnstrueimmediately.seq_txnLONG, optional -- target sequencer txn to wait for. When omitted (orNULL), the call captures the table's currentseq_txnat start time and waits for that. The argument must be a constant or runtime constant (not a column reference).Behaviour:
trueas soon as the writer txn reachesseq_txn.table is suspended [tableName=...]if the table becomes suspended while the call is waiting.table does not existif the table is dropped while the call is waiting.CANCEL QUERY, and broken client connection abort the wait promptly.CANCEL QUERY, orquery.timeoutalways wins.Concurrency: while parked, the worker that started the call is released to run other queries. The number of concurrent
wait_wal_tablecalls is no longer bounded by the shared-worker pool size.Examples:
sleep(seconds)Pauses the current query for the given number of seconds and emits a single
TIMESTAMProw containing the server's wall-clock time at wake. Implemented as a table function, so it can be used as a top-level query as well as insideSELECT.Argument:
secondsDOUBLE-- sleep duration in seconds. Must be finite and non-negative. Maximum is 24 hours; longer values are rejected with asleep duration exceeds 24 hour maximumerror. Sub-millisecond values round down to 0 ms and return immediately.Behaviour:
TIMESTAMPcolumn namedsleep.griffin.query.continuation.wake.interval, default 1s): query timeout, explicitCANCEL QUERY, and broken client connection abort the sleep within one wake interval.sleep aborted, connection closing.Concurrency: while parked, the worker carrier is released. Concurrent
sleepcalls do not pin worker threads.Examples:
Configuration
cairo.timer.shards-- number ofTimerShardsdaemon threads driving deadline-based wakeups. Defaults tomin(4, max(1, cpu_count / 4)). Higher values reduceDelayQueuelock contention at the cost of one always-on thread per shard.griffin.query.continuation.wake.interval-- millisecond interval at which a parked suspending function body wakes to probe the circuit breaker. Default1000. Lower values trade timeout/cancel latency for more wake-up work.Known risk: context-local reader-pool supervisor
To let the reader-pool leak supervisor survive a continuation park/resume, this PR moves it off a per-thread
ThreadLocaland onto theSqlExecutionContext(read throughSqlExecutionContext.getReaderPoolSupervisor()). This relies on an invariant: a singleSqlExecutionContextis only ever driven by one thread at a time. Production satisfies the invariant -- each connection and query owns its context, and a parked continuation resumes on at most one carrier thread -- so the supervisor never sees concurrent access.The supervisor's backing state (
QueryProgress.readers) is not thread-safe. When two threads do share one context, concurrentgetReadercalls cross-wire the supervisor slot and corrupt that list, which leaks a reader; the leaked, goodbye'd-but-retained reader then pollutes later work on the same engine. Several concurrency stress tests hit exactly this by fanning one context across worker threads. Commit a0051ae gave the parallel group-by contention tests their own per-thread context, and a follow-up does the same forSampleByTestandSampleByNanoTimestampTest.The invariant is enforced by convention, not by the type. Any future production path or test that shares one
SqlExecutionContextacross threads reintroduces the corruption. Making the supervisor access thread-safe, or asserting single-thread ownership on the context, would remove the risk at the source; this PR does not do that and instead relies on the ownership convention plus the test fixes above.