Skip to content

Fix rolling-deploy VM context thrash - #4811

Open
justin808 wants to merge 32 commits into
mainfrom
fix/4810-bounded-rollout-vm-pool
Open

Fix rolling-deploy VM context thrash#4811
justin808 wants to merge 32 commits into
mainfrom
fix/4810-bounded-rollout-vm-pool

Conversation

@justin808

@justin808 justin808 commented Jul 28, 2026

Copy link
Copy Markdown
Member

Closes #4810.

Why

During a rolling deploy, one Node Renderer can receive alternating requests from the old and new Rails revisions.
An RSC-enabled revision needs both server and RSC VM contexts, so the old/new overlap needs four contexts per worker.
The prior default of two repeatedly evicted and rebuilt those contexts. Disk-cache seeding avoided uploads but did not
prevent VM rebuild churn.

The same topology also made deployment diagnosis easy to overstate: Rails can only prove capacity when it can
statically observe the renderer configuration, and a hard VM-pool cap does not bound execution contexts or source
maps still held by concurrent in-flight requests.

What changed

  • Raise the bounded per-worker VM pool default from 2 to 4 and validate MAX_VM_POOL_SIZE on every config build.
  • Track successful bundle sets as rollout generations, retain the most recently observed successful set, and drain
    inactive unshared contexts after a configurable 60-second timeout using one unreferenced timer.
  • Preserve the absolute LRU hard cap, bound generation metadata, keep active execution contexts usable after pool
    eviction, and emit rate-limited pressure plus cumulative build/eviction diagnostics.
  • Add vmPoolRolloutDrainTimeout / VM_POOL_ROLLOUT_DRAIN_TIMEOUT.
  • Add a stable node_renderer_rollout_capacity Doctor check. Successful Rails boot is required before live Pro
    configuration can prove RSC enablement; a failed boot is reported as unverified and conservatively enabled.
    Renderer capacity is observed only from unambiguous CommonJS provenance and directly scoped launcher/config
    literals. Rebound or destructured require, direct eval, wrapper arguments, hoisted function require, aliases,
    mutation, spreads, computed properties, conditional or unreachable calls, and all other ambiguous cases fail
    closed as unverified.
  • Make renderer-process cleanup race-safe: timeout cleanup targets only the detached negative process group, treats
    ECHILD as a cleanup race, and never falls back to a positive PID that could have been reused.
  • Document the topology formula, rolling-deploy tradeoffs, /ready limitation, pre-seeding versus VM compilation,
    fleet capacity, and pooled-versus-transient memory boundary.
  • Add a reproducible rollout benchmark with p50/p95/max latency, timeout, build, rebuild, eviction, retained-context,
    RSS, heap, and caveat output.

Rollout evidence

On an M1 arm64 (Node 24.8.0, Ruby 3.4.6, Rails 8.1.3), two cloned flagship Rails revisions with distinct server and
RSC bundle hashes shared one candidate renderer. Each fresh-process 40-second run offered 3 requests/second at a fixed
concurrent rate and cycled old server, old RSC, new server, and new RSC paths. The balanced order used three cap-2 and
three cap-4 runs with a preseeded disk cache and cold VM contexts.

Metric Cap 2 Cap 4
Runs / requests 3 / 360 3 / 360
Successful / failed 360 / 0 360 / 0
Over 1,000 ms or transport error 1 0
Rails access-log p95 median (range) 154 ms (153-157) 64 ms (64-72)
Exact-client p95 median (range) 171.13 ms (171.04-174.57) 91.48 ms (88.39-96.20)
Info-level build messages median 60 2
Maximum reported hard-limit evictions 93 0

Short debug-counter runs (32 successful requests each) recorded 32 builds / 30 evictions / max 2 retained contexts at
cap 2, versus 4 builds / 0 evictions / max 4 retained contexts at cap 4. The seed gate built exactly four contexts and
rendered all old/new server/RSC paths.

Cap 4 reduced median Rails p95 by 58.4% and client p95 by 46.5% in this workload. This is workload-specific evidence,
not a universal production-memory claim. In-flight requests can temporarily retain evicted execution contexts and
source maps outside the pool, so deployments still need measured concurrency headroom.

The M1 run executed renderer build 977168a6. Its VM runtime-pool files are byte-identical at exact candidate
5ec6f3220ab64833a4c404ca176cdae97dd61956 (vm.ts blob a19c397f45a8846514a2a7919cf0ec065ef84f37;
vmSourceMapSupport.ts blob 5f7667774b6be45ca79132139ee08367f055bf36). Later exact-head amendments affect
Doctor provenance analysis, process cleanup, specs, and matching diagnostics wording, not the VM runtime pool. The M1
run did not capture RSS or heap evidence; no real-workload memory measurement is claimed.

Five exact-head cap-2/cap-4 benchmark pairs on the synthetic fixture had zero timeouts. Candidate p95 median was
0.408625 ms versus 3.737083 ms at baseline, with 4 builds / 0 rebuilds / 0 evictions / 4 retained contexts versus
16 builds / 12 rebuilds / 14 evictions / 2 retained contexts. This supports repeatability; it does not replace the
cloned-Rails evidence or establish a production-memory ceiling.

Verification

Exact candidate: 5ec6f3220ab64833a4c404ca176cdae97dd61956

  • Node Renderer Jest: 41 suites, 614 tests passed
  • Doctor RSpec: 730 examples passed twice consecutively
  • Review regressions: env-derived numeric rollout config coercion, one full-script Doctor delimiter scan, complete PID
    publication before cleanup assertions, and rollout test spy restoration all passed
  • QA regression: both rollout settings accept numbers and nonempty numeric strings while booleans and arrays fail
    closed through the existing startup error path
  • Focused renderer rollout regression: 166 examples passed
  • Hostile Doctor provenance matrix: 13/13 ambiguous CommonJS cases reported unverified; canonical controls passed
  • TERM-resistant competing-waiter cleanup probe passed; only negative process-group targets were signalled
  • Process cleanup regressions prove the group leader remains unreaped through negative-group signalling, caller
    reaping is time-bounded through a detached waiter, and macOS EPERM probes distinguish a live leader from a
    competing-waiter group-only state
  • Workspace build, TypeScript type-check, ESLint, Prettier, docs sidebar, changed-link check, and full OSS/Pro RuboCop passed
  • Pro license headers: all 873 checked files passed
  • Repository-wide bin/ci-local passed setup, builds, lint, formatting, type-checking, JS/RSC suites, renderer suites,
    and generator execution before the gem-only RBS runtime wrapper failed across unrelated unchanged surfaces.
    Untouched origin/main reproduces the identical representative failure list (19 failures in the same 68-example
    packer_utils + render_options slice) under Ruby 4.0.5, so this is recorded as a pre-existing baseline/toolchain
    limitation rather than candidate regression.
  • Independent exact-head QA, fresh final checking, and independent high-effort autoreview are being repeated for this
    review-fix candidate; their prior-head results are not used as current-head merge evidence.

Companion demo

The flagship demo documents and configures the four-context old/new RSC rollout headroom:
shakacode/react-on-rails-demo-flagship#39

Copilot AI lite review requested due to automatic review settings July 28, 2026 14:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@justin808

Copy link
Copy Markdown
Member Author

+ci-status

@justin808

Copy link
Copy Markdown
Member Author

+ci-run-hosted

@github-actions

Copy link
Copy Markdown
Contributor

CI Status

Head SHA: 5d8b02333656
Changed files: 14
Docs-only heuristic (matches ci-changes-detector metadata paths): no
ready-for-hosted-ci label: absent
force-full-hosted-ci label: absent
Current hosted-CI waiver: not present for this SHA
Automatic release-target hosted mode: inactive
Observed exact-head coverage: modes[missing=9]; successful=0, pending=0, failed=0, missing=9

Only the required gate is active unless hosted CI is requested.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR adds bounded Node Renderer VM retention across rolling-deploy bundle generations, configurable drain timing, rollout-capacity Doctor diagnostics, deployment guidance, benchmark tooling, and regression coverage. The default maxVMPoolSize increases from 2 to 4 per worker.

Changes

Node Renderer rollout capacity

Layer / File(s) Summary
Runtime configuration and generation-aware pooling
packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts, packages/react-on-rails-pro-node-renderer/src/worker/*, packages/react-on-rails-pro-node-renderer/tests/*
Adds environment-backed VM pool settings, generation tracking, bounded retirement, eviction diagnostics, validation, and deterministic retention tests.
Doctor rollout-capacity reporting
react_on_rails/lib/react_on_rails/doctor.rb, react_on_rails/lib/react_on_rails/doctor_schema.rb, react_on_rails/spec/lib/react_on_rails/doctor_spec.rb
Adds the node_renderer_rollout_capacity check, static configuration evidence parsing, topology and RSC analysis, application-root resolution, and observed or unverified JSON results.
Deployment guidance and benchmark validation
benchmarks/bench-node-renderer-rollout.mjs, docs/oss/building-features/node-renderer/*, docs/pro/rolling-deploy-adapters.md, docs/oss/api-reference/doctor.md, CHANGELOG.md
Documents VM sizing, drain behavior, warmup distinctions, deployment topologies, Doctor evidence, and provides an alternating-generation JSON benchmark.

Estimated code review effort: 5 (Critical) | ~90+ minutes

Sequence Diagram(s)

sequenceDiagram
  participant RailsRequest
  participant NodeRenderer
  participant VMPool
  participant DrainScheduler
  RailsRequest->>NodeRenderer: render with resolved bundle set
  NodeRenderer->>VMPool: build or reuse VM context
  VMPool->>VMPool: record bundle generation
  VMPool->>DrainScheduler: schedule inactive-generation retirement
  DrainScheduler->>VMPool: evict drained contexts after timeout
Loading

Possibly related PRs

Suggested labels: benchmark

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.07% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address Issue #4810 objectives, including bounded VM retention, diagnostics, documentation, tests, topology guidance, and rollout benchmarking.
Out of Scope Changes check ✅ Passed The code, tests, benchmark, documentation, changelog, and Doctor updates all support the linked issue and PR objectives.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the primary change: preventing VM context rebuild thrashing during rolling deployments.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/4810-bounded-rollout-vm-pool

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Hosted CI Requested

Triggered 9 workflow(s) for 5d8b02333656.
Skipped 0 workflow(s) with equivalent exact-head coverage.
Mode: optimized hosted CI (path-selected by script/ci-changes-detector).
Added ready-for-hosted-ci, so future commits will keep running optimized hosted CI until +ci-stop-hosted is used.

View progress in the Actions tab.

@github-actions github-actions Bot added the ready-for-hosted-ci Run optimized hosted GitHub CI for this PR label Jul 28, 2026
@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown

Greptile Summary

Raises and validates Node Renderer VM-pool capacity while adding bounded rollout-generation retention and improved deployment diagnostics.

  • Increases the default per-worker VM pool from two to four contexts and adds a configurable rollout drain timeout.
  • Tracks successful bundle generations, retires inactive contexts, preserves source maps for active requests, and reports pool activity.
  • Adds a conservative Doctor check for statically verifiable rollout capacity.
  • Adds focused tests, deployment guidance, changelog notes, and a reproducible rollout benchmark.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete changed-code defect identified.

The VM lifecycle retains active execution resources across eviction, records only successful bundle generations, bounds both pooled contexts and generation metadata, and validates the new configuration before use; the Doctor analysis also fails closed when static evidence is ambiguous.

Important Files Changed

Filename Overview
packages/react-on-rails-pro-node-renderer/src/worker/vm.ts Adds bounded rollout-generation tracking, timer-driven retirement, LRU pressure diagnostics, and active-request-safe source-map cleanup.
packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts Raises the VM-pool default, re-evaluates environment-derived values on each build, and validates both pool and drain settings.
packages/react-on-rails-pro-node-renderer/src/worker/vmSourceMapSupport.ts Updates source-map retention documentation to reflect the new default pool size.
react_on_rails/lib/react_on_rails/doctor.rb Adds fail-closed static rollout-capacity analysis and makes application-root path resolution consistent.
react_on_rails/lib/react_on_rails/doctor_schema.rb Registers the new stable Doctor check and its remediation metadata.
benchmarks/bench-node-renderer-rollout.mjs Adds a synthetic benchmark reporting rollout latency, pool churn, retained contexts, and memory measurements.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  Request[Render request with bundle set] --> Build[Build or reuse VM contexts]
  Build --> Success{All contexts available?}
  Success -- No --> Error[Return existing build or cache error]
  Success -- Yes --> Observe[Record successful rollout generation]
  Observe --> Cap[Apply absolute VM-pool LRU cap]
  Cap --> Active{Evicted context still active?}
  Active -- Yes --> Defer[Retain source-map registration until release]
  Active -- No --> Cleanup[Unregister source map]
  Observe --> Timer[Schedule one unreferenced drain timer]
  Timer --> Retire[Retire expired non-latest generations]
  Retire --> Shared{Context referenced by another generation?}
  Shared -- Yes --> Keep[Keep pooled context]
  Shared -- No --> Cleanup
Loading

Reviews (1): Last reviewed commit: "Fix rolling-deploy VM rebuild thrash" | Re-trigger Greptile

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
react-on-rails/client bundled (gzip) 63.61 KB (0%)
react-on-rails/client bundled (gzip) (time) 63.61 KB (0%)
react-on-rails/client bundled (brotli) 54.61 KB (0%)
react-on-rails/client bundled (brotli) (time) 54.61 KB (0%)
react-on-rails-pro/client bundled (gzip) 64.96 KB (0%)
react-on-rails-pro/client bundled (gzip) (time) 64.96 KB (0%)
react-on-rails-pro/client bundled (brotli) 55.77 KB (0%)
react-on-rails-pro/client bundled (brotli) (time) 55.77 KB (0%)
registerServerComponent/client bundled (gzip) 135.45 KB (0%)
registerServerComponent/client bundled (gzip) (time) 135.45 KB (0%)
registerServerComponent/client bundled (brotli) 81.77 KB (0%)
registerServerComponent/client bundled (brotli) (time) 81.77 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) 127.9 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) (time) 127.9 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) 75 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) (time) 75 KB (0%)

@claude

claude Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review: Fix rolling-deploy VM context thrash

Overview: this raises the Node Renderer's per-worker VM pool default from 2 → 4, adds rollout-generation tracking with a timer-driven drain window so a draining bundle set stays reusable during rolling deploys, keeps the absolute LRU hard cap as a backstop, and adds a new node_renderer_rollout_capacity Doctor check that statically infers whether the configured cap covers the old/new RSC overlap. Well documented, with a large, careful test suite for the new pool/generation logic in vm.test.ts.

Correctness — VM pool / generation logic (vm.ts)

  • The generation-retirement scheduling (scheduleGenerationRetirement / retireDrainedBundleGenerations) is sound: "latest" is recomputed at fire time from observationSequence, so an early/late wake-up from a stale schedule just causes a harmless re-evaluation, not incorrect retirement. Nice use of an injectable VMPoolClock for deterministic tests.
  • Minor: if a buildExecutionContext call has one bundle fail to build (throws) while a sibling bundle in the same request set succeeds, the successful context is added to vmContexts but observeBundleGeneration is never called for that failed set (by design — see the comment above observeBundleGeneration). That orphaned context isn't tied to any tracked generation, so it won't be reclaimed by the generation-drain sweep and will only be evicted once LRU hard-limit pressure forces it out. Bounded by the hard cap, so not a leak, just a minor pool-efficiency edge case — probably fine given the documented "LRU is the absolute backstop" design.

Config validation — inconsistency (see inline comment)

configBuilder.ts's new maxVMPoolSize/vmPoolRolloutDrainTimeout validation uses a bare throw new Error(...) instead of the log.error(...) + process.exit(1) pattern every other validation in buildConfig uses (port, deprecated Sentry/Honeybadger options, timer polyfill rename, password). Because the primary entry point (reactOnRailsProNodeRenderer(config), called fire-and-forget per the dummy app's renderer/node-renderer.js) is async and calls buildConfig without try/catch, this throw becomes an unhandled promise rejection rather than the same clean, logged, deterministic exit the neighboring checks give you. Flagged inline with a suggested fix.

Doctor's node_renderer_rollout_capacity check (doctor.rb)

  • This adds ~370 lines of hand-rolled regex/Ripper-based static analysis to infer maxVMPoolSize from renderer/node-renderer.js and Procfiles without executing them. It's carefully designed to fail closed to unverified on anything ambiguous (template literals, spreads, computed properties, getters/setters, multiple calls, new-constructed lookalikes, etc.), which is the right default given it can't be a real JS parser. This is inherently high-complexity/high-maintenance code; worth a second maintainer's eyes specifically on the regex edge cases (e.g., NODE_RENDERER_CONFIG_OBJECT_PATTERN only unwraps one level of nested {} before bailing to "ambiguous" — correct/safe today, but easy to silently regress if someone tweaks the pattern later without re-deriving the "fails closed" property).
  • Good fix bundled in here: doctor_app_root/doctor_app_path now anchor config/initializers/react_on_rails_pro.rb and config/environment.rb lookups to the discovered Rails root instead of raw relative paths / Dir.pwd, fixing doctor behavior when invoked from a subdirectory.

Security

  • No injection concerns: the new benchmark script uses execFileSync('git', args, ...) (no shell interpolation). The Doctor JS-config scanner only reads and pattern-matches files (bounded to 1 MB via NODE_RENDERER_CONFIG_MAX_BYTES), never evaluates them.
  • additionalContext/supportModules sandbox-escape documentation is unchanged/pre-existing; not affected by this PR.

Performance / operational impact

  • Doubling the default VM pool from 2 → 4 per worker roughly doubles retained-context memory per worker by default — correctly called out in the CHANGELOG's "Upgrade memory impact" note. Worth double-checking that release notes/upgrade guidance are prominent enough that operators size memory requests/limits before upgrading, since this is a default behavior change, not opt-in.

Tests

  • vm.test.ts additions are thorough: alternating old/new rollout reuse, drain-timeout retirement, hard-cap-vs-generation interaction, shared-bundle retention across generations, pressure-warning rate limiting, and timer cleanup on resetVM(). configBuilder.test.ts covers the new env vars and precedence rules well, including the toThrow assertions for the inconsistency noted above.

Nothing else blocking; the config-validation inconsistency is the one item I'd want addressed (or consciously deferred) before merge.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5d8b023336

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts Outdated
@justin808 justin808 added the agent-claimed Advisory mirror of an active private agent coordination claim label Jul 28, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (8)
packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts (1)

240-248: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider parsing the drain timeout with Number for parity with MAX_VM_POOL_SIZE.

parseFloat silently accepts trailing garbage (VM_POOL_ROLLOUT_DRAIN_TIMEOUT=45s45), whereas Number(env.MAX_VM_POOL_SIZE) fails fast on the same input. Aligning both keeps the "invalid env fails startup" contract uniform.

♻️ Proposed change
 function defaultVmPoolRolloutDrainTimeout() {
-  return env.VM_POOL_ROLLOUT_DRAIN_TIMEOUT ? parseFloat(env.VM_POOL_ROLLOUT_DRAIN_TIMEOUT) : 60;
+  return env.VM_POOL_ROLLOUT_DRAIN_TIMEOUT ? Number(env.VM_POOL_ROLLOUT_DRAIN_TIMEOUT) : 60;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts` around
lines 240 - 248, Update defaultVmPoolRolloutDrainTimeout to parse
VM_POOL_ROLLOUT_DRAIN_TIMEOUT with Number instead of parseFloat, preserving the
existing fallback to 60 when the environment variable is absent and ensuring
malformed values fail consistently with defaultMaxVMPoolSize.
packages/react-on-rails-pro-node-renderer/tests/configBuilder.test.ts (1)

179-216: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add ENV-level invalid coverage for VM_POOL_ROLLOUT_DRAIN_TIMEOUT.

MAX_VM_POOL_SIZE gets an it.each over invalid ENV strings, but the drain timeout is only exercised through explicit config values. An ENV case would pin down the parseFloat behavior (e.g. '45s' currently yields 45 rather than failing).

🧪 Suggested additional case
+    it.each(['abc', '0', '-1'])('rejects invalid VM_POOL_ROLLOUT_DRAIN_TIMEOUT=%p from ENV', (timeout) => {
+      process.env.VM_POOL_ROLLOUT_DRAIN_TIMEOUT = timeout;
+      const { buildConfig } = loadConfigBuilderWithMockedLogger();
+
+      expect(() => buildConfig()).toThrow('vmPoolRolloutDrainTimeout must be a positive number of seconds');
+    });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/react-on-rails-pro-node-renderer/tests/configBuilder.test.ts` around
lines 179 - 216, Add an ENV-focused parameterized test alongside the existing
MAX_VM_POOL_SIZE invalid-ENV coverage, using loadConfigBuilderWithMockedLogger
and buildConfig to verify invalid VM_POOL_ROLLOUT_DRAIN_TIMEOUT
strings—including a suffixed value such as “45s”—throw the expected
positive-seconds validation error. Keep the explicit-configuration tests
unchanged.
react_on_rails/lib/react_on_rails/doctor.rb (1)

124-130: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider deriving the launcher list from NodeRendererProcfile::DEFAULT_COMMANDS.

check_launcher_procfiles (Line 1787) already notes launcher filenames must stay aligned with NodeRendererProcfile::DEFAULT_COMMANDS. This is a third hardcoded copy that can silently drift when a Procfile variant is added.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@react_on_rails/lib/react_on_rails/doctor.rb` around lines 124 - 130, Update
NODE_RENDERER_LAUNCHER_PATHS to derive its launcher filenames from
NodeRendererProcfile::DEFAULT_COMMANDS instead of maintaining a hardcoded list.
Ensure check_launcher_procfiles and related launcher checks continue using the
derived collection so newly added Procfile variants remain aligned
automatically.
react_on_rails/spec/lib/react_on_rails/doctor_spec.rb (3)

3707-3761: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

These three examples test Node's semantics, not Doctor.

Open3.capture3("node", ...) makes the Ruby suite fail on any machine or CI lane without Node on PATH, and the assertions verify V8 behavior rather than check_node_renderer_rollout_capacity. Consider gating them (skip unless system("node --version", out: File::NULL)) or moving them to a JS-side test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@react_on_rails/spec/lib/react_on_rails/doctor_spec.rb` around lines 3707 -
3761, Remove the three direct Node semantics examples from the Doctor spec, or
gate each example before invoking Open3 so the examples are skipped when Node is
unavailable on PATH. Keep the Doctor-focused tests independent of external Node
availability, preferably moving these runtime-semantics checks to an appropriate
JavaScript test suite.

3543-3546: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

No named subject for the method under test.

Every example calls doctor.send(:check_node_renderer_rollout_capacity) directly. A named subject (e.g. subject(:capacity_check) { doctor.send(:check_node_renderer_rollout_capacity) }) would satisfy the repo convention and remove the repetition across ~80 examples.

As per coding guidelines: "Ruby code must ... use named RSpec subjects such as subject(:method_result)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@react_on_rails/spec/lib/react_on_rails/doctor_spec.rb` around lines 3543 -
3546, Define a named RSpec subject for the result of
doctor.send(:check_node_renderer_rollout_capacity) near the affected examples,
then replace direct repeated calls with that subject. Preserve the existing
doctor setup and assertions while applying the subject consistently across the
capacity-check examples.

Source: Coding guidelines


3899-4427: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared fixture table and the JSON-doctor setup.

The array at Lines 4188-4405 is a near-subset of the one at Lines 3899-4174, and the json_doctor setup (described_class.new(format: :json, only: ...) + three allows + output capture) is repeated in at least seven examples. A single frozen fixture constant plus a run_json_capacity_check helper would keep these in sync as cases are added.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@react_on_rails/spec/lib/react_on_rails/doctor_spec.rb` around lines 3899 -
4427, The duplicated renderer source fixture arrays and JSON doctor setup should
be consolidated. Extract the shared cases into one frozen fixture constant,
reuse it in both example groups, and add a run_json_capacity_check helper
encapsulating described_class.new, the three stubs, output capture, and JSON
parsing; update the affected examples to call these shared symbols.
benchmarks/bench-node-renderer-rollout.mjs (2)

87-101: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Require the already-computed module paths.

configModulePath/vmModulePath are validated at Lines 89-94 but then discarded in favor of hardcoded relative specifiers, so the existence check and the actual load can drift. Using the computed absolute paths also resolves the two import/extensions ESLint errors.

♻️ Proposed change
-  const { buildConfig } = require('../packages/react-on-rails-pro-node-renderer/lib/shared/configBuilder');
-  const {
-    buildExecutionContext,
-    getVMContext,
-    resetVM,
-  } = require('../packages/react-on-rails-pro-node-renderer/lib/worker/vm');
+  const { buildConfig } = require(configModulePath);
+  const { buildExecutionContext, getVMContext, resetVM } = require(vmModulePath);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/bench-node-renderer-rollout.mjs` around lines 87 - 101, Update the
module loading in the benchmark to require the already-computed configModulePath
and vmModulePath values after validation, replacing the hardcoded relative
specifiers while preserving the existing imported symbols and missing-file error
handling.

Source: Linters/SAST tools


102-115: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fixture path is assumed to exist.

fixturePath is not covered by the Lines 89-94 preflight; if tests/fixtures/bundle.js moves, copyFileSync fails with a bare ENOENT instead of the actionable build hint. Adding it to the existing existsSync guard is a one-line change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/bench-node-renderer-rollout.mjs` around lines 102 - 115, Update
the existing preflight existsSync guard around the benchmark setup to also
validate fixturePath, alongside the paths already checked in lines 89-94. Keep
the existing actionable build hint and ensure the check runs before copyFileSync
uses fixturePath.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 29-40: Update the CHANGELOG entry for Issue 4810 to append the
required PR 4811 link and author attribution, matching the exact “[PR NNNN](...)
by [author](...)” format used by neighboring entries while preserving the inline
**[Pro]** marker.

In `@docs/oss/building-features/node-renderer/js-configuration.md`:
- Line 60: Change the “Sizing and draining the VM pool” heading from h3 to h2 to
satisfy markdownlint while preserving its existing slug and inbound links.

In `@react_on_rails/lib/react_on_rails/doctor.rb`:
- Around line 3512-3517: Update add_node_renderer_capacity_guidance to accept
the selected node renderer config path and use it in the remediation message
instead of hardcoding renderer/node-renderer.js. Pass node_renderer_config_path
from the caller, preserving the existing capacity guidance and legacy
client/node-renderer.js behavior handled by node_renderer_procfile_command.
- Around line 3226-3240: Update node_renderer_rsc_assignment_evidence so it
skips node.first only when that element is the node-type symbol; recursively
visit every element for plain Ripper child arrays, including the first
assignment. Preserve conditional detection for typed nodes and ensure
conflicting top-level assignments are both collected before producing any
inferred verdict.
- Around line 3392-3402: Update node_renderer_constructor_call? to return false
before calling String#rindex when call.begin(0) is zero, preventing the
negative-position scan from searching backward from the end of content; preserve
the existing constructor detection for calls with a positive starting offset.

---

Nitpick comments:
In `@benchmarks/bench-node-renderer-rollout.mjs`:
- Around line 87-101: Update the module loading in the benchmark to require the
already-computed configModulePath and vmModulePath values after validation,
replacing the hardcoded relative specifiers while preserving the existing
imported symbols and missing-file error handling.
- Around line 102-115: Update the existing preflight existsSync guard around the
benchmark setup to also validate fixturePath, alongside the paths already
checked in lines 89-94. Keep the existing actionable build hint and ensure the
check runs before copyFileSync uses fixturePath.

In `@packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts`:
- Around line 240-248: Update defaultVmPoolRolloutDrainTimeout to parse
VM_POOL_ROLLOUT_DRAIN_TIMEOUT with Number instead of parseFloat, preserving the
existing fallback to 60 when the environment variable is absent and ensuring
malformed values fail consistently with defaultMaxVMPoolSize.

In `@packages/react-on-rails-pro-node-renderer/tests/configBuilder.test.ts`:
- Around line 179-216: Add an ENV-focused parameterized test alongside the
existing MAX_VM_POOL_SIZE invalid-ENV coverage, using
loadConfigBuilderWithMockedLogger and buildConfig to verify invalid
VM_POOL_ROLLOUT_DRAIN_TIMEOUT strings—including a suffixed value such as
“45s”—throw the expected positive-seconds validation error. Keep the
explicit-configuration tests unchanged.

In `@react_on_rails/lib/react_on_rails/doctor.rb`:
- Around line 124-130: Update NODE_RENDERER_LAUNCHER_PATHS to derive its
launcher filenames from NodeRendererProcfile::DEFAULT_COMMANDS instead of
maintaining a hardcoded list. Ensure check_launcher_procfiles and related
launcher checks continue using the derived collection so newly added Procfile
variants remain aligned automatically.

In `@react_on_rails/spec/lib/react_on_rails/doctor_spec.rb`:
- Around line 3707-3761: Remove the three direct Node semantics examples from
the Doctor spec, or gate each example before invoking Open3 so the examples are
skipped when Node is unavailable on PATH. Keep the Doctor-focused tests
independent of external Node availability, preferably moving these
runtime-semantics checks to an appropriate JavaScript test suite.
- Around line 3543-3546: Define a named RSpec subject for the result of
doctor.send(:check_node_renderer_rollout_capacity) near the affected examples,
then replace direct repeated calls with that subject. Preserve the existing
doctor setup and assertions while applying the subject consistently across the
capacity-check examples.
- Around line 3899-4427: The duplicated renderer source fixture arrays and JSON
doctor setup should be consolidated. Extract the shared cases into one frozen
fixture constant, reuse it in both example groups, and add a
run_json_capacity_check helper encapsulating described_class.new, the three
stubs, output capture, and JSON parsing; update the affected examples to call
these shared symbols.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2d0d7869-86a9-4294-bbde-16890a8cfac5

📥 Commits

Reviewing files that changed from the base of the PR and between 822b4c3 and 5d8b023.

📒 Files selected for processing (14)
  • CHANGELOG.md
  • benchmarks/bench-node-renderer-rollout.mjs
  • docs/oss/api-reference/doctor.md
  • docs/oss/building-features/node-renderer/container-deployment.md
  • docs/oss/building-features/node-renderer/js-configuration.md
  • docs/pro/rolling-deploy-adapters.md
  • packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts
  • packages/react-on-rails-pro-node-renderer/src/worker/vm.ts
  • packages/react-on-rails-pro-node-renderer/src/worker/vmSourceMapSupport.ts
  • packages/react-on-rails-pro-node-renderer/tests/configBuilder.test.ts
  • packages/react-on-rails-pro-node-renderer/tests/vm.test.ts
  • react_on_rails/lib/react_on_rails/doctor.rb
  • react_on_rails/lib/react_on_rails/doctor_schema.rb
  • react_on_rails/spec/lib/react_on_rails/doctor_spec.rb

Comment thread CHANGELOG.md
Comment thread docs/oss/building-features/node-renderer/js-configuration.md Outdated
Comment thread react_on_rails/lib/react_on_rails/doctor.rb Outdated
Comment thread react_on_rails/lib/react_on_rails/doctor.rb
Comment thread react_on_rails/lib/react_on_rails/doctor.rb Outdated
Copilot AI review requested due to automatic review settings July 28, 2026 16:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@justin808

Copy link
Copy Markdown
Member Author

+ci-status

@justin808

Copy link
Copy Markdown
Member Author

+ci-run-hosted

@github-actions

Copy link
Copy Markdown
Contributor

CI Status

Head SHA: cc84f2653fea
Changed files: 14
Docs-only heuristic (matches ci-changes-detector metadata paths): no
ready-for-hosted-ci label: present
force-full-hosted-ci label: absent
Current hosted-CI waiver: not present for this SHA
Automatic release-target hosted mode: inactive
Observed exact-head coverage: modes[missing=9]; successful=0, pending=0, failed=0, missing=9

Optimized hosted CI is enabled for this PR.

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Hosted CI Requested

Triggered 9 workflow(s) for cc84f2653fea.
Skipped 0 workflow(s) with equivalent exact-head coverage.
Mode: optimized hosted CI (path-selected by script/ci-changes-detector).
Added ready-for-hosted-ci, so future commits will keep running optimized hosted CI until +ci-stop-hosted is used.

View progress in the Actions tab.

Comment thread react_on_rails/lib/react_on_rails/doctor.rb
@claude

claude Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review summary

Overview: this PR raises the default Node Renderer maxVMPoolSize from 2 to 4, adds a rollout-aware "generation" tracking layer with a configurable drain timeout so old/new bundle sets survive a rolling deploy without repeated VM rebuilds, adds a new node_renderer_rollout_capacity Doctor check, and updates docs/changelog/benchmark accordingly. Overall the core vm.ts change (generation tracking, timer-based drain, rate-limited pressure logging, cumulative counters) is well tested and the self-healing timer-rescheduling logic (comment at scheduleGenerationRetirement) checks out against the test suite, including the "last overlap request belongs to the old set" recovery case.

Two issues found, posted as inline comments:

  1. packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts (lines 241-247)defaultMaxVMPoolSize() uses env.MAX_VM_POOL_SIZE != null so an empty-string env value correctly fails validation and exits fast. defaultVmPoolRolloutDrainTimeout() uses a truthy check instead, so VM_POOL_ROLLOUT_DRAIN_TIMEOUT="" silently falls back to the default 60 with no warning — inconsistent with the "fail fast on bad config" goal of this PR, and untested (the env-rejection test list doesn't include '' for the drain timeout, unlike the pool-size test list).

  2. react_on_rails/lib/react_on_rails/doctor.rb (~line 3136 onward) — the new check_node_renderer_rollout_capacity check and its ~25 helper methods implement a hand-rolled regex-based JS literal detector (balanced braces, string/comment/template masking, constructor-vs-call disambiguation, alias/spread/getter detection, etc.), backed by ~1,900 lines of adversarial-case specs. This is very thorough, but regexes can't really parse JS, so the test suite is effectively an open-ended list of edge cases to keep patching. Since the check already fails closed to unverified for anything unprovable, it may be worth trading this bespoke parser for either a real JS parse (e.g. shelling out to Node) or a smaller scope (only claim observed for env-derived values). Flagging as a maintainability/complexity concern rather than a correctness bug.

Minor positive note: switching maxVMPoolSize/vmPoolRolloutDrainTimeout validation from throw new Error to log.error + process.exit(1) in buildConfig() matches the existing validation style already used a few lines below for port/password/deprecated-option checks in the same function, so it's consistent with current conventions rather than a new pattern.

No security concerns beyond the above (the Doctor check only reads local files/env, no fetch/upload; VM pool changes are purely in-process memory management). Test coverage is extensive on both the Node and Ruby sides.

Copilot AI review requested due to automatic review settings August 6, 2026 12:36
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@justin808

Copy link
Copy Markdown
Member Author

+ci-status

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

CI Status

Head SHA: 18717b16f946
Changed files: 14
Docs-only heuristic (matches ci-changes-detector metadata paths): no
ready-for-hosted-ci label: present
force-full-hosted-ci label: absent
Current hosted-CI waiver: not present for this SHA
Automatic release-target hosted mode: inactive
Observed exact-head coverage: modes[missing=9]; successful=0, pending=0, failed=0, missing=9

Optimized hosted CI is enabled for this PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@react_on_rails/spec/lib/react_on_rails/doctor_spec.rb`:
- Around line 3721-3726: Update the test around
check_node_renderer_rollout_capacity to poll until child_pid_path exists and
contains a complete, nonzero PID before calling File.read(child_pid_path).to_i,
reusing the polling approach from the later example while preserving the
existing process-exit assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 02d635b3-a740-4d1c-acaf-43fb44df5854

📥 Commits

Reviewing files that changed from the base of the PR and between f12eca3 and 18717b1.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • docs/oss/building-features/node-renderer/container-deployment.md
  • docs/oss/building-features/node-renderer/js-configuration.md
  • react_on_rails/lib/react_on_rails/doctor.rb
  • react_on_rails/spec/lib/react_on_rails/doctor_spec.rb
🚧 Files skipped from review as they are similar to previous changes (3)
  • CHANGELOG.md
  • docs/oss/building-features/node-renderer/js-configuration.md
  • docs/oss/building-features/node-renderer/container-deployment.md

Comment thread react_on_rails/spec/lib/react_on_rails/doctor_spec.rb

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated no new comments.

Suppressed comments (1)

packages/react-on-rails-pro-node-renderer/tests/vm.test.ts:980

  • warningSpy (created via jest.spyOn(...).mockImplementation(...)) is never restored. With the repo’s Jest config using clearMocks: true (not restoreMocks/resetMocks), that mocked implementation can leak into subsequent tests and alter behavior.
      expect(getVMPoolDiagnostics().hardLimitEvictions).toBe(4);
      expect(
        warningSpy.mock.calls.filter(
          ([payload]) =>
            typeof payload === 'object' &&

Comment thread react_on_rails/lib/react_on_rails/doctor.rb Outdated
@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review summary

This is a large, well-documented PR (VM pool bump 2→4, rollout-generation tracking/draining, config validation, a new Doctor provenance check, and race-safe process cleanup). I focused on the VM pool/rollout logic, config validation, and the new Doctor JS-provenance parser. Two inline comments were posted on concrete bugs; a third target (a pre-existing, unrelated-to-this-diff function) couldn't take an inline comment since GitHub only allows commenting on lines touched by the diff, so it's noted below instead.

Findings (inline)

  1. packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts:498-505 — the new fail-fast validation for maxVMPoolSize/vmPoolRolloutDrainTimeout runs before numeric coercion, so a valid env-derived string passed through userConfig (the same pattern the file explicitly supports for port a few lines below, with a comment calling this out) now crashes the renderer at startup. Needs Number(...) coercion before the check, mirroring port.
  2. react_on_rails/lib/react_on_rails/doctor.rb:3606-3608 (node_renderer_call_reachability_proven?) — recomputes node_renderer_delimiter_stack(content) over the entire file content on every candidate call match found in node_renderer_single_reachable_call's filter_map, even though the result doesn't depend on the match. With the 1 MB config-file cap and a file containing many textual occurrences of reactOnRailsProNodeRenderer(, this is effectively O(matches × file size) and can make doctor hang. Should be hoisted out of the loop and computed once.

Related observation (not inline-commentable — pre-existing code untouched by this diff)

react_on_rails/lib/react_on_rails/doctor.rb:4865-4868 (signal_rsc_dist_tag_process, used by the npm view ... dist-tags cleanup path) still falls back to signaling the bare positive pid on Errno::ESRCH:

def signal_rsc_dist_tag_process(signal, pid)
  Process.kill(signal, -pid)
rescue Errno::ESRCH
  Process.kill(signal, pid)
end

This is exactly the reused-PID race this PR's description says has been eliminated ("never falls back to a positive PID that could have been reused"), and the new sibling signal_node_renderer_syntax_check (same file, ~doctor.rb:3476) was explicitly hardened against this identical pattern with a comment explaining why. This function is unchanged by this PR (confirmed against origin/main), so it's out of scope for this diff, but since process-cleanup race-safety is one of this PR's stated goals and the fixed/unfixed versions now sit in the same file, it's worth a fast-follow to bring signal_rsc_dist_tag_process in line with signal_node_renderer_syntax_check.

Reviewed and found solid

  • vm.ts rollout-generation/eviction logic: LRU hard-cap eviction and generation-based draining are correctly composed; in-flight requests hold direct VMContext references so pool eviction never breaks a running request. The drain timer is .unref()'d and cancelled/rescheduled correctly. Generation metadata and the oversized-source-map-path cache are both explicitly bounded. No per-request state leaks into module scope (consistent with the RSC cross-request-isolation invariant). Tests in vm.test.ts meaningfully exercise the new behavior rather than just the happy path.
  • Doctor's new JS-provenance regexes: no ReDoS pattern found (alternatives are first-character-disjoint), no command injection (the syntax-check subprocess uses Process.spawn with an argument array, never shell interpolation), no path traversal (file paths come from constants or a tightly-anchored capture).

Design-level note (non-blocking)

The new Doctor provenance check is a ~700-line hand-rolled JS analyzer (masking, delimiter-stack matching, ambiguity heuristics) implemented in Ruby string/regex operations rather than a real parser. It already shells out to node --check for syntax validation — extracting the actual config value via Node instead of re-deriving it with bespoke regex heuristics would likely be both simpler and more robust against the long tail of edge cases this PR's commit history shows it iterating through.

Copilot AI review requested due to automatic review settings August 6, 2026 13:01
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@justin808

Copy link
Copy Markdown
Member Author

Addressed the complete review wave scanned since 2026-07-28T19:13:13Z at exact head f2ac4aa2f6f2930994ca24501ecf00b4fc603e6e.

Mattered

Optional

  • Restored Jest spies after every rollout VM-pool test so a suppressed warning spy cannot leak into later tests.

Skipped

  • Skipped duplicate CodeRabbit/Claude summary text and CI/reviewer status comments after handling their underlying findings.
  • Declined the Claude summary's pre-existing positive-PID fallback observation because it concerns the separate RSC dist-tag helper outside this PR's changed behavior and has no review thread on this diff.
  • Declined the architectural suggestion to replace the conservative bounded analyzer with a full JavaScript parser; this PR intentionally fails closed on ambiguous input and the suggestion does not identify an incorrect current-head result.

No deferred tracking issue was created. The exact-head configured reviewers and hosted CI are running again; any new feedback after this comment belongs to the next review scan.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated no new comments.

Suppressed comments (2)

react_on_rails/lib/react_on_rails/doctor.rb:3759

  • Launcher MAX_VM_POOL_SIZE assignments are validated with /\A[1-9]\d*\z/, which rejects values that the renderer accepts (e.g., leading zeros). This can prevent Doctor from recognizing an otherwise-static proven assignment and push the evidence path into the ambiguous/unverified bucket.
      return { state: :unverified, reason: "dynamic_or_ambiguous_launcher_vm_pool_assignment" } unless
        assignments.length == assignment_matches.length &&
        assignments.all? { |assignment| assignment.match?(/\A[1-9]\d*\z/) } &&
        assignments.uniq.one?

react_on_rails/lib/react_on_rails/doctor.rb:3733

  • Doctor treats MAX_VM_POOL_SIZE as invalid unless it matches /\A[1-9]\d*\z/, but the Node renderer accepts other positive-integer string forms (e.g., leading zeros or surrounding whitespace) because configBuilder coerces with Number() then checks integer-ness. This can misreport a valid setting as "invalid_doctor_process_environment_value" and unnecessarily degrade the rollout-capacity check to unverified.

This issue also appears on line 3756 of the same file.

      env_value = ENV.fetch("MAX_VM_POOL_SIZE", nil)
      if env_value
        unless env_value.match?(/\A[1-9]\d*\z/)
          return { state: :unverified, reason: "invalid_doctor_process_environment_value" }
        end

Comment thread react_on_rails/lib/react_on_rails/doctor.rb Outdated
Comment thread react_on_rails/lib/react_on_rails/doctor.rb
Copilot AI review requested due to automatic review settings August 6, 2026 13:13
Comment thread react_on_rails/lib/react_on_rails/doctor.rb
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Comment thread react_on_rails/spec/lib/react_on_rails/doctor_spec.rb
@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review summary

Focused on the core VM-pool rollout logic (vm.ts, configBuilder.ts) and the new Doctor check (doctor.rb). Left 4 inline comments; details below.

vm.ts / configBuilder.ts — looks solid

The rollout-generation tracking (bundleGenerations), the injectable VMPoolClock for deterministic drain-timeout testing, the unref'd single timer, and the interaction between the absolute LRU hard cap (manageVMPoolSize) and generation-based draining (retireDrainedBundleGenerations) are well thought through. Source-map registration retain/release counting correctly accounts for execution contexts outliving pool eviction. configBuilder.ts's new maxVMPoolSize/vmPoolRolloutDrainTimeout validation (fail-fast on invalid MAX_VM_POOL_SIZE) is a reasonable, clearly-changelogged behavior change. No correctness issues found here.

doctor.rbcheck_node_renderer_rollout_capacity (~600 lines, ~40 helper methods)

This is a hand-rolled regex-based static analyzer for JavaScript config files, used to try to prove a maxVMPoolSize literal without executing the file. Two concrete process-lifecycle bugs in the node --check spawn/cleanup path (inline comments):

  • A PID-reuse race: Process.wait2 reaps the child as a side effect inside a liveness check, but the surrounding cleanup loop keeps signaling by the same (now possibly-recycled) PID afterward.
  • An unbounded blocking Process.wait after SIGKILL with no timeout, which can defeat the whole bounded-time design if the child is ever stuck in uninterruptible I/O.

Both are low-probability (this only runs node --check against a local tempfile) but real gaps in an otherwise very careful cleanup routine.

Separately, I'd flag the overall approach as a maintainability concern worth a conscious team decision (left as a longer inline comment): the PR's own commit history needed ~20 sequential "fail closed on X" follow-ups to harden this regex parser against edge cases (unicode escapes, rebound require, template literals, conditional calls, etc.), and ~2800 of the added spec lines exist mostly to characterize this parser's failure modes. The fail-closed design keeps this safe (I didn't find a case producing a false "sufficient capacity" positive), but every new JS syntax form is a potential future patch. Since this code already shells out to node --check, using real JS parsing (via Node) instead of regex could collapse most of this surface area.

Minor

  • doctor_spec.rb: several fixture tables generate a near-duplicate JSON-output test alongside the plain-text test for every case, roughly doubling an already-huge spec file for limited extra coverage (inline comment with example).

Not reviewed in depth

benchmarks/bench-node-renderer-rollout.mjs (dev-only benchmark tooling) and the docs changes — skimmed, no issues spotted, but not scrutinized as closely as the runtime/diagnostic code.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated no new comments.

Suppressed comments (1)

packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts:345

  • envValuesUsed() uses truthiness to decide whether rollout settings came from ENV, which can misreport the sanitized metadata when a user explicitly provides a (falsy) value (e.g., via config file templating) and an ENV value is also present. Using an in userConfig check is consistent with other options in this object and avoids false attribution to ENV.
    MAX_VM_POOL_SIZE: !userConfig.maxVMPoolSize && env.MAX_VM_POOL_SIZE,
    VM_POOL_ROLLOUT_DRAIN_TIMEOUT: !userConfig.vmPoolRolloutDrainTimeout && env.VM_POOL_ROLLOUT_DRAIN_TIMEOUT,

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@justin808

justin808 commented Aug 6, 2026

Copy link
Copy Markdown
Member Author

Review disposition — exact candidate 5ec6f3220

All review feedback has been triaged against 5ec6f3220ab64833a4c404ca176cdae97dd61956.

Correctness findings fixed

  • Kept the syntax-check process-group leader unreaped until negative-group TERM/KILL probing and signalling complete, preventing PID/PGID reuse exposure.
  • Replaced the unbounded final Process.wait with detached, grace-bounded caller waiting while preserving eventual reap ownership.
  • Earlier in the review wave: restricted rollout numeric coercion to numbers and nonempty strings, made the Doctor delimiter scan single-pass, completed PID publication before cleanup assertions, and restored Jest spies.
  • Independent QA also found and drove the boolean/array rollout-config fail-closed regressions before this final candidate.

Discussed / optional findings

  • Retained the narrow fail-closed static Doctor analyzer instead of adding a JavaScript parser dependency. Ambiguous provenance never produces a pass; a parser extraction would be separate architecture work only if the supported grammar expands.
  • Retained explicit per-fixture JSON assertions because the public Doctor schema and human reason must stay paired for each hostile provenance case; no final-candidate abstraction churn was introduced.

Current-head evidence

  • All four new Claude review threads were answered and resolved.
  • Unresolved review-thread count: zero.
  • Exact-head Claude review: passed with no new findings.
  • Exact-head CodeRabbit check: passed.
  • Independent QA: PASS, P0–P3 none; Doctor 730/730; Node Renderer 41 suites and 614/614 tests; strict receipt replay satisfied.
  • Hosted CI: 49 passing, zero failures, zero pending.

No follow-up issue is proposed: the optional suggestions do not represent deferred correctness work for #4810.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-claimed Advisory mirror of an active private agent coordination claim ready-for-hosted-ci Run optimized hosted GitHub CI for this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Node Renderer] Rolling deployments thrash VM contexts when old and new server/RSC bundles exceed default pool

2 participants