ReviewOS

zig-utils/zig-js

Runtime: upgrade heap-limit Thread OOM sentinel to reserved OutOfMemoryError object

#26
Closed chris opened this 9 days ago ·
9 days ago

Parent: #24

Goal

The current shared-realm Thread heap-limit contract is intentionally fail-closed and allocation-free: when a spawned Thread hits native error.OutOfMemory, its completion record publishes the static string sentinel "OutOfMemoryError", so join() has deterministic JS-visible behavior even with zero remaining context heap budget.

That is robust, but the ideal final embedder-facing API should expose a real error object without reintroducing an allocation dependency on an already-exhausted heap.

Design sketch

  • Add a small per-Context emergency reserve or prebuilt OOM completion object for capped contexts.
  • Ensure Thread OOM completion can publish a real OutOfMemoryError object with stable .name, .message, and instanceof Error behavior.
  • Define how the reserve is replenished or consumed across repeated OOM completions.
  • Keep top-level host Context.evaluate() behavior distinct: it may still return Zig error.OutOfMemory to the embedder boundary.
  • Preserve the existing fail-closed guarantee: OOM reporting must not allocate from the already-exhausted ordinary heap path.

Acceptance criteria

  • join() on a heap-limit-exhausted spawned Thread throws an object, not a primitive string.
  • The thrown value is distinguishable as OutOfMemoryError and is still available when the ordinary context budget has no headroom.
  • Sibling Thread survivor tests continue to pass.
  • The docs in docs/threads/limits.md no longer need to call out the primitive sentinel as the final observable value.
9 days ago

Landed 78c5a4f (fix(threads): reserve oom error object).

What changed:

  • Added OutOfMemoryError to the built-in NativeError family, so it has a real constructor/prototype and chains through Error.
  • Capped contexts now prebuild an immutable reserved OutOfMemoryError instance during bootstrap, while heap budget still exists.
  • Spawned shared-realm Thread OOM completion publishes that reserved object; the old static string remains only as a fallback if a context somehow has no reserved object.
  • GC root tracing now marks the reserved OOM object from the Context root set.
  • The survivor witness now proves join() catches an object that is instanceof OutOfMemoryError, instanceof Error, passes Error.isError, has stable name/message, is frozen, and still allows a sibling Thread to join.
  • Docs now describe the reserved object contract instead of the temporary primitive sentinel.

Validation run locally:

  • zig fmt src/context.zig src/jsthread.zig src/interpreter.zig src/gc.zig
  • zig build test -Dtest-filter='Context heap_limit_bytes lets sibling Threads survive one Thread OOM' --summary none
  • zig build test -Dtest-filter='Context heap' --summary none
  • bun run docs:build
  • git diff --check

This satisfies the #26 acceptance criteria for the capped shared-realm Thread OOM path.

9 days ago

Closed by 78c5a4f. The capped Thread OOM path now rethrows a reserved immutable OutOfMemoryError object and the survivor witness covers the acceptance criteria.

Sign in to comment on this issue.