Skip to content

fix(components): avoid startup hangs on zombie process groups - #2085

Open
kriszyp wants to merge 5 commits into
mainfrom
fix/process-tree-confirmation-timeout
Open

fix(components): avoid startup hangs on zombie process groups#2085
kriszyp wants to merge 5 commits into
mainfrom
fix/process-tree-confirmation-timeout

Conversation

@kriszyp

@kriszyp kriszyp commented Aug 4, 2026

Copy link
Copy Markdown
Member

Human-Review-Need: 4 @ 0f03313

Summary

Prevent component preparation from waiting forever on an unreaped Linux zombie process group. This CI follow-up uses RocksDB's database-wide transaction-log purge scope, resolves import-only ESM package exports under Bun, and detects higher-priority extensionless candidates without relying on Bun's CommonJS resolver cache.

Refs #2072
Related #2076

Verification

  • npm run build
  • npm run lint:required
  • npx mocha unitTests/components/RuntimeModuleTracker.test.js (6 passing)
  • npm run test:integration -- --isolation=none integrationTests/database/txnlog-purge-stale-read-blast.test.ts (14 passing; RocksDB and LMDB arms)
  • HARPER_RUNTIME=bun npm run test:integration -- --isolation=none integrationTests/deploy/redeploy-runtime-equivalence.test.ts (18 passing; both repaired Bun regressions)
  • Latest Integration workflow: all original failing 1/6 and 5/6 shards pass on Bun, Node.js, uWS HTTP, and Windows.
  • Independent review: exact requested Gemini, Cursor Grok, and Cursor Composer legs ran on the final SHA. Composer completed; Gemini returned no output and Grok timed out. Claude was not used. The generated grade remains 4 pending human review.

Generated by GPT-5 Codex.

@kriszyp
kriszyp requested a review from Ethan-Arrowood August 4, 2026 22:34
@kriszyp

kriszyp commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Guided tour for reviewers:

  1. Start in server/threads/manageThreads.js. isProcessGroupAlive() is now the single liveness decision for both component installs and worker reclaim. It only scans /proc after the process-group leader is zombie or missing, and returns terminated only when no runnable member remains.
  2. Then read components/Application.ts, where install cleanup delegates to that shared decision. This prevents the observed zombie-only group from holding startup forever without releasing the preparation lock around a live descendant.
  3. Finally, see unitTests/components/applicationSpawn.test.js: the regression covers zombie-only groups, zombie leaders with live descendants, and reaped leaders with zombie members. It also replaces the fixture's fixed startup sleep with a condition wait.

Where to look hardest: the Linux-specific /proc inspection. It is intentionally conservative—failed inspection keeps the group alive—and is throttled to once per second for the anomalous zombie/missing-leader state. Other POSIX platforms retain the pre-existing conservative behavior.

Verification: build, focused component spawn test (13 passing), lint, and the full integration suite passed. The broad unit gate is blocked before discovery by the local RocksDB build's missing LZ4 support; the components glob also treats a fixture as a test.

Independent coverage: Gemini, Cursor Grok, and Cursor Composer ran. The final receipt requests human review (grade 4) because the exact policy omits a graded/adjudication leg and leaves the platform/performance tradeoff for review.

— GPT-5 Codex

@claude

claude Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request refactors process group status checks by introducing a robust isProcessGroupAlive function that scans /proc on Linux to ensure a process group is only considered alive if it contains non-zombie processes. It also updates the unit tests to use waitFor instead of arbitrary delays and adds test coverage for the new zombie process group detection. The reviewer suggested using assert.strictEqual instead of assert.equal in the new tests to align with the repository's style guide.

Comment on lines +243 to +278
assert.equal(zombieGroup(['123', '456']), false);
assert.equal(
isProcessGroupAlive(123, {
platform: 'linux',
processGroupExists: () => true,
readDirectory: () => ['123', '456'],
readStat: (path) =>
path === '/proc/123/stat' ? '123 (installer) Z 1 123 123' : '456 (still running) S 1 123 123',
}),
true
);
assert.equal(zombieGroup(['123']), false);
assert.equal(
isProcessGroupAlive(123, {
platform: 'linux',
processGroupExists: () => true,
readDirectory: () => ['456'],
readStat: (path) => {
if (path === '/proc/123/stat') throw new Error('leader reaped');
return '456 (unreaped child) Z 1 123 123';
},
}),
false
);
assert.equal(
isProcessGroupAlive(123, {
platform: 'linux',
processGroupExists: () => true,
readDirectory: () => ['456'],
readStat: (path) => {
if (path === '/proc/123/stat') throw new Error('leader reaped');
return '456 (running child) S 1 123 123';
},
}),
true
);

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.

medium

According to the repository style guide, strict assertion methods like assert.strictEqual should be used explicitly where strict semantics are needed (such as asserting boolean values), rather than loose equality checks like assert.equal.

		assert.strictEqual(zombieGroup(['123', '456']), false);
		assert.strictEqual(
			isProcessGroupAlive(123, {
				platform: 'linux',
				processGroupExists: () => true,
				readDirectory: () => ['123', '456'],
				readStat: (path) =>
					path === '/proc/123/stat' ? '123 (installer worker) Z 1 123 123' : '456 (installer child) Z 1 123 123',
			}),
			true
		);
		assert.strictEqual(zombieGroup(['123']), false);
		assert.strictEqual(
			isProcessGroupAlive(123, {
				platform: 'linux',
				processGroupExists: () => true,
				readDirectory: () => ['456'],
				readStat: (path) => {
					if (path === '/proc/123/stat') throw new Error('leader reaped');
					return '456 (unreaped child) Z 1 123 123';
				},
			}),
			false
		);
		assert.strictEqual(
			isProcessGroupAlive(123, {
				platform: 'linux',
				processGroupExists: () => true,
				readDirectory: () => ['456'],
				readStat: (path) => {
					if (path === '/proc/123/stat') throw new Error('leader reaped');
					return '456 (running child) S 1 123 123';
				},
			}),
			true
		);
References
  1. Use assert.strictEqual/assert.deepStrictEqual explicitly where strict semantics are needed. (link)
  2. Use the bare node:assert module instead of node:assert/strict for test assertions to comply with linting rules, while still utilizing strict assertion methods like assert.strictEqual from the bare module.

@kriszyp
kriszyp marked this pull request as ready for review August 4, 2026 23:27
@kriszyp
kriszyp marked this pull request as draft August 4, 2026 23:42
Comment on lines +131 to +143
function higherPriorityResolutionCandidates(
specifier: string,
referrerPath: string,
resolvedPath: string
): string[] | undefined {
if (!specifier.startsWith('.')) return;
const basePath = resolve(dirname(referrerPath), specifier);
const candidates = [...new Set(RESOLUTION_EXTENSIONS.map((extension) => basePath + extension))];
candidates.push(resolve(basePath, 'package.json'));
for (const extension of RESOLUTION_EXTENSIONS.slice(1)) candidates.push(resolve(basePath, `index${extension}`));
const resolvedIndex = candidates.indexOf(resolvedPath);
if (resolvedIndex === -1) return;
return candidates.slice(0, resolvedIndex);

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.

1. New Bun candidate-detection path has no test that exercises it

File: components/RuntimeModuleTracker.ts:131-170 (higherPriorityResolutionCandidates, candidateState, candidateStateAsync)

What: This commit (89a6c94) adds a new independent staleness-detection mechanism: for relative specifiers, it enumerates the filesystem candidates that would have been checked with higher priority than the resolved path, snapshots their state, and re-checks that state in #compare() before falling through to the pre-existing invalidateResolutionCache + createRequire().resolve() check.

The only test that superficially covers this scenario — unitTests/components/RuntimeModuleTracker.test.js › "detects a new higher-priority extensionless resolution candidate" — is unmodified by this PR and already exists unchanged on main (i.e. it passed before this candidate-tracking code existed). It passes because Node's own require.resolve() re-resolution (the old fallback, still present right after the new check) already picks up the newly-created helper.js and returns a different resolvedUrl. Deleting the entire new higherPriorityCandidates block would not make this test fail — so it isn't actually validating the new code path.

Per the commit message ("detect Bun resolution candidate changes") and the PR body's HARPER_RUNTIME=bun verification step, the new mechanism exists specifically to catch cases where the old resolve-based fallback doesn't reliably detect a new higher-priority file (e.g. under Bun, where Module._pathCache invalidation may not force a real re-resolution). That specific scenario — new logic detects a change the fallback misses — has no direct unit coverage.

Why it matters: This is new, non-trivial (~80 LOC) production logic gating whether a redeploy is treated as a hot-swap vs. requiring a restart. If the candidate/priority-ordering logic (e.g. the assumed extension-check order in RESOLUTION_EXTENSIONS) is wrong, a stale module could silently continue being served after a redeploy — and no test in this PR would catch that regression, since the existing test passes via a different mechanism entirely.

Suggested fix: Add a unit test that isolates the new branch — e.g. stub/neutralize the fallback re-resolution (as the existing "fails closed when Node resolution cache invalidation is unavailable" test does for Module._pathCache) so only higherPriorityCandidates can produce a true/false result, then verify it correctly flags a newly-added higher-priority file and correctly reports "unchanged" when candidates are untouched.

@kriszyp
kriszyp marked this pull request as ready for review August 5, 2026 04:51
@kriszyp
kriszyp requested a review from heskew August 5, 2026 04:51
kriszyp and others added 5 commits August 5, 2026 19:42
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
@kriszyp
kriszyp force-pushed the fix/process-tree-confirmation-timeout branch from 89a6c94 to 0f03313 Compare August 6, 2026 01:44
Comment on lines +243 to +278
assert.equal(zombieGroup(['123', '456']), false);
assert.equal(
isProcessGroupAlive(123, {
platform: 'linux',
processGroupExists: () => true,
readDirectory: () => ['123', '456'],
readStat: (path) =>
path === '/proc/123/stat' ? '123 (installer) Z 1 123 123' : '456 (still running) S 1 123 123',
}),
true
);
assert.equal(zombieGroup(['123']), false);
assert.equal(
isProcessGroupAlive(123, {
platform: 'linux',
processGroupExists: () => true,
readDirectory: () => ['456'],
readStat: (path) => {
if (path === '/proc/123/stat') throw new Error('leader reaped');
return '456 (unreaped child) Z 1 123 123';
},
}),
false
);
assert.equal(
isProcessGroupAlive(123, {
platform: 'linux',
processGroupExists: () => true,
readDirectory: () => ['456'],
readStat: (path) => {
if (path === '/proc/123/stat') throw new Error('leader reaped');
return '456 (running child) S 1 123 123';
},
}),
true
);

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.

Suggestion (non-blocking): these boolean checks still use assert.equal — gemini's earlier comment on this same block asked for assert.strictEqual, which matches the repo's own house style (AGENTS.md: use assert.strictEqual/assert.deepStrictEqual explicitly when a check needs strict semantics, e.g. booleans). Not gating merge, just flagging it's still outstanding.

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