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