@clickup/pg-microsharding
Version:
Microshards support for PostgreSQL
281 lines (259 loc) • 8.76 kB
text/typescript
import chalk from "chalk";
import chunk from "lodash/chunk";
import compact from "lodash/compact";
import first from "lodash/first";
import flatten from "lodash/flatten";
import mapValues from "lodash/mapValues";
import max from "lodash/max";
import range from "lodash/range";
import round from "lodash/round";
import sum from "lodash/sum";
import sumBy from "lodash/sumBy";
import { table } from "table";
import { weights } from "../api/weights";
import { chunkIntoN } from "../internal/chunkIntoN";
import { WEIGHT_COL_NAME_DEFAULT } from "../internal/getSchemaWeights";
import { indent } from "../internal/indent";
import { print, progress, stdout } from "../internal/logging";
import { dsnShort } from "../internal/names";
import { normalizeDsns } from "../internal/normalizeDsns";
import type { Args } from "../internal/parseArgs";
import { pluralize } from "../internal/pluralize";
import { promiseAllMap } from "../internal/promiseAllMap";
const DEBUG_RENDER_MUL = parseInt(process.env["DEBUG_RENDER_MUL"] ?? "0") || 0;
/**
* Shows the list of microshards and their weights.
*/
export async function actionList(args: Args): Promise<boolean> {
const weightSql = args["weight-sql"] || undefined;
const verbose = !!args["verbose"];
const json = !!args["json"];
const dsns = await normalizeDsns(args["dsns"] || args["dsn"]);
if (dsns.length === 0) {
throw "Please provide --dsn or --dsns, DB DSNs to list the microshards on";
}
const { islandNosToDsn, islands } = await calcIslandWeights({
dsns,
weightSql,
verbose,
});
if (DEBUG_RENDER_MUL > 0) {
[...islands.values()].forEach((shards) =>
shards.push(...flatten(range(DEBUG_RENDER_MUL - 1).map(() => shards))),
);
}
const grandTotalWeightColName =
first(flatten([...islands.values()]).map((s) => s.weightColName)) ??
WEIGHT_COL_NAME_DEFAULT;
const grandTotalWeight = round(
sum(flatten([...islands.values()]).map((s) => s.weight)),
2,
);
const grandTotalUnit = first(
compact(flatten([...islands.values()]).map((s) => s.unit)),
);
const grandTotalShards = sum([...islands.values()].map((s) => s.length));
const firstShard = first(
first([...islands.values()].filter((s) => s.length > 0)),
);
const header = ["Microshard"].concat(
firstShard
? [
firstShard.weightColName,
`${firstShard.weightColName} %`,
...Object.keys(firstShard.other),
]
: [WEIGHT_COL_NAME_DEFAULT, `${WEIGHT_COL_NAME_DEFAULT} %`],
);
// Width in characters of the longest rendered table row.
const tableWidth =
2 + // indent
header.length * 3 + // left borders and paddings around each cell
1 + // last border
(max(
// content of the longest row
flatten([...islands.values()]).map(
({ weight, appliedFactor, unit, otherNormalized, no }) =>
// Microshard
Math.max(header[0].length, no.toString().length) +
// Weight or Size or ...
Math.max(
header[2].length,
round(weight / appliedFactor, 5).toString().length +
(unit ? unit.length + 1 : 0) +
(appliedFactor !== 1 ? ` ✖${appliedFactor}`.length : 0),
) +
// Weight or Size or ... %
Math.max(header[2].length, "100%".length) +
// Other columns
sum(
Object.entries(otherNormalized).map(([k, v]) =>
Math.max(k.length, v.length),
),
),
),
) ?? 0);
const maxTablesSideBySide = Math.trunc(
((stdout.columns || 80) - 10) / tableWidth,
);
const toPrint = {
islands: [...islands].map(([islandNo, shards]) => {
const dsn = islandNosToDsn.get(islandNo)!;
const totalWeight = sumBy(shards, (shard) => shard.weight);
const totalUnit = first(shards)?.unit;
const totalOther = mapValues(first(shards)?.other, (_, k) => {
const values = shards
.map(({ other }) => parseFloat(other[k]))
.filter((v) => !isNaN(v));
return values.length > 0 ? sum(values).toString() : undefined;
});
return {
no: islandNo,
caption:
`${dsnShort(dsn)} — ` + `${pluralize(shards.length, "microshard")}`,
dsn: dsnShort(dsn),
header,
shards: shards.map(
({ no, weight, appliedFactor, unit, otherNormalized }) => [
no.toString(),
round(weight / appliedFactor, 5).toString() +
(unit ? ` ${unit}` : "") +
(appliedFactor !== 1 ? ` ✖${appliedFactor}` : ""),
totalWeight !== 0
? `${((weight / totalWeight) * 100).toFixed(1)}%`
: "-",
...Object.values(otherNormalized),
],
),
total:
shards.length > 0
? [
"total",
totalWeight + (totalUnit ? ` ${totalUnit}` : ""),
"100%",
...Object.values(totalOther).map((v) => v ?? ""),
]
: undefined,
};
}),
total:
`TOTAL ${grandTotalWeightColName.toUpperCase()}: ${grandTotalWeight}` +
(grandTotalUnit ? ` ${grandTotalUnit}` : "") +
`, ${pluralize(islands.size, "island")}, ${pluralize(grandTotalShards, "microshard")}`,
};
if (json) {
print(JSON.stringify(toPrint, null, 2) + "\n");
return true;
}
for (const island of toPrint.islands) {
print.section(island.caption);
if (island.shards.length === 0) {
print(indent(table([["No microshards"]])));
continue;
}
const tables =
json ||
islands.size === 1 ||
maxTablesSideBySide === 0 ||
grandTotalShards + islands.size * 8 < (stdout.rows || 25) ||
island.shards.length <= 4
? [island.shards]
: island.shards.length / maxTablesSideBySide < 2
? chunk(island.shards, 2)
: chunkIntoN(island.shards, maxTablesSideBySide);
print(
glueHorizontally(
tables.map((tbl, tblNo) =>
indent(
table(
compact([
island.header.map((cell) => chalk.bold(cell)),
...tbl.map((row) =>
row[0].trim() === "0"
? row.map((cell) => chalk.yellow(cell))
: row,
),
tblNo === 0 &&
island.total &&
island.total.map((cell) => chalk.bold(cell)),
]),
{
drawHorizontalLine: (i, rowCount) =>
i === 0 ||
i === 1 ||
(tblNo === 0 && i === rowCount - 1) ||
i === rowCount,
},
),
),
),
),
);
}
print(
chalk.bgBlue(" " + chalk.whiteBright(toPrint.total) + " ") +
(verbose && !weightSql ? "" : " (try --verbose)"),
);
return true;
}
export async function calcIslandWeights({
dsns,
weightSql,
verbose,
}: {
dsns: string[];
weightSql: string | undefined;
verbose?: boolean;
}): Promise<{
islandNosToDsn: Map<number, string>;
islands: Map<number, NonNullable<Awaited<ReturnType<typeof weights>>>>;
}> {
try {
const pendingPrefix = "Calculating microshard sizes on: ";
const pendingDsns = new Set<string>();
const islandNosToDsn = new Map([...dsns.entries()]);
const islands = new Map(
await promiseAllMap(
[...islandNosToDsn.entries()],
async ([islandNo, dsn]) => {
try {
pendingDsns.add(dsnShort(dsn));
progress(pendingPrefix + [...pendingDsns].join(", "));
const shards = await weights({
dsn,
weightSql,
verbose,
});
return [islandNo, shards];
} finally {
pendingDsns.delete(dsnShort(dsn));
if (pendingDsns.size > 0) {
progress(pendingPrefix + [...pendingDsns].join(", "));
}
}
},
),
);
return { islandNosToDsn, islands };
} finally {
progress.clear();
}
}
function glueHorizontally(tables: string[]): string {
const rowsByTable = tables.map((table) => table.split("\n"));
const maxRows = max(rowsByTable.map((rows) => rows.length)) ?? 0;
for (const rows of rowsByTable) {
const maxWidth = rows[0].length;
for (let i = 0; i < maxRows; i++) {
rows[i] ||= "";
rows[i] = rows[i].padEnd(maxWidth, " ");
}
}
const result = range(maxRows).map(() => [] as string[]);
for (const rows of rowsByTable) {
for (const [i, col] of rows.entries()) {
result[i].push(col);
}
}
return result.map((lineArray) => lineArray.join("")).join("\n");
}