#!/usr/bin/env bash # # StemeDB WAL Archival to S3 # # Ships WAL segments to S3 every 15 minutes to achieve RPO=15min. # Tracks archival state to avoid re-uploading already archived segments. # # Usage: # ./scripts/archive-wal-to-s3.sh # # Exit codes: # 0 - Archival completed successfully (or nothing to archive) # 1 - Archival failed # set -euo pipefail # Configuration readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly PROJECT_DIR="$(dirname "$SCRIPT_DIR")" readonly WAL_DIR="${STEMEDB_WAL_DIR:-${PROJECT_DIR}/data/wal}" readonly STATE_FILE="${STATE_FILE:-/var/lib/stemedb/wal-archival-state.json}" readonly S3_BUCKET="${AWS_S3_BUCKET:-}" readonly S3_PREFIX="${AWS_S3_PREFIX:-wal-archive}" readonly METRICS_DIR="${METRICS_DIR:-/var/lib/node_exporter/textfile_collector}" # Colors (if terminal supports it) if [[ -t 1 ]]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' BLUE='\033[0;34m' NC='\033[0m' else RED='' GREEN='' YELLOW='' BLUE='' NC='' fi # Logging helpers info() { echo -e "${BLUE}[INFO]${NC} $*"; } success() { echo -e "${GREEN}[OK]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } fail() { echo -e "${RED}[FAIL]${NC} $*"; exit 1; } # Load archival state load_state() { if [[ -f "$STATE_FILE" ]]; then cat "$STATE_FILE" else echo '{"last_archived_segment": "", "last_archival_timestamp": 0, "total_segments_archived": 0}' fi } # Save archival state save_state() { local last_segment="$1" local total_archived="$2" mkdir -p "$(dirname "$STATE_FILE")" cat > "$STATE_FILE" </dev/null || stat -f %m "$wal_file" 2>/dev/null) local now now=$(date +%s) echo $((now - wal_mtime)) } # Write Prometheus metrics write_metrics() { local segments_uploaded="$1" local segments_failed="$2" local max_lag="$3" local metrics_file="${METRICS_DIR}/stemedb_wal_archival.prom" mkdir -p "$(dirname "$metrics_file")" 2>/dev/null || true cat > "$metrics_file" < /dev/null; then fail "AWS CLI not found. Install with: apt install awscli" fi if [[ ! -d "$WAL_DIR" ]]; then fail "WAL directory not found: ${WAL_DIR}" fi # Load state local state state=$(load_state) local last_archived last_archived=$(echo "$state" | grep -o '"last_archived_segment": "[^"]*"' | cut -d'"' -f4) local total_archived total_archived=$(echo "$state" | grep -o '"total_segments_archived": [0-9]*' | cut -d: -f2 | tr -d ' ') info "Last archived: ${last_archived:-none}" info "Total archived: ${total_archived}" # Get segments to archive local segments mapfile -t segments < <(get_segments_to_archive "$last_archived") if [[ ${#segments[@]} -eq 0 ]]; then info "No new segments to archive" write_metrics 0 0 0 return 0 fi info "Found ${#segments[@]} segment(s) to archive" # Upload segments local uploaded=0 local failed=0 local max_lag=0 local new_last_archived="" for wal_file in "${segments[@]}"; do if upload_segment "$wal_file"; then ((uploaded++)) new_last_archived=$(basename "$wal_file") # Track archival lag local lag lag=$(calculate_archival_lag "$wal_file") if [[ $lag -gt $max_lag ]]; then max_lag=$lag fi else ((failed++)) fi done # Update state if [[ -n "$new_last_archived" ]]; then total_archived=$((total_archived + uploaded)) save_state "$new_last_archived" "$total_archived" fi # Write metrics write_metrics "$uploaded" "$failed" "$max_lag" # Summary echo "" echo "==========================================" if [[ $failed -eq 0 ]]; then echo -e " ${GREEN}Archival complete${NC}" else echo -e " ${YELLOW}Archival completed with errors${NC}" fi echo "==========================================" echo "" echo " Uploaded: ${uploaded}" echo " Failed: ${failed}" echo " Max lag: ${max_lag}s" echo " S3 path: s3://${S3_BUCKET}/${S3_PREFIX}/" echo "" if [[ $failed -gt 0 ]]; then exit 1 fi } main "$@"