Repository navigation
Native: answered queries no longer hold query queue capacity - #965
Conversation
Native keeps every query task in dw_query_cache until its receipt expires (deadline plus at least 60 s), whatever its status. query_entries scanned the namespace with LIMIT 65 across all statuses, and admission refused at 64 entries. So 64 answered queries in a namespace refused every new query, and blocked the worker claim scan, until the oldest receipts expired. Python SDK 2.12.4 hit it at query 64 of a sequential loop; PHP 2.11.8 answered all 75. The scan now reads only open (pending or leased) entries on each backend, and a bounded sweep removes expired receipts, which the scan used to do. Answered, failed and timed-out receipts stay readable by key until they expire. Admission keeps its 16-open-task bound, as PHP's counts only pending task ids. answered_queries_release_queue_capacity runs 80 sequential queries on one run. It fails at query 64 on 923d45b and passes now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Security review at exact head The status transition itself fixes #964's accounting, but it removes the only total-receipt bound while both new database paths are unbounded scans:
The concrete SQLite plan for the exact query shapes is Please preserve the terminal-receipt/idempotency contract while making both selection paths bounded by indexed state—for example, indexed materialized/generated namespace/status plus expiration columns, or a separately indexed/open-task representation—and add a high-terminal-cardinality counterfactual that proves admission/claim does not scan the retained population. The 80-query functional regression is necessary but does not exercise this resource bound. I have not found an authority/scope error in terminal keyed reads, the 16-open admission cap remains enforced, and the multi-database status predicates themselves are consistent. Security hold is specifically the unbounded shared-table work above. |
|
Design carry for the correction to Security's hold, before a new head is pushed: The revised exact-key open-set design avoids the PostgreSQL collation trap in the first range-key proposal, but Security clearance will require these invariants in the exact implementation and tests:
These are acceptance constraints, not a request to widen the patch into a schema migration. The current exact head remains held by the unbounded scans in comment 6589640509 until its replacement is reviewed. |
…ans (#964) Security's hold on 6594f94: filtering the namespace scan to open rows left it, and the expiry sweep, scanning every retained receipt in the shared cache table (JSON predicates and expiration have no index). A key range isn't safe either: on PostgreSQL the key column uses the database's collation (en_US.utf8 in the CI image), which doesn't order punctuation bytewise. Every request-path read is now by primary key: - Open set: one row per namespace (key from a namespace digest, value naming the namespace in full) lists its open task ids, validated as unique canonical ULIDs, at most 64. Admission adds the new id. Each read fetches the listed tasks by key, keeps only this namespace's pending or leased ones under their own id, and rewrites the set when members closed, expired or vanished. - Expiry: every task row is filed at admission under the minute it expires, in pages of at most 128 keys. A sweep behind a watermark row deletes expired keys by key, at most 30 minutes and 2 pages per call, and files a key again if its expiry moved. A row already expired when filed is deleted at once. All of it runs in the caller's transaction. - Cutover: 3.0.1 kept query rows with neither. A global marker walks the cache table one primary-key page (64 rows) per call, once for the whole server, listing open tasks in their namespaces' sets and filing every query row. No per-namespace fallback scan exists. Receipts keep their keys, so the waiting caller, idempotent retries and completions read them as before. thousands_of_retained_receipts_leave_ queries_unaffected runs 20 queries with 3000 answered receipts present. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Redesigned at Why not a key range: the key column on PostgreSQL uses the database's collation. The CI image's is Design
Evidence
The tier stays A. PR description updated. |
|
Security re-review at exact 5f78e8f: the new admission/claim open-set path is bounded, but the shared-table scan still has two live poll paths, so this head remains held.
This also makes the claim that steady-state query paths are exact-key-only false even though query_entries itself now is. Please replace both uses with the namespace open-set row plus at most 64 exact task-key GETs (or another genuinely indexed/bounded authority), and remove/retire the shared JSON scan constants. Counterfactual evidence should exercise (1) a held query poll and (2) a cross-kind-enabled non-query poll behind thousands of retained receipts, proving those probes issue only the open-set GET plus bounded exact task GETs—not merely that a planner labels the prefix walk an index scan. |
|
Additional atomicity requirement on exact 5f78e8f while the poll scan is being corrected: cut_over_query_rows uses let _ = Self::list_open(...).await. That discards every failure, although the comment only intends to tolerate query_task_queue_full. On a backend where a statement failure does not poison the transaction, a write/encoding/storage error can be swallowed and the cutover marker can still advance past that legacy open task permanently. Please suppress only the explicitly intended full-set refusal (if dropping that task to its existing deadline remains the chosen migration behavior) and propagate every other error, so the task indexing, expiry filing, and page-marker advance remain one atomic step. A fault-injection counterfactual should leave the marker at the prior page when open-set persistence fails. |
|
Concrete cutover correctness case at 5f78e8f: cut_over_query_rows calls file_expiry first, but when expiration <= now that helper deletes the row and returns no indication. Cutover then continues parsing the stale fetched value and can add its pending/leased ID to the namespace open set. This can permanently skip a live legacy task across tenants: place 64 expired rows whose stored status is still pending for namespace A before one unexpired open A row in key order; drive every cutover page from namespace B. The first page deletes the 64 rows but fills A's set with their IDs. B repairs only B's set. The later live A row then gets the intentionally swallowed queue-full refusal, the global marker advances past it, and only afterwards can A repair its stale set—too late to rediscover the live task. Expired rows must be deleted and excluded from open-set indexing in the same page transaction. Please add that two-namespace counterfactual and prove the live A task is claimable after B alone drives cutover to done. |
|
The same cross-tenant case applies before physical cache expiration: a legacy row can retain status=pending/leased after its query deadline while its receipt expiration is still in the future. Cutover currently trusts the stored status without authority::expire, so 64 deadline-expired A rows can fill A's set and make the later live A row get skipped while B drives the cursor. The page therefore needs to apply the query authority transition before deciding whether a legacy row consumes an open-set slot (persisting the terminal form as appropriate), not only test the cache expiration. Extend the two-namespace counterfactual to cover both physically expired rows and deadline-expired-but-retained rows. |
|
Severity correction after checking the exact 3.0.1 admission invariants: normal old state capped open pending/leased tasks at 16 and all retained query rows for a namespace at 64. Therefore the 64 stale-open saturation sequence in my preceding comments cannot arise from valid 3.0.1 traffic alone; it requires corrupted/out-of-contract cache state (or a broken prior invariant). Please still avoid indexing a row that the same cutover just deleted, apply authority expiry before classifying it open, and do not swallow unrelated persistence failures—those keep the migration internally consistent and are cheap counterfactuals. But I am not treating that saturation scenario as a second normal-upgrade release blocker. The demonstrated blocker at this head remains the two live repeated POLL_WAKE_QUERIES_SQL paths over retained cross-tenant receipts. |
Security's re-review of 5f78e8f: - query_poll_ready and poll_wake_snapshot still ran POLL_WAKE_QUERIES_SQL, the shared-table prefix plus JSON scan, on every long-poll probe. Both now use ready_query_keys: the namespace's open set read lock-free by key, then each listed task by key, with the old filters (queue, query deadline, pending or lease-expired). The statement is removed from every backend. - The cutover deletes an already-expired row and never lists it, closes a pending row past its query deadline before it could take a slot, and propagates every failure except a full set, so its page and marker roll back together. the_cutover_lists_only_answerable_rows: 128 stale 3.0.1 rows (expired, or pending past their deadline) leave three live queries answered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Security re-review of authoritative head I verified the exact three-commit diff from base and the final four-file delta from The cutover hardening is also present: a physically expired row is deleted and not listed; authority expiry runs before open classification; only The old Source clearance: exact |
|
Security hosted-gate update for exact head
This establishes a clean current-base result, but it is deliberately not final merge clearance yet because Security-cleared #957 must merge first and advance |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Security post-#957 carry review at exact bae9138: its parents are the previously reviewed cd1caa6 and exact merged main 9524e80; the merge has no combined-diff conflict resolution. The reviewed PR delta is content-equivalent across the carry (stable patch ID b6af62836678681be5bf0c42995d90b111da472e before and after), and the five release/identity files changed by #957 do not overlap the five query-runtime/test files changed here. Source carry is clear; final merge clearance still waits terminal hosted checks on this exact head. |
|
|
Security final carry clearance: exact head
Security final-clear is granted for #965 at |
|
Post-queue Security receipt: merged as |



Review tier A: the query claim scan feeds lease and fence decisions (write-path locking and concurrency). It's a narrow change: what the scan reads, plus an expiry sweep.
Fixes #964.
Cause
Native keeps every query task in
dw_query_cacheuntil its receipt expires (receipt_expires_at, the deadline plus at least 60 s), whatever its status.query_entriesscanned the namespace's entries withLIMIT 65regardless of status:MAX_ENTRIES);query_entriesitself refused above 64, and the worker claim scan and the task-queue admission view also call it.So 64 answered queries in one namespace refused every new query (
503 query_task_queue_full) and blocked claims until the oldest receipts expired. That matches Python SDK 2.12.4's cutoff at query 64 and the ~4–5 minute recovery. PHP 2.11.8's admission counts only pending task ids againstmax_pending_per_queue, and answered all 75.Change (redesigned at 5f78e8f; see the PR comment)
Checked
answered_queries_release_queue_capacity: 80 sequential queries on one run, each claimed, answered and returned before the next. It fails at query 64 on923d45b3("query 64 was not claimed") and passes with this change.cargo test --lockedon SQLite passes (672 execution, 397 lib).--test execution queryscenarios pass on PostgreSQL 16.15, MySQL 8.0.46 and MariaDB 10.11.19 (the CI image digests).Not in this PR
Native answers
query_task_queue_fullwith 503, PHP with 429. General decided native should match 429. That's a separate small PR, after checking the SDKs' handling.Ships in the next native patch with #953.
🤖 Generated with Claude Code