mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
fix(gateway): fence only superseded desktop sessions
The desktop core extraction replaced the owner fence's "stop strictly older owners" check with an unconditional session stop. A launcher that claims an owner epoch first, then reaches its async fencing pass after a same-epoch observe has already created the session, tore that session down and failed the observer with "stopped before connecting". Restore the original invariant in the registry that owns it: stopSuperseded() retires an entry only when its epoch is strictly lower than the claimant's, so peers sharing a generation keep the session. The regression test drives launch-then-observe at one epoch and fails on the pre-fix code inside fenceReplacedOwners.
This commit is contained in:
@@ -224,6 +224,17 @@ export function createDesktopSessionRegistry(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retires only owners strictly older than the claimant. An equal epoch shares the
|
||||
* session, so fencing must not tear down a peer that claimed the same generation.
|
||||
*/
|
||||
async function stopSuperseded(sourceKey: string, ownerEpoch: number): Promise<void> {
|
||||
const entry = entries.get(sourceKey);
|
||||
if (entry && entry.ownerEpoch < ownerEpoch) {
|
||||
await stopEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
async function stopAll(): Promise<void> {
|
||||
await Promise.all([...entries.values()].map(stopEntry));
|
||||
}
|
||||
@@ -235,6 +246,7 @@ export function createDesktopSessionRegistry(
|
||||
isOwnerEpochCurrent: (sourceKey: string, ownerEpoch: number) =>
|
||||
claimedOwnerEpochs.get(sourceKey) === ownerEpoch,
|
||||
stop,
|
||||
stopSuperseded,
|
||||
stopAll,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -396,4 +396,26 @@ describe("worker desktop tunnels", () => {
|
||||
await expect(launchApp(failed)).rejects.toThrow("launcher failed");
|
||||
await failed.stopAll();
|
||||
});
|
||||
|
||||
it("keeps a same-epoch desktop session alive when an app launch fences replaced owners", async () => {
|
||||
const fake = fakeRunner();
|
||||
const manager = createWorkerDesktopTunnels({ runner: fake.runner });
|
||||
// The launcher claims the epoch first, so its fencing pass runs after the
|
||||
// observer session for that same epoch already exists. Fencing must only
|
||||
// retire strictly older owners; equal epochs share the session.
|
||||
const launching = launchApp(manager, "browser", 1);
|
||||
const starting = acquire(manager, 1);
|
||||
await waitForStarts(fake.starts, 1);
|
||||
fake.starts[0]?.process.becomeReady();
|
||||
await starting;
|
||||
await launching;
|
||||
|
||||
const observer = manager.attachObserver("worker:one", {
|
||||
control: false,
|
||||
ownerEpoch: 1,
|
||||
close: vi.fn(),
|
||||
});
|
||||
expect(observer).toBeDefined();
|
||||
observer?.release();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -106,7 +106,7 @@ export function createWorkerDesktopTunnels(deps: {
|
||||
};
|
||||
|
||||
const fenceReplacedOwners = async (environmentId: string, ownerEpoch: number): Promise<void> => {
|
||||
await sessions.stop(environmentId);
|
||||
await sessions.stopSuperseded(environmentId, ownerEpoch);
|
||||
const staleLaunches = [...appLaunches.values()].filter(
|
||||
(entry) => entry.environmentId === environmentId && entry.ownerEpoch < ownerEpoch,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user