Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions apps/server/src/persistence/initializeV2Database.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// @effect-diagnostics nodeBuiltinImport:off
import * as NodeChildProcess from "node:child_process";
import * as NodeFS from "node:fs";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";
Expand Down Expand Up @@ -182,3 +183,28 @@ it.effect("starts fresh without V1 and never imports over existing V2 state", ()
Effect.ensuring(Effect.sync(() => NodeFS.rmSync(directory, { recursive: true, force: true }))),
);
});

it.effect("removes copies left by a killed start, but not one that is still running", () => {
const directory = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-v2-abandoned-"));
const killed = NodeChildProcess.spawnSync(process.execPath, ["-e", ""]).pid;
const running = `.v2-import-${process.pid}-Kq81Zx`;
const unrelated = ".v2-import-not-a-copy";
return Effect.gen(function* () {
for (const name of [`.v2-import-${killed}-Wm3nPa`, ".v2-import-Q3xZ9a", running]) {
NodeFS.mkdirSync(NodePath.join(directory, name));
NodeFS.writeFileSync(NodePath.join(directory, name, "snapshot.sqlite"), "partial copy");
}
NodeFS.mkdirSync(NodePath.join(directory, unrelated));
const destinationPath = NodePath.join(directory, "statev2.sqlite");
NodeFS.writeFileSync(destinationPath, "existing V2 database");
yield* initializeV2Database(destinationPath);
assert.deepEqual(
NodeFS.readdirSync(directory).sort(),
[running, unrelated, "statev2.sqlite"].sort(),
);
assert.equal(NodeFS.readFileSync(destinationPath, "utf8"), "existing V2 database");
}).pipe(
Effect.provide(NodeServices.layer),
Effect.ensuring(Effect.sync(() => NodeFS.rmSync(directory, { recursive: true, force: true }))),
);
});
30 changes: 29 additions & 1 deletion apps/server/src/persistence/initializeV2Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,33 @@ export class V2DatabaseImportError extends Schema.TaggedError<V2DatabaseImportEr
}
}

const isProcessAlive = (pid: number): boolean => {
if (!Number.isInteger(pid) || pid <= 0) return false;
try {
process.kill(pid, 0);
return true;
} catch (error) {
return error instanceof Error && "code" in error && error.code === "EPERM";
}
};

// Legacy names have no PID; newer copies stay while their process is alive.
const removeAbandonedCopies = Effect.fn("removeAbandonedCopies")(function* (directory: string) {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const entries = yield* fs.readDirectory(directory).pipe(
Effect.catchIf(
(error) => error.reason._tag === "NotFound",
() => Effect.succeed([]),
),
);
for (const entry of entries) {
const copy = /^\.v2-import-(?:(\d+)-)?[A-Za-z0-9]{6}$/.exec(entry);
if (copy === null || (copy[1] !== undefined && isProcessAlive(Number(copy[1])))) continue;
yield* fs.remove(path.join(directory, entry), { recursive: true });
}
});

/** Its copied legacy tables remain the source for lazy transcript import. */
export const initializeV2Database = Effect.fn("initializeV2Database")(function* (
destinationPath: string,
Expand All @@ -22,12 +49,13 @@ export const initializeV2Database = Effect.fn("initializeV2Database")(function*
const path = yield* Path.Path;
const directory = path.dirname(destinationPath);
const sourcePath = path.join(directory, "state.sqlite");
yield* removeAbandonedCopies(directory).pipe(Effect.ignore({ log: true }));
yield* Effect.gen(function* () {
if (yield* fs.exists(destinationPath)) return;
if (!(yield* fs.exists(sourcePath))) return;
const temporaryDirectory = yield* fs.makeTempDirectoryScoped({
directory,
prefix: ".v2-import-",
prefix: `.v2-import-${process.pid}-`,
});
const snapshotPath = path.join(temporaryDirectory, "snapshot.sqlite");
yield* Effect.tryPromise(async () => {
Expand Down
Loading