Skip to content

Commit a39a3b7

Browse files
committed
fix(deadcode): move restart handoffs to sqlite
1 parent 77a859f commit a39a3b7

2 files changed

Lines changed: 244 additions & 151 deletions

File tree

src/infra/restart-handoff.test.ts

Lines changed: 114 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,26 @@ import fs from "node:fs";
33
import os from "node:os";
44
import path from "node:path";
55
import { afterEach, describe, expect, it } from "vitest";
6+
import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js";
7+
import {
8+
closeOpenClawStateDatabaseForTest,
9+
openOpenClawStateDatabase,
10+
} from "../state/openclaw-state-db.js";
11+
import {
12+
executeSqliteQuerySync,
13+
executeSqliteQueryTakeFirstSync,
14+
getNodeSqliteKysely,
15+
} from "./kysely-sync.js";
616
import {
717
formatGatewayRestartHandoffDiagnostic,
8-
GATEWAY_SUPERVISOR_RESTART_HANDOFF_FILENAME,
918
GATEWAY_SUPERVISOR_RESTART_HANDOFF_KIND,
1019
readGatewayRestartHandoffSync,
1120
writeGatewayRestartHandoffSync,
1221
} from "./restart-handoff.js";
1322
import type { GatewayRestartHandoff } from "./restart-handoff.js";
1423

1524
const tempDirs: string[] = [];
25+
type GatewayRestartHandoffDatabase = Pick<OpenClawStateKyselyDatabase, "gateway_restart_handoff">;
1626

1727
function createHandoffEnv(): NodeJS.ProcessEnv {
1828
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-restart-handoff-"));
@@ -23,8 +33,77 @@ function createHandoffEnv(): NodeJS.ProcessEnv {
2333
};
2434
}
2535

26-
function handoffPath(env: NodeJS.ProcessEnv): string {
27-
return path.join(env.OPENCLAW_STATE_DIR ?? "", GATEWAY_SUPERVISOR_RESTART_HANDOFF_FILENAME);
36+
function legacyHandoffPath(env: NodeJS.ProcessEnv): string {
37+
return path.join(env.OPENCLAW_STATE_DIR ?? "", "gateway-supervisor-restart-handoff.json");
38+
}
39+
40+
function readHandoffRow(env: NodeJS.ProcessEnv) {
41+
const { db } = openOpenClawStateDatabase({ env });
42+
const stateDb = getNodeSqliteKysely<GatewayRestartHandoffDatabase>(db);
43+
return executeSqliteQueryTakeFirstSync(
44+
db,
45+
stateDb
46+
.selectFrom("gateway_restart_handoff")
47+
.select([
48+
"handoff_key",
49+
"kind",
50+
"version",
51+
"intent_id",
52+
"pid",
53+
"process_instance_id",
54+
"created_at",
55+
"expires_at",
56+
"reason",
57+
"restart_trace_started_at",
58+
"restart_trace_last_at",
59+
"source",
60+
"restart_kind",
61+
"supervisor_mode",
62+
])
63+
.where("handoff_key", "=", "current"),
64+
);
65+
}
66+
67+
function insertHandoffRow(
68+
env: NodeJS.ProcessEnv,
69+
values: {
70+
kind?: string;
71+
version?: number;
72+
intentId?: string;
73+
pid?: number;
74+
createdAt?: number;
75+
expiresAt?: number;
76+
reason?: string | null;
77+
source?: string;
78+
restartKind?: string;
79+
supervisorMode?: string;
80+
restartTraceStartedAt?: number | null;
81+
restartTraceLastAt?: number | null;
82+
},
83+
) {
84+
const { db } = openOpenClawStateDatabase({ env });
85+
const stateDb = getNodeSqliteKysely<GatewayRestartHandoffDatabase>(db);
86+
const now = Date.now();
87+
executeSqliteQuerySync(
88+
db,
89+
stateDb.insertInto("gateway_restart_handoff").values({
90+
handoff_key: "current",
91+
kind: values.kind ?? GATEWAY_SUPERVISOR_RESTART_HANDOFF_KIND,
92+
version: values.version ?? 1,
93+
intent_id: values.intentId ?? "intent-1",
94+
pid: values.pid ?? 111,
95+
process_instance_id: null,
96+
created_at: values.createdAt ?? 1_000,
97+
expires_at: values.expiresAt ?? 61_000,
98+
reason: values.reason ?? null,
99+
restart_trace_started_at: values.restartTraceStartedAt ?? null,
100+
restart_trace_last_at: values.restartTraceLastAt ?? null,
101+
source: values.source ?? "plugin-change",
102+
restart_kind: values.restartKind ?? "full-process",
103+
supervisor_mode: values.supervisorMode ?? "external",
104+
updated_at_ms: now,
105+
}),
106+
);
28107
}
29108

30109
function expectWrittenHandoff(
@@ -39,6 +118,7 @@ function expectWrittenHandoff(
39118

40119
describe("gateway restart handoff", () => {
41120
afterEach(() => {
121+
closeOpenClawStateDatabaseForTest();
42122
for (const dir of tempDirs.splice(0)) {
43123
fs.rmSync(dir, { force: true, recursive: true });
44124
}
@@ -67,7 +147,16 @@ describe("gateway restart handoff", () => {
67147
expect(handoff.supervisorMode).toBe("launchd");
68148
expect(handoff.createdAt).toBe(1_000);
69149
expect(handoff.expiresAt).toBe(61_000);
70-
expect(fs.statSync(handoffPath(env)).mode & 0o777).toBe(0o600);
150+
expect(readHandoffRow(env)).toMatchObject({
151+
handoff_key: "current",
152+
kind: GATEWAY_SUPERVISOR_RESTART_HANDOFF_KIND,
153+
pid: 12_345,
154+
reason: "plugin source changed",
155+
source: "plugin-change",
156+
restart_kind: "full-process",
157+
supervisor_mode: "launchd",
158+
});
159+
expect(fs.existsSync(legacyHandoffPath(env))).toBe(false);
71160
const persisted = readGatewayRestartHandoffSync(env, 1_500);
72161
expect(persisted?.pid).toBe(12_345);
73162
expect(persisted?.reason).toBe("plugin source changed");
@@ -126,27 +215,12 @@ describe("gateway restart handoff", () => {
126215
it("rejects malformed handoff payloads", () => {
127216
const env = createHandoffEnv();
128217

129-
fs.writeFileSync(
130-
handoffPath(env),
131-
`${JSON.stringify({
132-
kind: GATEWAY_SUPERVISOR_RESTART_HANDOFF_KIND,
133-
version: 1,
134-
intentId: "bad",
135-
pid: 111,
136-
createdAt: 1_000,
137-
expiresAt: 61_000,
138-
reason: 123,
139-
source: "bad-source",
140-
restartKind: "full-process",
141-
supervisorMode: "external",
142-
})}\n`,
143-
{ encoding: "utf8", mode: 0o600 },
144-
);
218+
insertHandoffRow(env, { intentId: "bad", source: "bad-source" });
145219

146220
expect(readGatewayRestartHandoffSync(env, 1_001)).toBeNull();
147221
});
148222

149-
it("rejects expired and oversized handoff files", () => {
223+
it("rejects expired handoff rows", () => {
150224
const env = createHandoffEnv();
151225

152226
expectWrittenHandoff({
@@ -158,53 +232,43 @@ describe("gateway restart handoff", () => {
158232
ttlMs: 1_000,
159233
});
160234
expect(readGatewayRestartHandoffSync(env, 2_001)).toBeNull();
161-
162-
fs.writeFileSync(handoffPath(env), "x".repeat(8192), { encoding: "utf8", mode: 0o600 });
163-
expect(readGatewayRestartHandoffSync(env, 2_001)).toBeNull();
164235
});
165236

166237
it("rejects persisted handoffs with a ttl longer than the supported window", () => {
167238
const env = createHandoffEnv();
168239

169-
fs.writeFileSync(
170-
handoffPath(env),
171-
`${JSON.stringify({
172-
kind: GATEWAY_SUPERVISOR_RESTART_HANDOFF_KIND,
173-
version: 1,
174-
intentId: "too-long",
175-
pid: 111,
176-
createdAt: 1_000,
177-
expiresAt: 61_001,
178-
source: "plugin-change",
179-
restartKind: "full-process",
180-
supervisorMode: "external",
181-
})}\n`,
182-
{ encoding: "utf8", mode: 0o600 },
183-
);
240+
insertHandoffRow(env, { intentId: "too-long", createdAt: 1_000, expiresAt: 61_001 });
184241

185242
expect(readGatewayRestartHandoffSync(env, 1_001)).toBeNull();
186243
});
187244

188-
it("does not follow an existing handoff-path symlink when writing", () => {
245+
it("overwrites the previous pending handoff row", () => {
189246
const env = createHandoffEnv();
190-
const targetPath = path.join(env.OPENCLAW_STATE_DIR ?? "", "attacker-target.txt");
191-
fs.writeFileSync(targetPath, "keep", "utf8");
192-
try {
193-
fs.symlinkSync(targetPath, handoffPath(env));
194-
} catch {
195-
return;
196-
}
197247

198248
expectWrittenHandoff({
199249
env,
200250
pid: 12_345,
201251
restartKind: "full-process",
202252
supervisorMode: "external",
203253
});
254+
expectWrittenHandoff({
255+
env,
256+
pid: 67_890,
257+
reason: "gateway.restart",
258+
restartKind: "update-process",
259+
supervisorMode: "systemd",
260+
});
204261

205-
expect(fs.readFileSync(targetPath, "utf8")).toBe("keep");
206-
expect(fs.lstatSync(handoffPath(env)).isSymbolicLink()).toBe(false);
207-
expect(readGatewayRestartHandoffSync(env)?.pid).toBe(12_345);
262+
expect(readHandoffRow(env)).toMatchObject({
263+
handoff_key: "current",
264+
pid: 67_890,
265+
reason: "gateway.restart",
266+
source: "operator-restart",
267+
restart_kind: "update-process",
268+
supervisor_mode: "systemd",
269+
});
270+
expect(readGatewayRestartHandoffSync(env)?.pid).toBe(67_890);
271+
expect(fs.existsSync(legacyHandoffPath(env))).toBe(false);
208272
});
209273

210274
it("formats a concise diagnostic line for status surfaces", () => {

0 commit comments

Comments
 (0)