HarperFast / HarperFast/harper

restartWorkers(name, 0) abandons the restart after one worker instead of throttling to it

Open
#2,601 1 comment 0 reactions 1 assignee Assigned to @dawsontoth View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.