Bounded query execution: heap-growth budget + scanColumn wiring (LLP 0097)#295
Conversation
…0097) Realizes LLP 0054/0055/0056 kernel-side against the pinned engines (squirreling 0.14.0 / icebird 0.8.13), closing the issue-#9 crasher class: an unbounded ORDER BY over ai_gateway_messages now refuses in ~1s at ~1.4GB peak RSS with a typed error instead of dying at 6GB. - sql.js: thread the abort signal into squirrelExecuteSql (LLP 0054 #signal-threading) and enforce a per-query heap-growth budget, checked INLINE on the scan path (every 4096 rows / per column chunk) plus a 100ms interval for post-scan phases. A timer-only watchdog measurably never fires during a hot query (event loop starved). Default 1GiB growth, HYP_QUERY_MAX_HEAP_MB / maxHeapBytes override, typed QueryExecutionBudgetError exported from hypaware/core/query. - storage.js: hide internal fields by scan-level projection instead of rebuilding every row (~5 allocations/row removed); forward scanColumn. - union-source.js: forward scanColumn by concatenating per-partition column streams, owning limit/offset over the merged stream (LLP 0055). - ai-gateway dataset.js: withSchemaColumns forwards scanColumn, null-filling a partition that physically lacks the column. - LLP 0097 records the Phase 0 measurements (LLP 0057) and the decision; editorial forward-refs on 0054/0057. Measured on the production cache (202k rows / 931MB): every benched query faster (up to -26% wall), none regressed; graph queries unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lank-env guard (LLP 0097) Review hardening for the heap-growth guard (dual-review of PR #295): - Terminal budget check after collect(): the inline guard samples only every 4096 rows / per column chunk and the interval watchdog cannot fire during a fully synchronous run, so growth concentrated in a sub-stride tail or in finalization could return a wrongly-successful result. One guard.check() after materialization closes that window. - Detach the linked upstream-signal abort listener in finally: a long-lived signal shared across many queries would otherwise retain the per-call controller closure. - resolveHeapBudgetBytes: a set-but-blank HYP_QUERY_MAX_HEAP_MB (how many config systems render an unset optional) resolved to Number('')===0 and silently disabled the guard. Only a non-empty value now counts as an override; blank/whitespace falls through to the measured default. Exports resolveHeapBudgetBytes for a deterministic unit test of the env resolution (the heap-growth path itself is not deterministically testable in a shared process). Full suite green (2152 pass); tsc build:types clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
neutral dual-review round — head
|
|
Approved — the one major (missing terminal budget check) and two minors (upstream-signal listener leak, blank HYP_QUERY_MAX_HEAP_MB disabling the guard) surfaced by the dual-review were fixed and pushed as 57fdd76. Full suite green (2152 pass), tsc build:types clean. Mergeable and green. Held for the maintainer to merge. |
|
Approval label withdrawn: the review fixes for the three findings were pushed as |
neutral round-2 review —
|
|
Approved - mergeable, green, reviewed at 57fdd76 (dual-review Codex+Claude). The three round-1 fixes are confirmed sound and complete; the one Codex major (scanColumn internal-field surface) was investigated and is a non-exploitable false positive (engine only names advertised columns; every dispatch path validates against them; empirical probe rejects internal-column queries with ColumnNotFoundError). Held for the maintainer to merge. |
Queries over the ~202k-row / 931MB
ai_gateway_messagescache could OOM-kill the daemon/server (hyparam/hypaware-server#9 class). This lands the kernel side of the bounded-query-execution corpus (LLP 0054/0055/0056/0057) with a new decision, LLP 0097, sized from a fresh measurement pass.Before / after (median of 3, fresh process per run, production cache)
COUNT(DISTINCT session_id)GROUP BY providerGROUP BY session_idtop-20ORDER BY ... LIMIT 20COUNT(DISTINCT content_text)SELECT * ORDER BY(crasher)hyp graph neighborsdepth 1/3Every measured query got faster; none regressed.
What changed
src/core/query/sql.js— threads anAbortSignalintosquirrelExecuteSql(LLP 0054#signal-threading) and enforces a per-query heap-growth budget: an inline check on the scan path (every 4096 rows / per column chunk) plus a 100ms interval for post-scan phases. The inline layer matters: a timer-only watchdog measurably never fires during a hot query (event loop starved; zero samples in 8s). Default 1GiB growth (2x headroom over the worst legitimate measured query), overridable viaHYP_QUERY_MAX_HEAP_MBormaxHeapBytesonExecuteSqlOptions(0 disables). Refusal is a typedQueryExecutionBudgetError(exported fromhypaware/core/query) per LLP 0056 - never a truncated result.src/core/cache/storage.js— internal cache fields are now hidden by scan-level projection instead of rebuilding every row object (~5 allocations/row removed on every query); forwardsscanColumn.src/core/query/union-source.js— forwardsscanColumnby concatenating per-partition column streams, owning limit/offset over the merged stream (LLP 0055; offered only when every partition can stream).dataset.js—withSchemaColumnsforwardsscanColumn, null-filling partitions that physically lack the column (same additive schema-drift rule as row reads).No engine changes required: squirreling 0.14.0 already streams aggregates and honors
signal; icebird 0.8.13 already implementsscanColumnat the leaf - the missing pieces were all kernel wrappers. squirreling's per-operatorExecutionBudget(PR hyparam/squirreling#42, still unmerged) remains the precise refinement; LLP 0097 keeps this guard as defense-in-depth when it lands.Tests
npm run build:typesclean;core_boot_noopsmoke ok. ref-check clean for the new refs (the 95 pre-existing 0040/0086 anchor errors are untouched).🤖 Generated with Claude Code