HarperFast / HarperFast/harper
restartWorkers(name, 0) abandons the restart after one worker instead of throttling to it
- Dominant language
- JavaScript
- Stars
- 89
- Forks
- 10
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 200
Description
## Summary
`restartWorkers(name, 0)` — or any falsy-but-not-NaN `maxWorkersDown`, such as `null` or `''` — does not restart the pool. It abandons the restart after the first worker and returns reporting the rest of the pool as still on the old code, with a misleading log line claiming zero replacements failed to start.
## Mechanism
`server/threads/manageThreads.js` normalizes a sub-1 `maxWorkersDown` as a *ratio* of the pool:
```js
} else if (maxWorkersDown < 1) {
// we accept a ratio of workers, and compute absolute maximum being down at a time from the total number of
// threads
maxWorkersDown = maxWorkersDown * workers.length;
}
```
`0 < 1` is true (and `null < 1` and `'' < 1` are true by coercion), so the ratio branch multiplies to `0`. `maxWorkersDown` is then `0` for the rest of the function, and the loop's replacement-failure cutoff is an unconditional truth on the very first iteration:
```js
if (replacementsFailedToStart >= maxWorkersDown) {
```
`0 >= 0` is `true` with no replacement having failed, so the loop breaks after one worker. It logs `0 replacement worker thread(s) did not start; stopping this restart with N worker(s) still on the previous code` and returns `{ workersKeptOnOldCode: N }`.
The throttle point above it, `if (waitingToFinish.length >= maxWorkersDown) await Promise.race(waitingToFinish)`, is harmless by comparison: the array always holds at least the promise pushed on the line before, so the race resolves and the restart merely serializes. The break is the defect.
## Reachability
No in-repo caller passes a falsy value today. `components/operations.js:867`, `components/operations.js:1441`, `bin/restart.ts:67`, `bin/restart.ts:328` and `components/componentLoader.ts:1202` all pass a literal `undefined`; `shutdownWorkers` passes `Infinity`. The parameter is not bound to any network-facing field, so this is latent rather than live — but the ratio form is a documented part of the signature ("we accept a ratio of workers"), and `0` is the natural boundary of that form.
## Suggested fix
Clamp the ratio result to the documented minimum, e.g. `maxWorkersDown = Math.max(Math.floor(maxWorkersDown * workers.length), 1)`, so a ratio that rounds below one worker still restarts one at a time. The `< 1` branch and the failure cutoff should not be able to disagree about whether zero is a legal throttle.
## Verification route
Unit: `unitTests/server/threads/restartThrottle.test.js` already has the harness. A case passing `0` and asserting every selected worker is restarted (and `workersKeptOnOldCode === 0`) fails on current `main` and passes with the clamp.
## Provenance
Found by the cross-model adjudication leg while reviewing https://github.com/HarperFast/harper/pull/2600 (the #2491 fix). It is pre-existing on `main` and untouched by that PR, which clamps `NaN` only and deliberately leaves `Infinity` alone as `shutdownWorkers`' "all at once" sentinel. Gemini first raised it as an indefinite hang; the hang mechanism is refuted above, but the underlying concern is real through the failure cutoff.
Contributor guide
Assessment
This issue has not been assessed yet.