Skip to content

fix(ext/node): implement proper resourceLimits for node:worker_threads#32430

Merged
bartlomieju merged 9 commits intodenoland:mainfrom
bartlomieju:feat/worker-threads-resource-limits
Mar 6, 2026
Merged

fix(ext/node): implement proper resourceLimits for node:worker_threads#32430
bartlomieju merged 9 commits intodenoland:mainfrom
bartlomieju:feat/worker-threads-resource-limits

Conversation

@bartlomieju
Copy link
Member

@bartlomieju bartlomieju commented Mar 3, 2026

Summary

Implements proper resourceLimits support for node:worker_threads, matching Node.js behavior:

  • Individual V8 ResourceConstraints: Uses set_max_old_generation_size_in_bytes, set_max_young_generation_size_in_bytes, set_code_range_size_in_bytes
  • Stack size: Sets OS thread stack size via std::thread::Builder::stack_size() for stackSizeMb
  • Resolved limits: Reads back V8 defaults for unspecified values via new op_worker_get_resource_limits op, so resourceLimits inside the worker always shows actual values (matching Node.js behavior)
  • OOM handling: Installs near-heap-limit callback that terminates the worker with ERR_WORKER_OUT_OF_MEMORY error code
  • Init ordering fix: Stores resolved limits in op_state before worker.bootstrap() so the polyfill can read them during init

Closes #26156
Closes #10036

Implements the `resourceLimits` option for `node:worker_threads` Worker
constructor, resolving denoland#26156. Previously, resourceLimits was accepted
but silently ignored.

- Thread `resourceLimits` from JS through `op_create_worker` to the V8
  isolate's `CreateParams::heap_limits()`
- Install a near-heap-limit callback that gracefully terminates the
  worker instead of crashing the entire process with a fatal V8 OOM
- Emit `ERR_WORKER_OUT_OF_MEMORY` error with exit code 1, matching
  Node.js behavior
- Expose correct `resourceLimits` inside the worker via serialized
  metadata, and clear to `{}` after worker exits
- Enable `test-worker-resource-limits.js` node_compat test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bartlomieju bartlomieju marked this pull request as draft March 3, 2026 14:59
bartlomieju and others added 2 commits March 3, 2026 16:22
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix JSON key order in main.out to match actual object property order
- Assert ERR_WORKER_OUT_OF_MEMORY error code and message in OOM test
- Fix expected OOM exit code to 1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bartlomieju
Copy link
Member Author

bartlomieju commented Mar 3, 2026

Needs denoland/rusty_v8#1918 to work

bartlomieju and others added 2 commits March 4, 2026 10:24
…urceLimits

- Replace incorrect `heap_limits()` call with individual setters:
  `set_max_old_generation_size_in_bytes`, `set_max_young_generation_size_in_bytes`,
  `set_code_range_size_in_bytes` (requires v8 crate with ResourceConstraints API)
- Handle `stackSizeMb` by setting OS thread stack size via `std::thread::Builder::stack_size()`
- Add `ResolvedResourceLimits` struct and `op_worker_get_resource_limits` op
  to pass V8 defaults back to the worker JS context
- Read back resolved values from CreateParams getters for unspecified fields
- Update worker_threads.ts to use resolved limits from Rust instead of metadata
- Update spec test expected outputs
- Disable node_compat test-worker-resource-limits.js (needs v8.getHeapSpaceStatistics)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Upgrade v8 crate from path dep to published 146.2.0. Fix timing issue
where resolved resource limits were stored in op_state after bootstrap,
but the worker_threads polyfill reads them during bootstrap. Now uses
WebWorker::from_options + manual bootstrap to inject limits before init.

Remove plan.md (disallowed as top-level file by linter).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bartlomieju bartlomieju changed the title feat(ext/node): implement resourceLimits for node:worker_threads feat(ext/node): implement proper resourceLimits for node:worker_threads Mar 4, 2026
@bartlomieju bartlomieju marked this pull request as ready for review March 4, 2026 09:58
… for CI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bartlomieju bartlomieju changed the title feat(ext/node): implement proper resourceLimits for node:worker_threads fix(ext/node): implement proper resourceLimits for node:worker_threads Mar 5, 2026
@kajukitli
Copy link
Contributor

solid PR overall. few things:

  1. near-heap-limit callback returns current_limit * 2 — this just delays the OOM. the callback will fire again and again until it eventually crashes. you probably want to just return current_limit or 0 to avoid extending the limit after triggering termination

  2. division before storing resolved limitsparams.max_young_generation_size_in_bytes() / mb does integer division. if v8 returns something not perfectly divisible by 1MB you're silently dropping precision. probably fine for practical purposes but worth a comment

  3. resourceLimits: any = {} in typescript is a bit rough. you already have the type from WorkerOptions["resourceLimits"], could keep that

  4. the test test-worker-resource-limits.js is skipped on all platforms — might be worth leaving a TODO for when v8.getHeapSpaceStatistics() lands

looks good otherwise, clean impl that matches node behavior

@bartlomieju
Copy link
Member Author

  1. near-heap-limit callback returns current_limit * 2 — this just delays the OOM. the callback will fire again and again until it eventually crashes. you probably want to just return current_limit or 0 to avoid extending the limit after triggering termination

This is fine - we're calling terminate_execution() there so JS will not execute anymore

  1. the test test-worker-resource-limits.js is skipped on all platforms — might be worth leaving a TODO for when v8.getHeapSpaceStatistics() lands

It will be addressed in #32483.

@kajukitli
Copy link
Contributor

Checked against Node source. A few behavioral differences:

  1. Node always installs NearHeapLimitCallback for workers, regardless of whether resourceLimits is set (line 199 in node_worker.cc). This PR only installs it when has_resource_limits is true. Workers without explicit limits won't get graceful OOM handling in Deno.

  2. Node has a minimum stack size check — if stackSizeMb * kMB < kStackBufferSize (192KB), it clamps to kStackBufferSize. This PR doesn't have that floor.

  3. Node clamps maxOldGenerationSizeMb to minimum 2 in parseResourceLimits (lib/internal/worker.js line 632). This PR doesn't.

  4. Node's resourceLimits getter on parent side calls getResourceLimits() on the handle to get live resolved values from V8. This PR just copies the input resourceLimits_ object on the parent side (only worker side gets resolved values via op).

  5. NearHeapLimit return value — node returns current_heap_limit + 16MB, this PR returns current_limit * 2. Both work, just different.

Otherwise looks good, clean impl.

if has_resource_limits {
let ts_handle = worker.js_runtime.v8_isolate().thread_safe_handle();
let oom_flag = worker.oom_triggered.clone();
worker.js_runtime.add_near_heap_limit_callback(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fwiw this callback runs when v8 performs a last-resort gc. that gc might reclaim enough space that js is able to continue running normally.

…ts properly

Address PR review feedback: add comment explaining sub-MB truncation
is intentional for resolved resource limits, and use
WorkerOptions["resourceLimits"] type instead of `any`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bartlomieju bartlomieju force-pushed the feat/worker-threads-resource-limits branch from bf92b8d to fe804d1 Compare March 5, 2026 14:31
@bartlomieju bartlomieju merged commit d26e5c4 into denoland:main Mar 6, 2026
112 checks passed
@bartlomieju bartlomieju deleted the feat/worker-threads-resource-limits branch March 6, 2026 07:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deno ignores ResourceLimits for node:worker_threads Resource limit for webworkers

3 participants