UNPKG

@clickup/pg-microsharding

Version:
267 lines (244 loc) 8.44 kB
import chalk from "chalk"; import delay from "delay"; import compact from "lodash/compact"; import { advanceSequences } from "../internal/advanceSequences"; import { analyzeTables } from "../internal/analyzeTables"; import { changeTableOwners } from "../internal/changeTableOwners"; import { cleanUpPubSub } from "../internal/cleanUpPubSub"; import { disableSubscription } from "../internal/disableSubscription"; import { enableSubscription } from "../internal/enableSubscription"; import { getDump } from "../internal/getDump"; import { getTablesInSchema } from "../internal/getTablesInSchema"; import { inspectError } from "../internal/inspectError"; import { log, progress, stdout } from "../internal/logging"; import { join } from "../internal/quote"; import { resultAbort } from "../internal/resultAbort"; import { resultCommit } from "../internal/resultCommit"; import { runSqlProgressNonTransactional, runSqlProgressTransactional, } from "../internal/runSqlProgress"; import { startCopyingTables } from "../internal/startCopyingTables"; import { validatePrimaryKeysFromDump } from "../internal/validatePrimaryKeysFromDump"; import { waitUntilBackfillCompletes } from "../internal/waitUntilBackfillCompletes"; import { waitUntilEverythingIsNotLaggingMuch } from "../internal/waitUntilEverythingIsNotLaggingMuch"; import type { Unlock } from "../internal/waitUntilIncrementalCompletes"; import { waitUntilIncrementalCompletes } from "../internal/waitUntilIncrementalCompletes"; import { wrapSigInt } from "../internal/wrapSigInt"; /** * Moves or copies a schema from one master DB to another. */ export async function move({ schema, fromDsn, toDsn, commitAction, deactivateSQL, maxReplicationLagSec = 20, wait = false, validateFKs = false, }: { schema: string; fromDsn: string; toDsn: string; commitAction: "deactivate-activate" | "rename-to-new" | null; deactivateSQL?: string; maxReplicationLagSec?: number; wait?: boolean; validateFKs?: boolean; }): Promise<void> { let unlock: Unlock = async (): Promise<void> => {}; try { const { tables, partitionedTables } = await getTablesInSchema({ fromDsn, schema, }); const { preData, primaryKeys, postData } = await getDump({ fromDsn, schema, validateFKs, }); validatePrimaryKeysFromDump({ primaryKeys, allTables: [...tables, ...partitionedTables], }); await waitUntilEverythingIsNotLaggingMuch({ when: "before starting the work", fromDsn: undefined, // only destination physical replicas are checked tables, toDsn, schema, maxReplicationLagSec, waitForUserToContinue: false, }); try { await cleanUpPubSub({ fromDsn, toDsn, schema }); await runSqlProgressTransactional( toDsn, preData, `Applying initial DDL (except indexes) for ${schema} to ${toDsn}`, ); } catch (e: unknown) { // No cleanup if failed here since it's one transaction. log.error(inspectError(e)); throw ( "Exited abnormally with no-op" + (typeof e === "string" ? ` (${e})` : "") ); } try { await wrapSigInt(async (throwIfAborted) => { if (primaryKeys.length > 0) { await runSqlProgressTransactional( toDsn, join(primaryKeys, "\n"), "Creating minimal indexes to let the logical replication replay the data", ); } // During the tables copying, change their owner to the toDsn user, // assuming that toDsn's user may have custom values for e.g. work_mem // or other parameters. PostgreSQL runs tablesync replication worker // with the privileges and settings of the table's owner, so it allows // us to have "per-tablesync" limits customization. E.g. we may want a // way higher value for work_mem (gigabytes) to prevent excessive // creation of temp files, and it's safe, since there are just a few // tablesync workers running concurrently in the cluster (depends on // max_sync_workers_per_subscription PostgreSQL setting). const newOwner = new URL(toDsn).username; const tablesWithOtherOwner = tables.filter( ({ owner }) => owner !== newOwner || process.env["DEBUG_CHANGE_TABLE_OWNERS"], ); await changeTableOwners({ toDsn, tables: tablesWithOtherOwner.map(({ schema, name }) => ({ schema, name, owner: newOwner, })), }); throwIfAborted(); await startCopyingTables({ fromDsn, tables, toDsn, schema }); throwIfAborted(); await waitUntilBackfillCompletes( { fromDsn, tables, toDsn, schema }, throwIfAborted, ); throwIfAborted(); await changeTableOwners({ toDsn, tables: tablesWithOtherOwner, comment: "Restoring table(s) ownership back, since tablesync workers have completed", }); throwIfAborted(); await waitUntilEverythingIsNotLaggingMuch( { when: "before creating heavy indexes", fromDsn, tables, toDsn, schema, maxReplicationLagSec, waitForUserToContinue: wait, }, throwIfAborted, ); throwIfAborted(); await disableSubscription({ toDsn, tables, schema }); throwIfAborted(); await runSqlProgressNonTransactional( toDsn, postData, "Creating heavy indexes, foreign keys etc.", ); throwIfAborted(); await enableSubscription({ toDsn, tables, schema }); throwIfAborted(); await analyzeTables({ toDsn, tables }); throwIfAborted(); await waitUntilEverythingIsNotLaggingMuch( { when: commitAction ? "before activating the destination microshard" : "before removing the logical subscription", fromDsn, tables, toDsn, schema, maxReplicationLagSec, waitForUserToContinue: wait, }, throwIfAborted, ); throwIfAborted(); unlock = await waitUntilIncrementalCompletes( { fromDsn, schema, tables }, throwIfAborted, ); // This runs while the fromDsn tables are still locked. await advanceSequences({ fromDsn, toDsn }); }); } catch (e: unknown) { log.error(inspectError(e)); log("Cleaning up the half-migrated destination."); try { await unlock("error"); } catch (e: unknown) { log.error(`Unlock failed: ${inspectError(e)}`); } finally { unlock = async () => {}; } await delay(1000); await resultAbort({ fromDsn, toDsn, schema }); throw "Exited abnormally" + (typeof e === "string" ? ` (${e})` : ""); } try { await cleanUpPubSub({ fromDsn, toDsn, schema }); if (commitAction) { await resultCommit({ activateOnDestination: commitAction === "deactivate-activate", deactivateSQL, fromDsn, tables, toDsn, schema, }); } try { await analyzeTables({ toDsn, tables }); } catch (e: unknown) { log.error(`Post-move ANALYZE failed (ignoring): ${inspectError(e)}`); } } catch (e: unknown) { log.error(`Commit failed: ${inspectError(e)}`); try { await unlock("error"); } catch (e: unknown) { log.error(`Unlock failed: ${inspectError(e)}`); } finally { unlock = async () => {}; } const dangerBand = chalk.bgRed( chalk.white( "💀 DANGER! " .repeat(300) .trimEnd() .substring(0, (stdout.columns || 80) * 3), ), ); throw compact([ dangerBand, "Exited abnormally while committing the result!", `Schema ${schema} is in half-working state.`, commitAction && `E.g. it may already be inactive on ${fromDsn}, but not yet activated on ${toDsn}.`, "You will likely need a manual intervention.", dangerBand, ]).join("\n"); } } finally { progress.clear(); await unlock(); } }