Skip to content

feat(node): bare-metal worker autoscaler - #542

Merged
kartikeya-27 merged 8 commits into
ByteVeda:masterfrom
stromanni:feat/node-bare-metal-autoscaler
Jul 25, 2026
Merged

feat(node): bare-metal worker autoscaler#542
kartikeya-27 merged 8 commits into
ByteVeda:masterfrom
stromanni:feat/node-bare-metal-autoscaler

Conversation

@stromanni

@stromanni stromanni commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Closes #518.

Node was the only SDK without a local autoscaler — Python has the process-based AutoscaleController + CLI, Java has the thread-pool Autoscaler, and Node had only the KEDA serveScaler. This adds the bare-metal one.

What it does

serveAutoscaler(queue, { app }) runs a control loop that spawns and drains worker processes to track queue depth, for hosts without Kubernetes. The formula mirrors Python's (and the Kubernetes HPA):

depthDesired = ceil(pending / targetQueueDepthPerWorker)
utilDesired  = ceil(workers × (utilisation / targetUtilisation))
desired      = clamp(max(depthDesired, utilDesired), minWorkers, maxWorkers)

with per-direction stabilisation windows (scale-up immediate, scale-down 5 min), a 10% tolerance band, an overload override, and crash replacement up to minWorkers.

taskito --db taskito.db autoscale ./app.js --min-workers 2 --max-workers 20

Design notes

  • Workers are OS processes, not threads. A Node process is single-threaded, so real parallelism means more processes — and a separate process heartbeats on its own, which is what lets a crashed worker be noticed and replaced. Java's thread-pool resizing has no equivalent here, since a native worker's concurrency is fixed at start.
  • The child is bootstrapped inline (node --input-type=module -e), importing the user's app module and driving it through the public Queue API. Nothing has to locate a dist/ entry, so it behaves the same from source, from a bundle, and from an install. Paths arrive via argv, never interpolated into the source.
  • concurrencyPerWorker is applied, not just declared. Python's threads_per_worker is an operator promise the controller can't verify; here the autoscaler spawns the workers, so it sets their concurrency and the utilisation signal's capacity is right by construction.
  • Metrics follow the workers. With queues set, the depth signal reads only those queues rather than global stats.
  • Draining workers stop counting as live immediately, so a tick mid-drain doesn't terminate them twice.

Verification

  • 15 unit tests (decision formula, config validation, tick behaviour against a stubbed pool) and 2 integration tests that spawn real worker processes — one drains a job then exits cleanly, one is SIGKILLed and reported as crashed exactly once.
  • Full Node suite: 70 files / 416 tests pass. The 10 dashboard files fail on master too (vite isn't installed in the dashboard/ workspace locally) and are untouched by this change.
  • typecheck, biome check, and the docs typecheck / lint / check:parity are all clean.
  • End-to-end smoke run: 200 jobs seeded, the pool scaled 1 → 6, drained the queue, fell back to 1 when idle, and drained cleanly on SIGTERM with no orphaned processes.

Docs

New node/guides/operations/autoscaling guide, plus entries in the operations index, both CLI pages, and the capabilities table.

Summary by CodeRabbit

  • New Features

    • Added bare-metal autoscaling for Node.js worker processes based on queue depth and utilization.
    • Added the autoscale CLI command with configurable worker limits and scaling options.
    • Added graceful worker draining, crash replacement, stabilization, and shutdown handling.
  • Documentation

    • Added comprehensive autoscaling guides, CLI references, examples, and operational guidance.
    • Documented integration options for non-Kubernetes environments and KEDA.
  • Tests

    • Added coverage for scaling decisions, worker lifecycle management, crash recovery, and configuration validation.

Spawns and drains worker processes to track queue depth on hosts without
Kubernetes, mirroring Python's AutoscaleController: HPA depth and utilisation
signals, per-direction stabilisation windows, a tolerance band, and crash
replacement.
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@stromanni, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 37 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8e1d2afa-2a39-403b-b81d-8dc534bc77a5

📥 Commits

Reviewing files that changed from the base of the PR and between 479db07 and 959e9ab.

📒 Files selected for processing (6)
  • docs/content/docs/node/guides/operations/autoscaling.mdx
  • sdks/node/src/autoscale/config.ts
  • sdks/node/src/autoscale/controller.ts
  • sdks/node/src/autoscale/index.ts
  • sdks/node/src/autoscale/processManager.ts
  • sdks/node/test/observability/autoscale.test.ts
📝 Walkthrough

Walkthrough

Changes

Node bare-metal autoscaler

Layer / File(s) Summary
Scaling configuration and decisions
sdks/node/src/autoscale/config.ts, sdks/node/src/autoscale/controller.ts, sdks/node/test/observability/autoscale.test.ts, docs/content/docs/node/guides/operations/autoscaling.mdx
Adds validated autoscaler options, HPA-shaped worker calculations, tolerance handling, stabilization windows, and corresponding tests and documentation.
Worker process lifecycle
sdks/node/src/autoscale/processManager.ts, sdks/node/test/integrations/autoscale.test.ts, docs/content/docs/node/guides/operations/autoscaling.mdx
Adds detached worker spawning, graceful termination with escalation, crash tracking, signal handling, and integration coverage.
Autoscaler control loop
sdks/node/src/autoscale/controller.ts, sdks/node/test/observability/autoscale.test.ts, docs/content/docs/node/guides/operations/autoscaling.mdx
Coordinates queue metric reads, minimum-worker replacement, scaling actions, stabilization history, shutdown, and runtime behavior tests.
Public API and CLI integration
sdks/node/src/autoscale/index.ts, sdks/node/src/index.ts, sdks/node/src/cli/commands/autoscale.ts, sdks/node/src/cli/commands/index.ts, sdks/node/src/cli/index.ts, docs/content/docs/node/api-reference/cli.mdx, docs/content/docs/node/guides/operations/cli.mdx, docs/content/docs/node/guides/operations/autoscaling.mdx
Exports autoscaling APIs and registers the autoscale CLI command with worker and control-loop options.
Operations documentation and release metadata
docs/content/docs/node/guides/operations/index.mdx, docs/content/docs/node/guides/operations/meta.json, docs/content/docs/node/getting-started/capabilities.mdx, CHANGELOG.md, docs/content/docs/node/guides/operations/autoscaling.mdx
Adds autoscaling navigation, capability listings, operational comparisons, and changelog coverage.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Autoscaler
  participant Queue
  participant WorkerProcessManager
  Autoscaler->>Queue: Read queue statistics
  Queue-->>Autoscaler: Return pending and running counts
  Autoscaler->>Autoscaler: Compute and stabilize desired worker count
  Autoscaler->>WorkerProcessManager: Spawn or drain workers
  WorkerProcessManager-->>Autoscaler: Update worker pool state
Loading

Suggested labels: rust, ci, docs, tests, workflows

Suggested reviewers: pratyush618

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding a bare-metal worker autoscaler for Node.
Linked Issues check ✅ Passed The PR implements the requested Node bare-metal autoscaler rather than leaving it unsupported, satisfying issue #518.
Out of Scope Changes check ✅ Passed The code, tests, CLI, and docs all support the autoscaler objective and do not introduce clear unrelated changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

🤖 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 `@sdks/node/src/autoscale/controller.ts`:
- Around line 159-218: Track the currently executing tick promise in the
autoscaler, and have stop() await that promise after disabling scheduling and
before calling manager.shutdown(). Ensure the tick promise is cleared when it
settles, while preserving the existing non-overlapping schedule behavior and
making repeated stop() calls safe.
- Around line 220-257: Update applyWindows to record every tick’s raw desired
recommendation in the relevant stabilization history, including ticks where
desired equals current, so a transient low recommendation is compared with the
prior stable recommendation. Preserve the existing min/max aggregation semantics
for scale-up and scale-down windows, and ensure a failed gatherMetrics reading
cannot immediately reduce a pool above minWorkers.

In `@sdks/node/src/autoscale/processManager.ts`:
- Around line 80-118: Move the child.once("error", ...) registration in
spawnWorker() to immediately after spawn(), before checking child.pid or
throwing for an undefined pid. Keep the existing error logging and forget(pid,
record) behavior, while ensuring the listener safely handles failed spawns where
no pid is available.

In `@sdks/node/test/observability/autoscale.test.ts`:
- Around line 205-214: The autoscaler regression test currently covers only a
pool already at minWorkers. Extend the test around autoscaler and metricsSource
with a case that seeds more workers than the configured minimum (for example, 5
versus 1), simulates one metrics-read failure, and asserts the decision
preserves the current pool size without spawning or terminating workers.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8ecdb1b7-db1c-4b07-a876-0e21136f620b

📥 Commits

Reviewing files that changed from the base of the PR and between ed67d79 and 479db07.

📒 Files selected for processing (17)
  • CHANGELOG.md
  • docs/content/docs/node/api-reference/cli.mdx
  • docs/content/docs/node/getting-started/capabilities.mdx
  • docs/content/docs/node/guides/operations/autoscaling.mdx
  • docs/content/docs/node/guides/operations/cli.mdx
  • docs/content/docs/node/guides/operations/index.mdx
  • docs/content/docs/node/guides/operations/meta.json
  • sdks/node/src/autoscale/config.ts
  • sdks/node/src/autoscale/controller.ts
  • sdks/node/src/autoscale/index.ts
  • sdks/node/src/autoscale/processManager.ts
  • sdks/node/src/cli/commands/autoscale.ts
  • sdks/node/src/cli/commands/index.ts
  • sdks/node/src/cli/index.ts
  • sdks/node/src/index.ts
  • sdks/node/test/integrations/autoscale.test.ts
  • sdks/node/test/observability/autoscale.test.ts

Comment thread sdks/node/src/autoscale/controller.ts
Comment thread sdks/node/src/autoscale/controller.ts
Comment thread sdks/node/src/autoscale/processManager.ts
Comment thread sdks/node/test/observability/autoscale.test.ts
A spawn that fails asynchronously emits 'error' with no pid; registering the
listener after the throw left it unhandled, taking the autoscaler down.
stop() cleared the timer and drained immediately, so a tick still awaiting its
metrics read could spawn afterwards and leak a detached worker.
Only direction changes were buffered, so the first dip after a stable stretch
had nothing to smooth against and took effect at once — including a dip caused
by a failed metrics read.
@kartikeya-27
kartikeya-27 merged commit 2a22048 into ByteVeda:master Jul 25, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Node: bare-metal autoscaler or documented as unsupported

2 participants