@clickup/pg-microsharding
Version:
Microshards support for PostgreSQL
114 lines (104 loc) • 3.72 kB
text/typescript
import { createConnectedClient } from "./createConnectedClient";
import type { TableInfo } from "./getTablesInSchema";
import { inspectError } from "./inspectError";
import { log, progress } from "./logging";
import { subName } from "./names";
import { subtractLsn } from "./parseLsn";
import { pollDelay } from "./pollDelay";
import { ident, join, sql } from "./quote";
import { tookToHuman } from "./tookToHuman";
export type Unlock = (status?: "error") => Promise<void>;
/**
* Locks the tables for write and waits until incremental flow to the
* destination tables exhausts.
*
* If succeeded, the function returns a callback which, when called, unlocks the
* tables on the source. This callback must be called after deactivating the
* shard on the source. (But even if it's not called, it'll likely be okay
* still, because the process will end, PG will disconnect, and the tables will
* be auto-unlocked.)
*/
export async function waitUntilIncrementalCompletes(
{
fromDsn,
schema,
tables,
}: {
fromDsn: string;
schema: string;
tables: TableInfo[];
},
throwIfAborted: () => void,
): Promise<Unlock> {
if (tables.length === 0) {
return async () => {};
}
const waitStartedAt = performance.now();
const fromClient = await createConnectedClient(fromDsn);
try {
const fromServerVersion = await fromClient.serverVersion();
await fromClient.query("BEGIN");
await fromClient.query("SET LOCAL statement_timeout TO 0");
await fromClient.query(
"SET LOCAL idle_in_transaction_session_timeout TO 0",
);
fromServerVersion[0] >= 17 &&
(await fromClient.query("SET LOCAL transaction_timeout TO 0"));
await fromClient.query(`SET LOCAL search_path TO ${schema}`);
throwIfAborted();
const query = sql`LOCK TABLE ONLY ${join(
tables.map(({ name }) => sql`${ident(name)}`),
", ",
)} IN EXCLUSIVE MODE`.toString();
log.shellCmd({
comment: "Locking source tables to pause all WRITES",
cmd: `node-postgres ${fromDsn}#${schema}`,
input: query,
});
await fromClient.query(query);
throwIfAborted();
const srcLsn = await fromClient.currentWalInsertLsn();
while (true) {
const dstLsn = await fromClient.confirmedFlushLsn(subName(schema));
// srcLsn is constant and in the future; dstLsn is in the past, grows
// monotonically and catches up srcLsn eventually. Once srcLsn-dstLsn
// crosses 0, we continue.
const gap = subtractLsn(srcLsn, dstLsn)!;
progress(
`Waiting for dst=${dstLsn} to be greater src=${srcLsn}, gap=${gap}`,
);
if (gap <= 0) {
break;
}
await pollDelay();
throwIfAborted();
}
} catch (e: unknown) {
try {
await fromClient.query("ROLLBACK");
await fromClient.end();
} catch (e: unknown) {
log.error(`Failed to rollback transaction: ${inspectError(e)}`);
}
throw e;
}
progress.clear();
log.shellCmd({
comment: "Incremental logical replication completed",
cmd: "",
input: `Tables were write-locked for ${tookToHuman(waitStartedAt)}`,
});
return async (status) => {
// It doesn't really matter for the locks, whether we commit or rollback
// here. But in case of a success the "ROLLBACK" message in the logs scare
// people. So we use "COMMIT" in case of success and "ROLLBACK" otherwise.
const query = status === "error" ? "ROLLBACK" : "COMMIT";
log.shellCmd({
comment: `Unlocking source tables; write lock was held for ${tookToHuman(waitStartedAt)}`,
cmd: `node-postgres ${fromDsn}`,
input: query,
});
await fromClient.query(query);
await fromClient.end();
};
}