@clickup/pg-microsharding
Version:
Microshards support for PostgreSQL
151 lines (135 loc) • 4.73 kB
text/typescript
import { execSync } from "child_process";
import { writeFileSync } from "fs";
import compact from "lodash/compact";
import { fileSync, setGracefulCleanup } from "tmp";
import { TMP_DIR, print } from "./logging";
import { shellQuote } from "./quote";
const TMUX_SESSION = "pg-microsharding-tmux";
const ACTIVE_BG = "#000033";
// We do not want to pass the following psql envs through tmux session, because
// the data in them is already included in the DSNs in the command line.
const EXCLUDE_PG_ENV = [
"PGUSER",
"PGPASSWORD",
"PGHOST",
"PGPORT",
"PGDATABASE",
"PGDSNS",
];
/**
* Returns true if this script runs inside a TMUX session.
*/
export function isInTmuxSession(): boolean {
return !!process.env["TMUX"];
}
/**
* If there is already an active TMUX session, reattaches to it. We need to keep
* the strong notion of singleton for all long-running microsharding actions, so
* that e.g. if we run `pg-microsharding rebalance` in one terminal, and then
* run it again in another terminal, the second run will attach to the first
* one.
*/
export async function tryReattachToTmuxSession(): Promise<void> {
if (isInTmuxSession()) {
return;
}
try {
execSync("which tmux", { stdio: "ignore" });
} catch {
throw "TMUX is required to run this action; please install it";
}
// Try to reattach to an existing session instead of starting a new one (in
// case we e.g. got disconnected).
try {
execSync(`tmux attach-session -d -t ${TMUX_SESSION} 2>/dev/null`, {
stdio: "inherit",
});
process.exit(0);
} catch {
// No such session yet.
}
}
/**
* Opens M TMUX panes, in each run N commands sequentially (panes[M][N]). If a
* single command fails, then fails the whole pane.
*
* In case we already run it from a TMUX session, just runs all the commands
* sequentially.
*/
export async function runInTmux(
panes: Array<Array<{ key: string; title: string; command: string[] }>>,
): Promise<void> {
if (isInTmuxSession()) {
for (const { title, command } of panes.flat()) {
print.section(`\n##\n## ${title}\n##\n`);
execSync(command.map((v) => shellQuote(v)).join(" "), {
stdio: "inherit",
});
}
return;
}
setGracefulCleanup();
const tmuxCommands: string[] = ["set -e"];
for (const [i, sequence] of panes.entries()) {
const commands: string[] = [
"set -e",
String.raw`trap 'test $? -eq 0 && printf "\033[1;32mDONE. Press ^C to close this pane..." || printf "\033[0;31mFAILURE. Press ^C to close this pane..."; sleep 100000' EXIT`,
];
for (const [i, { title, command }] of sequence.entries()) {
const progress = `${i + 1} of ${sequence.length}`;
commands.push(
`printf '\\033]2;%s\\033\\' ' ${progress} | ${title} '`,
// Pass env vars like PGOPTIONS, PGSSLCERT etc. through tmux.
...compact(
Object.entries(process.env).map(
([k, v]) =>
v &&
k.match(/^PG[A-Z][A-Z_]*$/) &&
!EXCLUDE_PG_ENV.includes(k) &&
`export ${k}=${shellQuote(v)}`,
),
),
command.map(shellQuote).join(" "),
"echo",
);
}
// TMUX has a limit on the command total length:
// https://github.com/tmux/tmux/issues/254. So instead of passing the big
// script, we put this script in a temporary file and then run it using the
// `bash this-temp-file` plain command via TMUX. Notice that the temporary
// files are auto-removed, but if the script is terminated cruelly with e.g.
// -9 signal, they may linger; it's not a big deal since they are cleaned up
// by linux tmpwatch anyway, we just don't have a better option.
const tmpFile = fileSync({
tmpdir: TMP_DIR,
prefix:
"tmux-" +
sequence
.map(({ key }) => key)
.slice(0, 10)
.join("-"),
}).name;
writeFileSync(tmpFile, commands.join("\n"));
if (i === 0) {
tmuxCommands.push(
`tmux new-session -s ${TMUX_SESSION} -d bash ${shellQuote(tmpFile)}`,
);
} else {
tmuxCommands.push(
`tmux split-window -f bash ${shellQuote(tmpFile)}`,
"tmux select-layout tiled",
);
}
}
tmuxCommands.push(
'tmux bind -n "C-c" confirm-before -p "Confirm SIGINT? (y/n)" "send-keys C-c"',
`tmux set -g window-active-style "bg=${ACTIVE_BG}"`,
`tmux set -g pane-active-border-style "bg=${ACTIVE_BG}"`,
"tmux set mouse on",
"tmux set pane-border-status top",
'tmux set pane-border-format "#T"',
"tmux select-layout tiled",
`tmux attach-session -d -t ${TMUX_SESSION}`,
);
execSync(tmuxCommands.join("\n"), { stdio: "inherit" });
}