#!/usr/bin/env bash
# PAC Postgres 每日备份 — pg_dump 自定义压缩格式 + 落地校验 + 保留最近 KEEP 份。
#
# 部署:测试机 47.251.104.47 的 /root/pac-backup.sh,cron `0 4 * * *`,
#      日志 >> /root/pac-backups/backup.log、stderr >> backup.err。
# 存放:/root/pac-backups/(宿主盘)。⚠️ 同机同盘,防误删/逻辑错误/升级回滚;
#      ⛔ 不防"整机/磁盘损坏" —— 异地容灾(如阿里云 OSS)仍是下一步。
#
# 用法:
#   bash /root/pac-backup.sh              # 正常跑
#   bash /root/pac-backup.sh --dry-run    # 只算不动手(预检 + 会删哪几份),用来验空间够不够
#
# ═══════════════════════════════════════════════════════════════════════
# 🔴 2026-08-15 事故:**这个脚本自己把库搞崩了**,而且连崩两次(08-11、08-15)。
#
# 经过:04:00 开始写第 4 份 dump(~12G),而**轮转写在 dump 成功之后** ——
#   旧 3 份 33G 全程占着 → 盘打满 → Postgres 当场:
#     PANIC: could not write to file "pg_logical/replorigin_checkpoint.tmp":
#            No space left on device
#     checkpointer process was terminated by signal 6: Aborted
#   → 整个实例重启、走 WAL 崩溃恢复;pg_dump 的连接跟着断,
#     报出来的却是 "server closed the connection unexpectedly"。
#
# ⚠️ **它看起来像 PG 的毛病,其实是这个脚本撑爆的盘。**
#   而且脚本删掉 .partial 之后空间就回来了、库自己恢复完毕,
#   白天查什么都正常 —— 所以连崩两次都没人发现。
# ⚠️ 崩溃恢复还顺手清空了 pg_stat_*(统计文件不跨崩溃保留):
#   那之后查到的死元组数只是崩溃之后攒的,⛔ 别拿它判断表膨胀。
#
# ⇒ 两处结构性修改,⛔ 别改回去:
#   ① **先轮转、再 dump**(留 KEEP-1 份进 dump,写完正好 KEEP 份)。
#      峰值占用从 (KEEP+1)×份 压到 KEEP×份 —— 不再需要凭空多出一份的余量。
#      ⚠️ 代价:dump 失败时手上只剩 KEEP-1 份。比起"把库撑崩",这个代价可接受。
#   ② **动手之前先算够不够**,不够就**不开工**,大声记日志后退出。
#      ⛔ 不许为了腾地方自动多删旧备份 —— 少留几天恢复窗口是人的决定,不是脚本的。
# ═══════════════════════════════════════════════════════════════════════
set -euo pipefail

DIR=/root/pac-backups
CONTAINER=pac-postgres-1
DB=pac
DBUSER=pac

# 🔴 2026-08-15 由 3 降到 2。算式(盘 197G,PG 数据卷已 75G 且在长,一份 dump ~12G):
#   KEEP=3 静息就占 36G,两天内盘会重新回到 5G 余量 —— 那正是事故前的水位。
#   KEEP=2 静息 24G、dump 期间峰值同样 24G(先轮转),给 WAL 和别的项目构建留出余地。
# ⚠️ 这是权宜之计:dump 每天还在长(11G→11G→12G),真正的出路是把它送出本盘。
KEEP=2

# dump 跑完盘上至少还要剩这么多(GB)。⛔ 别调到 0:
#   PG 的 WAL、docker 构建、以及这台机器上另外 ~30 个无关容器都还要写盘。
RESERVE_GB=10

DRY_RUN=0
[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=1

TS=$(date +%F_%H%M)
FILE="$DIR/pac-$TS.dump"
mkdir -p "$DIR"
log() { printf "%s %s\n" "$(date "+%F %T")" "$*"; }
gb() { awk -v k="$1" 'BEGIN{printf "%.1fG", k/1024/1024}'; }

# ── backup.err 自身也会把盘写满(每次失败都追加,事故当天涨得最快)────────
if [[ -f "$DIR/backup.err" ]] && (($(du -k "$DIR/backup.err" | cut -f1) > 5120)); then
  mv -f "$DIR/backup.err" "$DIR/backup.err.1"
fi

# ── ① 预检:先算,不够就不开工 ──────────────────────────────────────
mapfile -t DUMPS < <(ls -1t "$DIR"/pac-*.dump 2>/dev/null || true)

# 这次大概要写多大:按现存最大的一份 ×1.25 估(库在长,⛔ 别按上次的实际值卡死)
if ((${#DUMPS[@]} > 0)); then
  NEED_KB=$(($(du -k "${DUMPS[@]}" | sort -nr | head -1 | cut -f1) * 5 / 4))
else
  NEED_KB=$((14 * 1024 * 1024)) # 一份都没有(首次/刚清过):按 14G 估
fi

# 轮转会删掉哪几份、能腾出多少 —— ⚠️ 这一步只算,还没删
DOOMED=()
((${#DUMPS[@]} >= KEEP)) && DOOMED=("${DUMPS[@]:$((KEEP - 1))}")
FREEABLE_KB=0
((${#DOOMED[@]} > 0)) && FREEABLE_KB=$(du -k "${DOOMED[@]}" | awk '{s+=$1} END{print s+0}')

AVAIL_KB=$(df -Pk "$DIR" | awk 'NR==2{print $4}')
AFTER_KB=$((AVAIL_KB + FREEABLE_KB - NEED_KB))
log "预检:可用 $(gb "$AVAIL_KB") + 轮转可腾 $(gb "$FREEABLE_KB") − 本次约需 $(gb "$NEED_KB") = 结束后约 $(gb "$AFTER_KB")(下限 ${RESERVE_GB}G)"

if ((AFTER_KB < RESERVE_GB * 1024 * 1024)); then
  log "FAIL: 空间不足,本次不备份 —— ⛔ 现存 ${#DUMPS[@]} 份一份没动。"
  log "      (⛔ 不自动多删旧备份腾地方:少留几天恢复窗口是人的决定,不是脚本的)"
  log "      处理:清盘 / 调小 KEEP / 把 dump 送出本盘(OSS)。"
  exit 1
fi

# ── ② 先轮转,再 dump(峰值就是靠这个顺序压下来的)──────────────────
if ((${#DOOMED[@]} > 0)); then
  log "先轮转(⚠️ 在 dump 之前):删 ${#DOOMED[@]} 份,留 $((KEEP - 1)) 份进本次 dump"
  printf '  %s\n' "${DOOMED[@]}"
  ((DRY_RUN == 0)) && rm -f "${DOOMED[@]}"
fi

if ((DRY_RUN == 1)); then
  log "--dry-run:到此为止,⛔ 没删文件、没跑 pg_dump"
  exit 0
fi

log "开始 pg_dump -> $FILE"
if docker exec "$CONTAINER" pg_dump -U "$DBUSER" -Fc "$DB" >"$FILE.partial" 2>>"$DIR/backup.err"; then
  mv "$FILE.partial" "$FILE"
else
  rm -f "$FILE.partial"
  log "FAIL: pg_dump 失败(见 backup.err);盘还剩 $(gb "$(df -Pk "$DIR" | awk 'NR==2{print $4}')")"
  exit 1
fi
log "完成 size=$(du -h "$FILE" | cut -f1)"

# 落地校验:能列出归档 TOC = 文件可读(不是坏 dump)
if docker exec -i "$CONTAINER" pg_restore -l <"$FILE" >/dev/null 2>&1; then
  log "校验 OK(pg_restore -l 可读)"
else
  log "WARN: 校验失败,dump 可能损坏 — 保留文件人工查"
fi

# ⚠️ 这里**不再**轮转:该删的在 dump 之前就删了,现在正好 KEEP 份。
log "现有 $(ls -1 "$DIR"/pac-*.dump 2>/dev/null | wc -l) 份(上限 $KEEP),盘还剩 $(gb "$(df -Pk "$DIR" | awk 'NR==2{print $4}')"):"
ls -lh "$DIR"/pac-*.dump 2>/dev/null | awk '{print "  "$9" "$5}'
