Skip to content

context-graph: dedicated finite projection heap budget (HYP_GRAPH_PROJECTION_MAX_HEAP_MB) - step 1 of #376#378

Open
philcunliffe wants to merge 1 commit into
masterfrom
fix/issue-376
Open

context-graph: dedicated finite projection heap budget (HYP_GRAPH_PROJECTION_MAX_HEAP_MB) - step 1 of #376#378
philcunliffe wants to merge 1 commit into
masterfrom
fix/issue-376

Conversation

@philcunliffe

Copy link
Copy Markdown
Contributor

Step 1 of #376: immediate prod unblock

projectGraph ran its three executeQuerySql scans without a maxHeapBytes override, so they inherited the 1 GiB user-query heap budget added in #295 (LLP 0097). The shared per-contract scan fully materializes the whole source table before the rule loop sees a row, so on the ~1.66M-row prod table it trips that budget every sweep, logs graph_projection.scope_failed, re-marks the scope dirty, and freezes the fleet graph.

The fix

  • New helper resolveProjectionMaxHeapBytes() reads a dedicated knob HYP_GRAPH_PROJECTION_MAX_HEAP_MB (default 3 GiB = 3221225472 bytes), MB -> bytes, mirroring the existing HYP_QUERY_MAX_HEAP_MB blank-var handling.
  • That budget is passed as maxHeapBytes at all three projection call sites: the shared per-contract scan, the raw-SQL rule scans, and the dedup id read.
  • Never 0. A blank, zero, non-positive, or non-numeric knob falls back to the default rather than disabling executeQuerySql's watchdog. maxHeapBytes: 0 would remove the guard entirely and risk a daemon OOM crash-loop, which is strictly worse than a stale graph.

Properties preserved

  • User-query budget untouched. This is a dedicated budget applied only at the projection scan sites; the 1 GiB default that protects POST /v1/query traffic is unchanged.
  • Fail-clean. Because the budget stays finite, when the table eventually outgrows it projection refuses with the typed QueryExecutionBudgetError (surfaced by the scheduler as graph_projection.scope_failed) instead of crashing the daemon. Acknowledged treadmill; it buys time for step 2.

Testing

test/plugins/context-graph-projection-budget.test.js captures the query-exec seam and asserts the finite knob-derived budget reaches every scan site: default 3 GiB when unset, the knob override propagates, and the unset/blank/zero/negative cases resolve to the default (never 0). Verified it fails when the maxHeapBytes additions are reverted and passes with them. Full npm test green apart from the known pre-existing sandbox failures (hyparquet/icebird module-resolution, vector-search, ~7 leave/join).

Deferred

The durable incremental + chunked projection (step 2) is tracked in #377.

Fixes #376

Graph projection ran its three executeQuerySql scans (the shared per-contract
scan, the raw-SQL rule scans, and the dedup id read) without a maxHeapBytes
override, so they inherited the 1 GiB user-query heap budget added in #295 (LLP
0097). The shared scan fully materializes the source table before the rule loop
sees a row, so on the ~1.66M-row prod table it trips that budget every sweep,
logs graph_projection.scope_failed, and freezes the fleet graph.

Step 1 (immediate unblock): resolve a DEDICATED projection budget from a new
knob HYP_GRAPH_PROJECTION_MAX_HEAP_MB (default 3 GiB) in one helper,
resolveProjectionMaxHeapBytes, and pass it as maxHeapBytes at all three scan
sites. Never 0: a blank, zero, non-positive, or non-numeric knob falls back to
the default rather than disabling executeQuerySql's watchdog (0 risks a daemon
OOM crash-loop, strictly worse than a stale graph). This leaves the 1 GiB
user-query guard untouched and preserves the fail-clean property: when the table
outgrows the dedicated budget, projection refuses cleanly instead of crashing
the daemon.

The durable incremental + chunked projection (step 2) is deferred to #377.

Adds a regression test asserting the finite knob-derived budget reaches every
scan site (default 3 GiB, override propagates, unset != 0), captured through an
injectable query-exec seam.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@philcunliffe

Copy link
Copy Markdown
Contributor Author

Neutral review - PR #378 (step 1 of #376): dedicated finite projection heap budget

Verdict: clean. No actionable findings. CI green. Reviewed git diff origin/master...HEAD at head ad33b24f941386036485aa5bb312e8a87b422f87 in an isolated LLP 0012 worktree. Regression test passes (3/3); a mutation revert of a single scan site fails the test (2/3), confirming it guards the fix.

Focus points

  1. Never 0 / never unbounded - YES. resolveProjectionMaxHeapBytes (project.js:41) .trim()s the env, and returns the parsed value only when Number.isFinite(mb) && mb > 0. Blank, whitespace-only, 0, negative, NaN, Infinity, and unset all fall through to GRAPH_PROJECTION_DEFAULT_MAX_HEAP_BYTES = 3072*1024*1024 (3221225472, finite, positive). Never returns 0/NaN/Infinity/falsy. This matters because sql.js:413 if (budgetBytes <= 0) return disables the watchdog for a non-positive budget - the exact OOM crash-loop the issue warns against, which this guard avoids.
  2. All three sites budgeted - YES. maxHeapBytes is resolved once (project.js:68) and passed at the shared per-contract scan (:141), the raw-SQL rule scan (:172), and both dedup id reads via dedupExisting (:195-196 -> :251). No projection scan inherits the 1 GiB default. Test captures all four executeQuerySql calls and asserts each carries the projection budget.
  3. User-query budget untouched - YES. The diff touches only project.js and the new test. DEFAULT_MAX_HEAP_GROWTH_BYTES (1 GiB, sql.js:34), HYP_QUERY_MAX_HEAP_MB, resolveHeapBudgetBytes, and POST /v1/query are unchanged. Projection supplies an explicit per-call maxHeapBytes, which resolveHeapBudgetBytes honors via if (optionBytes !== undefined) return optionBytes - a per-call override, not a global change. Projection-only knob.
  4. Fail-clean preserved - YES. The finite budget flows into the same watchdog (budgetBytes > 0 keeps it armed, sql.js:419); over-budget still raises QueryExecutionBudgetError (query_budget_exceeded), surfaced by the scheduler as graph_projection.scope_failed. The guard is raised for projection, not removed.
  5. Injectable seam production-safe - YES. __executeSql defaults to the real executeQuerySql. The only in-repo caller of projectGraph (command.js:42) does not pass the seam, so production behavior is identical. (Only the test injects a recording fake.)
  6. Test quality - YES. Asserts default 3 GiB reaches all four sites when unset, override (512) propagates to every site, and blank/whitespace/0/negative/non-numeric => default (never 0). Mutation-verified: reverting any one site fails the suite.
  7. @ref honesty / style - YES. The one new annotation @ref LLP 0097 [constrained-by] resolves (0097 is the heap-growth execution-budget decision) and the gloss is accurate. All other refs in the file resolve, including LLP 0105#surfaces (explicit {#surfaces} anchor). No semicolons, no em dashes; JSDoc @import of ExecuteSqlOptions/ExecuteSqlResult (declared in src/core/query/types.d.ts) follows the root-anchored .js convention.

Step 2 (durable incremental + chunked) is correctly out of scope, deferred to #377. No changes pushed - the PR stands as reviewed.

@philcunliffe
philcunliffe marked this pull request as ready for review July 22, 2026 23:12
@philcunliffe philcunliffe added the neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030) label Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030)

Projects

None yet

1 participant