Docker images / prepare-release (push) Successful in 9s
Docker images / backend-test (push) Successful in 2m25s
Docker images / frontend-image (push) Successful in 3m26s
Docker images / updater-image (push) Successful in 45s
Docker images / backend-image (push) Successful in 2m32s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 13s
PostgreSQL 42809: FILLFACTOR и autovacuum нельзя задавать на partitioned parent — только на листьях. Co-authored-by: Cursor <[email protected]>
193 lines
6.7 KiB
TypeScript
193 lines
6.7 KiB
TypeScript
import type { Pool } from "pg"
|
|
|
|
export type PartitionKind = "day" | "week" | "month"
|
|
|
|
export interface PartitionSpec {
|
|
parent: string
|
|
kind: PartitionKind
|
|
keepDays: number
|
|
}
|
|
|
|
/** Leaf-only: PG forbids storage params on partitioned parents (SQLSTATE 42809). */
|
|
const FACT_LEAF_STORAGE =
|
|
"fillfactor = 70, autovacuum_vacuum_scale_factor = 0.05, autovacuum_vacuum_cost_limit = 2000"
|
|
|
|
const FACT_PARENTS = new Set(["flow_hour_facts", "flow_daily_facts"])
|
|
|
|
export const PARTITION_SPECS: PartitionSpec[] = [
|
|
{ parent: "flow_buckets", kind: "day", keepDays: 4 },
|
|
{ parent: "flow_minute_stats", kind: "day", keepDays: 4 },
|
|
{ parent: "flow_minute_dims", kind: "day", keepDays: 4 },
|
|
{ parent: "flow_daily_dims", kind: "month", keepDays: 420 },
|
|
{ parent: "flow_hour_facts", kind: "day", keepDays: 3 },
|
|
{ parent: "flow_daily_facts", kind: "month", keepDays: 420 },
|
|
{ parent: "traffic_samples", kind: "week", keepDays: 21 },
|
|
{ parent: "servers_rest_ping_samples", kind: "week", keepDays: 35 },
|
|
{ parent: "uptime_probe_samples", kind: "week", keepDays: 21 },
|
|
{ parent: "uptime_resource_samples", kind: "week", keepDays: 21 },
|
|
{ parent: "server_snapshots", kind: "week", keepDays: 21 },
|
|
{ parent: "internet_path_snapshots", kind: "week", keepDays: 21 },
|
|
]
|
|
|
|
const WEEK_SLACK_DAYS = 7
|
|
|
|
async function resolveKeepDays(pool: Pool): Promise<Map<string, number>> {
|
|
const map = new Map(PARTITION_SPECS.map((s) => [s.parent, s.keepDays]))
|
|
try {
|
|
const traffic = await pool.query<{ retention_days: number }>(
|
|
`SELECT retention_days FROM traffic_settings WHERE id = 1`,
|
|
)
|
|
const td = Number(traffic.rows[0]?.retention_days)
|
|
if (Number.isFinite(td) && td > 0) map.set("traffic_samples", td + WEEK_SLACK_DAYS)
|
|
|
|
const uptime = await pool.query<{ retention_days: number }>(
|
|
`SELECT retention_days FROM uptime_settings WHERE id = 1`,
|
|
)
|
|
const ud = Number(uptime.rows[0]?.retention_days)
|
|
if (Number.isFinite(ud) && ud > 0) {
|
|
map.set("uptime_probe_samples", ud + WEEK_SLACK_DAYS)
|
|
map.set("uptime_resource_samples", ud + WEEK_SLACK_DAYS)
|
|
}
|
|
|
|
const path = await pool.query<{ retention_days: number }>(
|
|
`SELECT retention_days FROM internet_path_settings WHERE id = 1`,
|
|
)
|
|
const pd = Number(path.rows[0]?.retention_days)
|
|
if (Number.isFinite(pd) && pd > 0) map.set("internet_path_snapshots", pd + WEEK_SLACK_DAYS)
|
|
} catch {
|
|
/* settings may be absent mid-migration */
|
|
}
|
|
return map
|
|
}
|
|
|
|
function utcDate(d: Date): Date {
|
|
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()))
|
|
}
|
|
|
|
function addUtcDays(d: Date, n: number): Date {
|
|
const x = utcDate(d)
|
|
x.setUTCDate(x.getUTCDate() + n)
|
|
return x
|
|
}
|
|
|
|
function startOfWeekUtc(d: Date): Date {
|
|
const x = utcDate(d)
|
|
const day = x.getUTCDay() || 7
|
|
x.setUTCDate(x.getUTCDate() - (day - 1))
|
|
return x
|
|
}
|
|
|
|
function startOfMonthUtc(d: Date): Date {
|
|
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1))
|
|
}
|
|
|
|
function fmtDay(d: Date): string {
|
|
return d.toISOString().slice(0, 10)
|
|
}
|
|
|
|
function partName(parent: string, kind: PartitionKind, start: Date): string {
|
|
if (kind === "day") return `${parent}_d_${fmtDay(start).replaceAll("-", "")}`
|
|
if (kind === "week") return `${parent}_w_${fmtDay(start).replaceAll("-", "")}`
|
|
return `${parent}_m_${start.getUTCFullYear()}${String(start.getUTCMonth() + 1).padStart(2, "0")}`
|
|
}
|
|
|
|
function rangeFor(kind: PartitionKind, ts: Date): { start: Date; end: Date } {
|
|
if (kind === "day") {
|
|
const start = utcDate(ts)
|
|
return { start, end: addUtcDays(start, 1) }
|
|
}
|
|
if (kind === "week") {
|
|
const start = startOfWeekUtc(ts)
|
|
return { start, end: addUtcDays(start, 7) }
|
|
}
|
|
const start = startOfMonthUtc(ts)
|
|
const end = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth() + 1, 1))
|
|
return { start, end }
|
|
}
|
|
|
|
export async function ensurePartitionFor(
|
|
pool: Pool,
|
|
parent: string,
|
|
kind: PartitionKind,
|
|
ts: Date,
|
|
): Promise<string> {
|
|
const { start, end } = rangeFor(kind, ts)
|
|
const name = partName(parent, kind, start)
|
|
const from = fmtDay(start)
|
|
const to = fmtDay(end)
|
|
await pool.query(
|
|
`CREATE TABLE IF NOT EXISTS ${name} PARTITION OF ${parent} FOR VALUES FROM ('${from}') TO ('${to}')`,
|
|
)
|
|
if (FACT_PARENTS.has(parent)) {
|
|
await pool.query(`ALTER TABLE ${name} SET (${FACT_LEAF_STORAGE})`)
|
|
}
|
|
return name
|
|
}
|
|
|
|
export async function ensurePartitionsBetween(
|
|
pool: Pool,
|
|
parent: string,
|
|
kind: PartitionKind,
|
|
from: Date,
|
|
to: Date,
|
|
): Promise<void> {
|
|
const start = from.getTime() <= to.getTime() ? from : to
|
|
const end = from.getTime() <= to.getTime() ? to : from
|
|
for (let t = new Date(start.getTime()); t <= end; ) {
|
|
await ensurePartitionFor(pool, parent, kind, t)
|
|
if (kind === "day") t = addUtcDays(t, 1)
|
|
else if (kind === "week") t = addUtcDays(t, 7)
|
|
else t = new Date(Date.UTC(t.getUTCFullYear(), t.getUTCMonth() + 1, 1))
|
|
}
|
|
}
|
|
|
|
export async function ensurePartitionsAround(pool: Pool, around = new Date()): Promise<void> {
|
|
const keepDays = await resolveKeepDays(pool)
|
|
for (const spec of PARTITION_SPECS) {
|
|
const keep = keepDays.get(spec.parent) ?? spec.keepDays
|
|
const daysAhead = spec.kind === "month" ? 40 : spec.kind === "week" ? 21 : 8
|
|
const start = addUtcDays(around, -keep)
|
|
const end = addUtcDays(around, daysAhead)
|
|
for (let t = new Date(start.getTime()); t < end; ) {
|
|
await ensurePartitionFor(pool, spec.parent, spec.kind, t)
|
|
if (spec.kind === "day") t = addUtcDays(t, 1)
|
|
else if (spec.kind === "week") t = addUtcDays(t, 7)
|
|
else t = new Date(Date.UTC(t.getUTCFullYear(), t.getUTCMonth() + 1, 1))
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function dropExpiredPartitions(pool: Pool, around = new Date()): Promise<void> {
|
|
const keepDays = await resolveKeepDays(pool)
|
|
for (const spec of PARTITION_SPECS) {
|
|
const keep = keepDays.get(spec.parent) ?? spec.keepDays
|
|
const cutoff = addUtcDays(around, -keep)
|
|
const { rows } = await pool.query<{ relname: string }>(
|
|
`SELECT c.relname
|
|
FROM pg_inherits i
|
|
JOIN pg_class c ON c.oid = i.inhrelid
|
|
JOIN pg_class p ON p.oid = i.inhparent
|
|
WHERE p.relname = $1`,
|
|
[spec.parent],
|
|
)
|
|
for (const row of rows) {
|
|
const m = row.relname.match(/_([dwm])_(\d{8}|\d{6})$/)
|
|
if (!m) continue
|
|
const stamp = m[2]
|
|
let start: Date
|
|
if (stamp.length === 6) {
|
|
start = new Date(Date.UTC(Number(stamp.slice(0, 4)), Number(stamp.slice(4, 6)) - 1, 1))
|
|
} else {
|
|
start = new Date(`${stamp.slice(0, 4)}-${stamp.slice(4, 6)}-${stamp.slice(6, 8)}T00:00:00Z`)
|
|
}
|
|
if (start < cutoff) {
|
|
await pool.query(`DROP TABLE IF EXISTS ${row.relname}`)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function specForParent(parent: string): PartitionSpec | undefined {
|
|
return PARTITION_SPECS.find((s) => s.parent === parent)
|
|
}
|