UNPKG

@clickup/pg-microsharding

Version:
179 lines (165 loc) 5.68 kB
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(); } }