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
18 changes: 13 additions & 5 deletions apps/server/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1847,6 +1847,8 @@ function listen(): Server<SocketData> {

/** Nothing in memory is worth losing to a Ctrl-C. */
let draining: Promise<void> | undefined;
/** Startup's session reset, which replaces the held writer lease with a re-fenced one. */
let resettingLease: Promise<unknown> | undefined;

function drain(): Promise<void> {
return draining ??= (async () => {
Expand All @@ -1861,7 +1863,7 @@ function drain(): Promise<void> {
record(err);
}
};
await attempt(() => server.stop(true));
if (server) await attempt(() => server.stop(true));
if (researchRecoveryTimer) clearTimeout(researchRecoveryTimer);
if (experimentTimer) clearInterval(experimentTimer);
if (recoveringResearch) await attempt(() => recoveringResearch!);
Expand Down Expand Up @@ -1897,6 +1899,7 @@ function drain(): Promise<void> {
}
await attempt(() => shutdownHarnesses());
await attempt(() => writerLease.stop());
if (resettingLease) await attempt(() => resettingLease!);
let lease = writerLease.held;
if (lease) await attempt(() => storage.leases.release(lease));
await attempt(() => storage.close());
Expand Down Expand Up @@ -2867,8 +2870,15 @@ try {
);
if (!acquired) throw new Error("another Chopin instance owns the database");
writerLease.hold(acquired);
let reset = await storage.sessions.reset(new Date(), acquired, LEASE_TTL_MS);
writerLease.held = reset.lease;
// A stop that arrives while startup still runs must release the lease, or the next start finds it held.
process.on("SIGINT", signal);
process.on("SIGTERM", signal);
let resetting = storage.sessions.reset(new Date(), acquired, LEASE_TTL_MS).then(reset => {
writerLease.held = reset.lease;
return reset;
});
resettingLease = resetting;
let reset = await resetting;
// Before any room opens, so no child Conversation is ever loaded or served.
let children = await storage.collaboration.dropChildConversations(reset.lease);
if (children.cleared.length) {
Expand Down Expand Up @@ -2926,7 +2936,5 @@ try {
await storage.close().catch(() => {});
throw err;
}
process.on("SIGINT", signal);
process.on("SIGTERM", signal);

console.log(describe(config));
30 changes: 30 additions & 0 deletions apps/server/src/storage/postgres/startup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,36 @@ if (database) {
await storage.close();
}, 20_000);

it("releases the writer lease when stopped while it is still starting", async () => {
let storage = new PostgresStorage(database);
await storage.migrate();
let sql = new SQL(database);
let held = async () => {
let [row] = await sql<{ owner: string }[]>`
SELECT owner FROM storage_leases
WHERE name = 'chopin:writer' AND expires_at > clock_timestamp()
`;
return row?.owner;
};
let before = await held();
let child = spawn(9077, "pipe");
let owner: string | undefined;
for (let attempt = 0; attempt < 2_000 && (!owner || owner === before); attempt++) {
owner = await held();
if (!owner || owner === before) await Bun.sleep(5);
}
expect(owner).toBeDefined();
expect(owner).not.toBe(before);
child.kill("SIGTERM");
await child.exited;
expect(await held()).toBeUndefined();
let next = await storage.leases.acquire("chopin:writer", crypto.randomUUID(), 30_000);
expect(next).toBeDefined();
await storage.leases.release(next!);
await sql.close();
await storage.close();
}, 20_000);

it("recovers an inline Research card without an open room, once across restarts", async () => {
let storage = new PostgresStorage(database);
await storage.migrate();
Expand Down
Loading