You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/08/23 13:04:17 UTC
(maka) branch main updated: fix(runtime-host): bind retirement to one service generation (#3600)
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git
The following commit(s) were added to refs/heads/main by this push:
new 2a545af0a fix(runtime-host): bind retirement to one service generation (#3600)
2a545af0a is described below
commit 2a545af0a66e414565bf7503860d6afcc5887631
Author: Wang <m4...@gmail.com>
AuthorDate: Sun Aug 23 21:04:12 2026 +0800
fix(runtime-host): bind retirement to one service generation (#3600)
Wait for the prepared Host to release the State Root, then hold the writer fence through service stop. Fail closed when a successor changes the managed service PID before the fence is acquired, instead of stopping the successor.
Generated-by: Codex
---
.../__tests__/runtime-host-service-manager.test.ts | 67 ++++++++++++++++++++++
packages/cli/src/runtime-host-service-manager.ts | 26 +++++++++
2 files changed, 93 insertions(+)
diff --git a/packages/cli/src/__tests__/runtime-host-service-manager.test.ts b/packages/cli/src/__tests__/runtime-host-service-manager.test.ts
index 1531b94cf..b7c277b19 100644
--- a/packages/cli/src/__tests__/runtime-host-service-manager.test.ts
+++ b/packages/cli/src/__tests__/runtime-host-service-manager.test.ts
@@ -1020,6 +1020,73 @@ describe('managed Runtime Host service', () => {
assert.equal(serviceState, 'stopped');
});
+ it('fails closed without stopping a successor that won the State Root', async (t) => {
+ const base = await mkdtemp(join(tmpdir(), 'maka-runtime-host-retirement-generation-'));
+ t.after(() => rm(base, { recursive: true, force: true }));
+ const clientDataRoot = join(base, 'config');
+ const root = await resolveStorageRoot({ path: join(base, 'state'), kind: 'interactive' });
+ const cliPath = join(base, 'cli.js');
+ await writeFile(cliPath, '#!/usr/bin/env node\n', 'utf8');
+ let serviceState: 'running' | 'stopped' = 'running';
+ let servicePid: number | null = 42;
+ let stops = 0;
+ let successor: Awaited<ReturnType<typeof tryAcquireInteractiveRootOwner>>;
+ const backend: RuntimeHostServiceBackend = {
+ ...createReadyBackend(),
+ status: async () => ({
+ manager: 'systemd_user',
+ installed: true,
+ enabled: true,
+ active: serviceState === 'running',
+ state: serviceState,
+ pid: serviceState === 'running' ? servicePid : null,
+ lastExitCode: 0,
+ }),
+ stop: async () => {
+ stops += 1;
+ serviceState = 'stopped';
+ servicePid = null;
+ },
+ };
+ const common = {
+ clientDataRoot,
+ defaultRootPath: root.canonicalPath,
+ nodePath: process.execPath,
+ cliPath,
+ } as const;
+ await manageRuntimeHostService({ ...common, action: 'install' }, backend, {
+ waitForReady: async () => undefined,
+ });
+ const expectedTarget = {
+ serviceId: resolveRuntimeHostManagedServiceId(clientDataRoot),
+ rootPath: root.canonicalPath,
+ rootId: root.rootId,
+ } as const;
+ await assert.rejects(
+ manageRuntimeHostService(
+ { ...common, action: 'retire', expectedTarget, allowInterruptActiveTasks: true },
+ backend,
+ {
+ prepareRetirement: async (
+ _config: RuntimeHostManagedServiceConfig,
+ expectedPid: number,
+ ) => {
+ assert.equal(expectedPid, 42);
+ successor = await tryAcquireInteractiveRootOwner(root);
+ assert.ok(successor);
+ servicePid = 43;
+ return { kind: 'prepared', hostEpoch: 'host-a', pid: expectedPid } as const;
+ },
+ },
+ ),
+ (error: unknown) =>
+ error instanceof RuntimeHostServiceManagerError && error.code === 'retirement_failed',
+ );
+ assert.equal(stops, 0);
+ assert.equal(servicePid, 43);
+ await successor?.close();
+ });
+
it('restores the deployed service when the replacement never becomes ready', async (t) => {
const base = await mkdtemp(join(tmpdir(), 'maka-runtime-host-service-rollback-'));
t.after(() => rm(base, { recursive: true, force: true }));
diff --git a/packages/cli/src/runtime-host-service-manager.ts b/packages/cli/src/runtime-host-service-manager.ts
index d13357014..3c1a15634 100644
--- a/packages/cli/src/runtime-host-service-manager.ts
+++ b/packages/cli/src/runtime-host-service-manager.ts
@@ -387,6 +387,7 @@ async function manageRuntimeHostServiceLocked(
return { schemaVersion: 1, action: input.action, service, retirement };
}
prepared = retirement;
+ rootFence = await acquirePreparedRuntimeHostRootRetirementFence(root, prepared.pid, backend);
} else if (service.active) {
throw new RuntimeHostServiceManagerError(
'retirement_failed',
@@ -948,6 +949,31 @@ async function acquireRuntimeHostRootRetirementFence(
}
}
+async function acquirePreparedRuntimeHostRootRetirementFence(
+ root: StorageRootCapability<'interactive'>,
+ expectedPid: number,
+ backend: RuntimeHostServiceBackend,
+): Promise<InteractiveRootOwner> {
+ const deadline = Date.now() + SERVICE_READY_TIMEOUT_MS;
+ while (Date.now() < deadline) {
+ const owner = await tryAcquireInteractiveRootOwner(root);
+ const status = await backend.status();
+ if (status.pid !== null && status.pid !== expectedPid) {
+ await owner?.close().catch(() => undefined);
+ throw new RuntimeHostServiceManagerError(
+ 'retirement_failed',
+ 'Runtime Host service identity changed before retirement could stop the prepared Host',
+ );
+ }
+ if (owner) return owner;
+ await new Promise<void>((resolveWait) => setTimeout(resolveWait, SERVICE_READY_POLL_MS));
+ }
+ throw new RuntimeHostServiceManagerError(
+ 'retirement_failed',
+ 'The prepared Runtime Host did not release the State Root writer before retirement timed out',
+ );
+}
+
async function releaseRuntimeHostRootRetirementFence(owner: InteractiveRootOwner): Promise<void> {
try {
await owner.close();
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org