#!/usr/bin/env bash
# Install whp-backup.slice (cgroup v2 io.max throttling for backup workers)
# and rewrite the backup-worker cron entry to invoke through the
# whp-backup-runner wrapper.
#
# Background: ionice -c3 (the previous throttle inside BackupJob.php) is a
# no-op when the disk uses the `none` / mq-deadline / kyber scheduler —
# only bfq honors it. On the WHP01 VM both sda and sdb use `none`, so
# every previous ionice call had zero effect on actual I/O priority.
# cgroup v2 io.max applies at the block layer regardless of scheduler.
#
# This script:
#   1. verifies cgroup v2 is mounted
#   2. detects the underlying block device for /docker/backups
#   3. writes /etc/systemd/system/whp-backup.slice with bandwidth+IOPS caps
#      keyed to that device, and reloads systemd
#   4. installs whp-backup-runner.sh to /usr/local/sbin/whp-backup-runner
#   5. rewrites the backup-worker crontab entry to invoke through the runner
#
# Idempotent: safe to re-run; replaces stale slice/runner/cron entries only
# when they actually drift.

set -euo pipefail

SLICE_PATH="/etc/systemd/system/whp-backup.slice"
# Resolve sibling scripts via tarball-relative path. update.sh runs versioned
# scripts BEFORE rsyncing scripts/ to /root/whp/scripts/, so /root/whp/scripts/
# is stale (or empty) during initial install. The tarball layout is:
#   <root>/scripts/updates/<version>/setup-backup-throttle.sh   <- this file
#   <root>/scripts/whp-backup-runner.sh
#   <root>/scripts/apply-backup-throttle.php
# After a successful deploy these also live at /root/whp/scripts/, so the same
# relative path resolves correctly on subsequent re-runs.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SIBLING_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
RUNNER_SRC="${SIBLING_DIR}/whp-backup-runner.sh"
APPLY_SRC="${SIBLING_DIR}/apply-backup-throttle.php"
RUNNER_DEST="/usr/local/sbin/whp-backup-runner"
BACKUP_DIR="/docker/backups"
LOCK_PATH="/run/whp-backup-worker.lock"
CRON_TIMEOUT="600"
WORKER_CMD="/usr/bin/php /docker/whp/web/scripts/backup-worker.php"
# Final cron entry shape: flock+timeout stay visible at the cron layer (per
# CLAUDE.md convention) and the runner sits between them and the worker to
# attach the cgroup scope. wrap-per-minute-crons.sh recognizes this shape
# as already-wrapped and will skip it on future runs.
CRON_LINE="* * * * * /usr/bin/flock -n ${LOCK_PATH} /usr/bin/timeout ${CRON_TIMEOUT} ${RUNNER_DEST} ${WORKER_CMD} >/dev/null 2>&1"

# Bandwidth + IOPS caps. Conservative starting points — operator can override
# per-host via /etc/systemd/system/whp-backup.slice.d/local.conf without
# editing this script.
CAP_READ_BW="80M"
CAP_WRITE_BW="40M"
CAP_READ_IOPS="3000"
CAP_WRITE_IOPS="1500"

# ---- 1. Verify cgroup v2 ------------------------------------------------
if ! grep -q "^cgroup2 " /proc/mounts; then
    echo "ERROR: cgroup v2 not mounted; io.max is unavailable." >&2
    echo "       Boot with systemd.unified_cgroup_hierarchy=1 (default on AlmaLinux 9)." >&2
    exit 1
fi

# ---- 2. Verify sibling scripts are present in the tarball ----------------
if [[ ! -f "${RUNNER_SRC}" ]]; then
    echo "ERROR: ${RUNNER_SRC} missing — release tarball layout has changed?" >&2
    exit 1
fi
if [[ ! -f "${APPLY_SRC}" ]]; then
    echo "ERROR: ${APPLY_SRC} missing — release tarball layout has changed?" >&2
    exit 1
fi

# ---- 3. Resolve the underlying block device for /docker/backups ---------
mkdir -p "${BACKUP_DIR}"
SRC_DEV="$(df --output=source "${BACKUP_DIR}" | tail -1)"
# Walk to parent disk if SRC_DEV is a partition (e.g. /dev/sda1 becomes
# /dev/sda) so io.max applies to the whole device, not one partition.
PARENT="$(lsblk -ndo PKNAME "${SRC_DEV}" 2>/dev/null || true)"
if [[ -n "${PARENT}" ]]; then
    BLK_DEV="/dev/${PARENT}"
else
    BLK_DEV="${SRC_DEV}"
fi
if [[ ! -b "${BLK_DEV}" ]]; then
    echo "ERROR: could not resolve block device for ${BACKUP_DIR} (got ${BLK_DEV})" >&2
    exit 1
fi
echo "  backup target ${BACKUP_DIR} -> ${BLK_DEV}"

# ---- 4. Write the slice unit (only if it has drifted) -------------------
NEW_SLICE=$(cat <<EOF
[Unit]
Description=WHP backup workers (bounded I/O so backups cannot starve live traffic)

[Slice]
# I/O throttling at the block layer — works regardless of disk scheduler.
# Throttles ${BLK_DEV} (the device hosting ${BACKUP_DIR}).
# Override per-host via /etc/systemd/system/whp-backup.slice.d/local.conf.
IOAccounting=yes
IOWeight=10
IOReadBandwidthMax=${BLK_DEV} ${CAP_READ_BW}
IOWriteBandwidthMax=${BLK_DEV} ${CAP_WRITE_BW}
IOReadIOPSMax=${BLK_DEV} ${CAP_READ_IOPS}
IOWriteIOPSMax=${BLK_DEV} ${CAP_WRITE_IOPS}

# CPU bound — pigz can chew N/2 cores; this keeps it from dominating.
CPUAccounting=yes
CPUWeight=20
EOF
)

CURRENT_SLICE="$(cat "${SLICE_PATH}" 2>/dev/null || true)"
if [[ "${CURRENT_SLICE}" != "${NEW_SLICE}" ]]; then
    echo "${NEW_SLICE}" > "${SLICE_PATH}"
    systemctl daemon-reload
    echo "  installed ${SLICE_PATH}"
else
    echo "  ${SLICE_PATH}: already current"
fi

# ---- 5. Install the runner wrapper at a stable path ---------------------
# install(1) replaces atomically; safe vs. an in-flight cron firing.
install -m 0755 "${RUNNER_SRC}" "${RUNNER_DEST}"
echo "  installed ${RUNNER_DEST}"

# ---- 6. Rewrite the backup-worker cron entry ----------------------------
CURRENT_CRON="$(crontab -l 2>/dev/null || true)"

# Already wrapped with the runner? Done.
if echo "${CURRENT_CRON}" | grep -qF "${RUNNER_DEST} ${WORKER_CMD}"; then
    echo "  crontab: already invokes through ${RUNNER_DEST}"
else
    # Drop any existing line that mentions backup-worker.php (wrapped or not).
    TRIMMED="$(echo "${CURRENT_CRON}" | grep -vF "backup-worker.php" || true)"
    if [[ -n "${TRIMMED}" ]]; then
        NEW_CRON="${TRIMMED}"$'\n'"${CRON_LINE}"
    else
        NEW_CRON="${CRON_LINE}"
    fi
    echo "${NEW_CRON}" | crontab -
    echo "  crontab: backup-worker entry now invokes through ${RUNNER_DEST}"
fi

mkdir -p /run

# ---- 7. Re-materialize UI-set caps from settings.json (if any) ----------
# Installer ships baseline defaults baked into the main slice unit. If the
# operator has previously set caps via Server Settings -> Services, those
# live in settings.json and need to be materialized into the slice drop-in
# so an installer re-run after an upgrade preserves them.
/usr/bin/php "${APPLY_SRC}" || echo "  (apply-backup-throttle.php returned non-zero; drop-in unchanged)"

echo
echo "setup-backup-throttle.sh: OK"
echo "  device : ${BLK_DEV}"
echo "  caps   : ${CAP_READ_BW} read / ${CAP_WRITE_BW} write / ${CAP_READ_IOPS} rIOPS / ${CAP_WRITE_IOPS} wIOPS"
echo "         (override via Server Settings -> Services -> Backup I/O Throttle)"
echo "  verify : systemctl show whp-backup.slice -p IOWriteBandwidthMax"
echo "  stats  : cat /sys/fs/cgroup/whp-backup.slice/io.stat   (after a backup run)"
