fix(memhub): preserve migration log write order

This commit is contained in:
u7-u7 committed 2026-10-28 00:49:54 +08:00
1 parent e09fbd16f9
commit d76bfa6474
2 files changed
+126 -11

No files matched your search

@@ -0,0 +1,101 @@
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
sdkMigrationLogPath,
writeSdkMigrationLog,
} from "./sdk-migration-log.js";
async function events(dataDir: string): Promise<string[]> {
return (await readFile(sdkMigrationLogPath(dataDir), "utf8"))
.trim()
.split("\n")
.map((line) => JSON.parse(line).event);
}
describe("SDK migration log ordering", () => {
it("preserves invocation order for concurrent progress and completion writes", async () => {
const dataDir = await mkdtemp(
path.join(os.tmpdir(), "migration-log-order-"),
);
try {
const expected = Array.from(
{ length: 100 },
(_, i) => `migration.progress.${i}`,
);
expected.push("migration.finished");
await Promise.all(
expected.map((event) =>
writeSdkMigrationLog(dataDir, {
writer: "desktop",
event,
outcome: "started",
}),
),
);
expect(await events(dataDir)).toEqual(expected);
} finally {
await rm(dataDir, { recursive: true, force: true });
}
});
it("awaiting the terminal event also drains preceding progress writes", async () => {
const dataDir = await mkdtemp(
path.join(os.tmpdir(), "migration-log-drain-"),
);
const pending: Promise<void>[] = [];
try {
const expected = Array.from(
{ length: 50 },
(_, i) => `migration.progress.${i}`,
);
for (const event of expected) {
pending.push(
writeSdkMigrationLog(dataDir, {
writer: "desktop",
event,
outcome: "started",
}),
);
}
await writeSdkMigrationLog(dataDir, {
writer: "desktop",
event: "migration.finished",
outcome: "succeeded",
});
expect(await events(dataDir)).toEqual([
...expected,
"migration.finished",
]);
} finally {
await Promise.all(pending);
await rm(dataDir, { recursive: true, force: true });
}
});
it("does not let a failed append block subsequent writes to the same home", async () => {
const dataDir = await mkdtemp(
path.join(os.tmpdir(), "migration-log-recovery-"),
);
try {
await writeFile(path.join(dataDir, "logs"), "not a directory");
await expect(
writeSdkMigrationLog(dataDir, {
writer: "desktop",
event: "migration.progress",
outcome: "started",
}),
).resolves.toBeUndefined();
await rm(path.join(dataDir, "logs"));
await writeSdkMigrationLog(dataDir, {
writer: "desktop",
event: "migration.finished",
outcome: "succeeded",
});
expect(await events(dataDir)).toEqual(["migration.finished"]);
} finally {
await rm(dataDir, { recursive: true, force: true });
}
});
});
@@ -33,6 +33,9 @@ export const SDK_MIGRATION_LOG_NAME = "sdk-migration.log";
const MAX_BYTES = 4 * 1024 * 1024;
const SAFE_TEXT = /^[A-Za-z0-9_.:/@+-]{0,160}$/;
const MAX_ITEMS = 32;
// Preserve invocation order within this process, including fire-and-forget
// progress followed by an awaited terminal event. Homes remain independent.
const pendingWrites = new Map<string, Promise<void>>();
export function sdkMigrationLogPath(dataDir: string): string {
return path.join(dataDir, "logs", SDK_MIGRATION_LOG_NAME);
@@ -103,17 +106,28 @@ export async function writeSdkMigrationLog(
platform: process.platform,
...(entry.details === undefined ? {} : { details: entry.details }),
});
const target = sdkMigrationLogPath(dataDir);
await mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
const size = await stat(target).then(
(info) => info.size,
() => 0,
);
if (size > MAX_BYTES) await rename(target, `${target}.1`);
await appendFile(target, `${JSON.stringify(line)}\n`, {
encoding: "utf8",
mode: 0o600,
});
const target = path.resolve(sdkMigrationLogPath(dataDir));
const previous = pendingWrites.get(target) ?? Promise.resolve();
const pending = previous
.catch(() => undefined)
.then(async () => {
await mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
const size = await stat(target).then(
(info) => info.size,
() => 0,
);
if (size > MAX_BYTES) await rename(target, `${target}.1`);
await appendFile(target, `${JSON.stringify(line)}\n`, {
encoding: "utf8",
mode: 0o600,
});
});
pendingWrites.set(target, pending);
try {
await pending;
} finally {
if (pendingWrites.get(target) === pending) pendingWrites.delete(target);
}
} catch {
// Never let the log decide whether a migration or a startup happens.
}