Skip to content

fix(runtime): clamp fixed-port applications to one worker when reusePort is not available - #5076

Open
mcollina wants to merge 1 commit into
fix/restore-per-worker-port-assignmentfrom
fix/clamp-workers-without-reuseport
Open

fix(runtime): clamp fixed-port applications to one worker when reusePort is not available#5076
mcollina wants to merge 1 commit into
fix/restore-per-worker-port-assignmentfrom
fix/clamp-workers-without-reuseport

Conversation

@mcollina

@mcollina mcollina commented Aug 15, 2026

Copy link
Copy Markdown
Member

Fixes #5070

Stacked on #5075 (restore portAssignment: perWorkerIncrement) — review only the last commit; retarget to v4 once #5075 lands.

Summary

The guard that clamped a multi-worker application to a single worker on platforms without SO_REUSEPORT (macOS, Windows) was removed with the entrypoint in #5014, because it was gated on application.entrypoint. On those platforms an application with a fixed server.port and workers > 1 failed to start its additional workers with a raw (and, for @platformatic/node factories, delayed) error instead of degrading with a warning.

Changes

  • Restore the clamp, gated on the listener configuration (packages/runtime/lib/runtime.js, #setupApplication): the listener is owned by the capability, so when reusePort is not available and the application has workers > 1 (or dynamic scaling), the runtime sets up the first worker, reads its configuration via ITC (getApplicationConfig) and, if server.port is a fixed port and server.portAssignment is not perWorkerIncrement, logs a warning naming the OS limitation and the portAssignment alternative, sets workers to { dynamic: false, static: 1 } (which also disables the dynamic scaler for that application) and skips the other workers. Applications with perWorkerIncrement, port 0, or no managed listener are unaffected.
  • Meaningful error when workers of the same application collide anyway (e.g. after a manual updateApplicationsResources scale up, or with reuseTcpPorts: false):
    • packages/runtime/lib/worker/itc.js: the start handler now sends the failure as a plain object, so error properties such as port/address survive the structured clone (previously the error.port check in #startWorker could never match through ITC).
    • #startWorker reports the new PLT_RUNTIME_WORKER_EADDR_IN_USE (WorkerAddressInUseError) when the port is held by another worker of the same application, keeps PLT_RUNTIME_EADDR_IN_USE for another application, and leaves the original EADDRINUSE when the owner is an external process (instead of the odd "another process" and "app" message). Both codes are non-retryable.
    • (The related @platformatic/node unhandled-rejection fix, which turned a listen error into a 30s start timeout, moved to fix(runtime): restore per-worker port assignment (portAssignment: perWorkerIncrement) #5075 since its Windows CI needed it.)
  • Docs: workers/reuseTcpPorts sections and errors.md.

Tests

  • packages/runtime/test/multiple-workers/no-reuse-port.test.js: simulates the missing reusePort feature (the runtime runs in-process, so features.node.reusePort is patched) and covers the clamp with static workers, the dynamic scaling case, no clamp with perWorkerIncrement and with an ephemeral port, plus the same-application collision error (forced with reuseTcpPorts: false, so it runs on Linux too).
  • packages/runtime/test/ports.test.js now asserts the exact PLT_RUNTIME_EADDR_IN_USE error for two applications on the same port.

Assisted-by: Claude Code:claude-fable-5

…sePort

The guard that clamped a multi-worker application to a single worker on
platforms without SO_REUSEPORT (macOS, Windows) was removed with the
entrypoint in #5014, since it was gated on the entrypoint flag.

Restore it gated on the application listener configuration: since the
listener is owned by the capability, the runtime sets up the first worker,
inspects its configuration and, if the application would share a fixed
port between multiple workers (i.e. `server.portAssignment` is not
`perWorkerIncrement`), warns and clamps the workers to 1, disabling
dynamic scaling as well.

Also make the failure meaningful when workers of the same application
still collide on a port (e.g. after a manual scale up):
- the start error sent by the worker keeps its properties (like `port`)
  when crossing the thread boundary;
- the runtime reports `PLT_RUNTIME_WORKER_EADDR_IN_USE`, naming the
  application, the reusePort constraint and the `portAssignment` fix,
  when the port is held by another worker of the same application;
- `@platformatic/node` no longer reports a listen error of a factory
  application as an unhandled rejection, which made the worker exit
  before the start error could reach the runtime (start timeout).

Fixes #5070

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Matteo Collina <hello@matteocollina.com>
@mcollina
mcollina force-pushed the fix/clamp-workers-without-reuseport branch from 447127d to edf8393 Compare August 16, 2026 00:46
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.

1 participant