#!/usr/bin/env bash set -euo pipefail NAME="fixed-phase" CYCLE_SECS="${PHASE_CYCLE_SECS:-900}" OFFSET_SECS="${PHASE_OFFSET_SECS:-0}" LOCK_FILE="${RPKI_HEAVY_LOCK:-/var/lock/rpki-heavy-run.lock}" LOCK_WAIT_SECS="${LOCK_WAIT_SECS:-30}" usage() { cat <<'USAGE' Usage: fixed_phase_loop.sh [--name ] [--cycle-secs ] [--offset-secs ] [--lock-file ] [--lock-wait-secs ] -- [args...] Runs one command at fixed wall-clock phases. Missed phases are skipped rather than caught up, which keeps independent RP jobs from drifting into each other. A shared flock protects against unexpected overruns. USAGE } die() { echo "error: $*" >&2 exit 2 } is_non_negative_int() { [[ "$1" =~ ^[0-9]+$ ]] } while [[ $# -gt 0 ]]; do case "$1" in --name) shift NAME="${1:?--name requires a value}" ;; --cycle-secs) shift CYCLE_SECS="${1:?--cycle-secs requires a value}" ;; --offset-secs) shift OFFSET_SECS="${1:?--offset-secs requires a value}" ;; --lock-file) shift LOCK_FILE="${1:?--lock-file requires a value}" ;; --lock-wait-secs) shift LOCK_WAIT_SECS="${1:?--lock-wait-secs requires a value}" ;; --help|-h) usage exit 0 ;; --) shift break ;; *) die "unknown argument: $1" ;; esac shift done [[ $# -gt 0 ]] || die "missing command after --" is_non_negative_int "$CYCLE_SECS" || die "--cycle-secs must be a non-negative integer" is_non_negative_int "$OFFSET_SECS" || die "--offset-secs must be a non-negative integer" is_non_negative_int "$LOCK_WAIT_SECS" || die "--lock-wait-secs must be a non-negative integer" (( CYCLE_SECS > 0 )) || die "--cycle-secs must be > 0" (( OFFSET_SECS < CYCLE_SECS )) || die "--offset-secs must be < --cycle-secs" mkdir -p "$(dirname "$LOCK_FILE")" timestamp_utc() { date -u +%Y-%m-%dT%H:%M:%SZ } format_epoch_utc() { date -u -d "@$1" +%Y-%m-%dT%H:%M:%SZ } LAST_TARGET_EPOCH=-1 next_phase_epoch() { local now="$1" local shifted=$((now - OFFSET_SECS)) local remainder=$((shifted % CYCLE_SECS)) if (( remainder < 0 )); then remainder=$((remainder + CYCLE_SECS)) fi local sleep_secs=$((CYCLE_SECS - remainder)) if (( sleep_secs == CYCLE_SECS )); then sleep_secs=0 fi printf '%s\n' "$((now + sleep_secs))" } while true; do now_epoch="$(date +%s)" target_epoch="$(next_phase_epoch "$now_epoch")" if (( target_epoch <= LAST_TARGET_EPOCH )); then target_epoch=$((LAST_TARGET_EPOCH + CYCLE_SECS)) fi LAST_TARGET_EPOCH="$target_epoch" sleep_secs=$((target_epoch - now_epoch)) echo "[$(timestamp_utc)] $NAME next_phase=$(format_epoch_utc "$target_epoch") sleep=${sleep_secs}s cycle=${CYCLE_SECS}s offset=${OFFSET_SECS}s" >&2 if (( sleep_secs > 0 )); then sleep "$sleep_secs" fi started_epoch="$(date +%s)" echo "[$(timestamp_utc)] $NAME phase_start target=$(format_epoch_utc "$target_epoch") lock=$LOCK_FILE wait=${LOCK_WAIT_SECS}s" >&2 set +e flock -w "$LOCK_WAIT_SECS" "$LOCK_FILE" "$@" code=$? set -e ended_epoch="$(date +%s)" if (( code == 0 )); then echo "[$(timestamp_utc)] $NAME phase_done exit=0 elapsed=$((ended_epoch - started_epoch))s" >&2 else echo "[$(timestamp_utc)] $NAME phase_done exit=$code elapsed=$((ended_epoch - started_epoch))s skipped_or_failed=1" >&2 fi done