fix(memhub): preserve migration log write order
This commit is contained in:
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.
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user