@clickup/pg-microsharding
Version:
Microshards support for PostgreSQL
179 lines (165 loc) • 5.68 kB
text/typescript
import readline from "readline";
import type { ClearablePromise } from "delay";
import type { PhysicalReplica } from "./getPhysicalReplicas";
import { getPhysicalReplicas } from "./getPhysicalReplicas";
import type { SlotInfo } from "./getSlotInfo";
import { getSlotInfo } from "./getSlotInfo";
import type { TableInfo } from "./getTablesInSchema";
import { log, progress } from "./logging";
import { subName } from "./names";
import { pollDelay } from "./pollDelay";
import { secToHuman } from "./secToHuman";
import { tookToHuman } from "./tookToHuman";
const FORCE_CONTINUE_KEY = {
sequence: "S",
prompt: "press Shift+S to force-continue: not recommended!",
};
/**
* Waits until all physical replicas on a DSN are not lagging for too much, so
* we can e.g. call waitUntilIncrementalCompletes() and then resultCommit().
* Also, if fromDsn is passed, waits for the logical slot to lag not more than
* maxReplicationLagSec time.
*
* For the logical slot replication lag estimation, in the beginning, we
* "latch", i.e. remember the master's LSN, and then wait until the slot replay
* is past that LSN. Once it happens, the amount of time passed is roughly the
* logical replication slot lag in seconds. This is the only way to estimate,
* how many seconds behind is a logical slot.
*
* For physical replicas, we query their replication lag in seconds directly.
*
* If waitForUserToContinue=true, then the function exits ONLY when the user
* told it to do so explicitly by pressing the hotkey.
*/
export async function waitUntilEverythingIsNotLaggingMuch(
{
when,
fromDsn,
tables,
toDsn,
schema,
maxReplicationLagSec: maxLagSec,
waitForUserToContinue,
}: {
when: string;
fromDsn?: string | null;
tables: TableInfo[];
toDsn: string;
schema: string;
maxReplicationLagSec: number;
waitForUserToContinue: boolean;
},
throwIfAborted?: () => void,
): Promise<void> {
if (tables.length === 0) {
return;
}
const waitStartedAt = performance.now();
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
try {
let physicalReplicas: PhysicalReplica[] | undefined = undefined;
let slotInfo: SlotInfo | undefined = undefined;
let stats: string[] = [];
let delayPromise: ClearablePromise<void> | null = null;
let userForcedContinue = false;
readline.emitKeypressEvents(process.stdin, rl);
rl.on("SIGINT", () => {
throwIfAborted = () => {
throw "Aborted due to SIGINT";
};
});
process.stdin.setRawMode?.(true);
process.stdin.on("keypress", (_str, key) => {
if (key.sequence === FORCE_CONTINUE_KEY.sequence) {
userForcedContinue = true;
delayPromise?.clear();
}
});
for (let i = 0, needWait = true; needWait && !userForcedContinue; i++) {
throwIfAborted?.();
physicalReplicas = await getPhysicalReplicas({
dsn: toDsn,
comment:
i === 0
? `Waiting till ${fromDsn ? "the slot and " : ""}replicas are not lagging much (${when})`
: undefined,
});
slotInfo = fromDsn
? await getSlotInfo(
{ dsn: fromDsn, slotName: subName(schema) },
slotInfo,
)
: undefined;
needWait = waitForUserToContinue;
stats = [];
if (slotInfo) {
const { srcLsn, dstLsn, gap, gapTillLatch, lagSec } = slotInfo;
const prefix = "Logical slot";
const info = `src=${srcLsn} dst=${dstLsn} gap=${gap} gapTillLatch=${gapTillLatch}`;
if (lagSec === null) {
needWait = true;
stats.push(
`? ${prefix}: lag is unknown yet (likely LARGE; be patient, estimating); ${info}`,
);
} else if (lagSec > maxLagSec) {
needWait = true;
stats.push(
`- ${prefix}: lag is ${secToHuman(lagSec)} > ${secToHuman(maxLagSec)}; ${info}`,
);
} else {
stats.push(
`+ ${prefix}: lag is ${secToHuman(lagSec)} <= ${secToHuman(maxLagSec)}; ${info}`,
);
}
}
for (const {
clientAddr,
userName,
applicationName,
lagSec,
feedbackAgoSec,
masterLsn,
replicaLsn,
gap,
} of physicalReplicas) {
const prefix = `Physical replica of the dst ${userName}@${clientAddr} (${applicationName})`;
const info = `master=${masterLsn} replica=${replicaLsn} gap=${gap} feedbackAgoSec=${feedbackAgoSec}`;
if (lagSec === null) {
needWait = true;
stats.push(`? ${prefix}: lag is unknown yet; ${info}`);
} else if (lagSec > maxLagSec) {
needWait = true;
stats.push(
`- ${prefix}: lag is ${secToHuman(lagSec)} > ${secToHuman(maxLagSec)}; ${info}`,
);
} else {
stats.push(
`+ ${prefix}: lag is ${secToHuman(lagSec)} <= ${secToHuman(maxLagSec)}; ${info}`,
);
}
}
progress(
waitForUserToContinue
? `Waiting for the user action: ${FORCE_CONTINUE_KEY.prompt}`
: `Waiting for the lag to stabilize (or ${FORCE_CONTINUE_KEY.prompt}):`,
...stats,
);
await (delayPromise = pollDelay());
}
progress.clear();
log.shellCmd({
comment: userForcedContinue
? `Force-continued in ${tookToHuman(waitStartedAt)}`
: `Everything is caught up in ${tookToHuman(waitStartedAt)}`,
cmd: "",
input: stats.join("\n"),
});
} finally {
rl.close();
process.stdin.setRawMode?.(false);
process.stdin.pause();
}
}