diff --git a/start.sh b/start.sh index 35d2a9f..e299dc7 100755 --- a/start.sh +++ b/start.sh @@ -191,7 +191,29 @@ setup_scheduled_jobs() { fi local update_line backup_line - update_line="0 3 * * * cd $SCRIPT_DIR && ./update.sh >> $SCRIPT_DIR/logs/update.log 2>&1" + local slot_seed slot_hash slot_bucket slot_hour slot_minute + local update_window_minutes update_window_hours + + update_window_minutes="${INVENTAR_UPDATE_WINDOW_MINUTES:-120}" + if ! [[ "$update_window_minutes" =~ ^[0-9]+$ ]]; then + update_window_minutes="120" + fi + if [ "$update_window_minutes" -lt 15 ]; then + update_window_minutes="15" + fi + + update_window_hours=$(( (update_window_minutes + 59) / 60 )) + if [ "$update_window_hours" -lt 1 ]; then + update_window_hours=1 + fi + + slot_seed="$(cat /etc/machine-id 2>/dev/null || hostname)|${INVENTAR_INSTANCE_ID:-${HOSTNAME:-unknown}}|$SCRIPT_DIR|nightly-update" + slot_hash="$(printf '%s' "$slot_seed" | sha256sum | awk '{print $1}')" + slot_bucket=$((16#${slot_hash:0:8} % update_window_minutes)) + slot_hour=$((3 + (slot_bucket / 60))) + slot_minute=$((slot_bucket % 60)) + + update_line="$slot_minute $slot_hour * * * cd $SCRIPT_DIR && INVENTAR_UPDATE_MODE=auto ./update.sh >> $SCRIPT_DIR/logs/update.log 2>&1" backup_line="30 2 * * * cd $SCRIPT_DIR && ./backup.sh --mode auto >> $SCRIPT_DIR/logs/backup.log 2>&1" local existing_cron @@ -212,7 +234,7 @@ setup_scheduled_jobs() { fi echo "Nightly backup scheduled at 02:30" - echo "Nightly auto-update scheduled at 03:00" + printf 'Nightly auto-update scheduled at %02d:%02d (deterministic instance slot, %s-minute window)\n' "$slot_hour" "$slot_minute" "$update_window_minutes" } ensure_tls_certificates() { diff --git a/update.sh b/update.sh index d99cfb4..0054829 100755 --- a/update.sh +++ b/update.sh @@ -15,6 +15,9 @@ APP_IMAGE_ASSET_PREFIX="inventarsystem-image-" ENV_FILE="$PROJECT_DIR/.docker-build.env" APP_IMAGE_REPO="ghcr.io/aiirondev/legendary-octo-garbanzo" DIST_DIR="$PROJECT_DIR/dist" +LOCK_FILE="$PROJECT_DIR/.update.lock" + +DEFAULT_UPDATE_BUFFER_MINUTES="${INVENTAR_UPDATE_BUFFER_MINUTES:-90}" mkdir -p "$LOG_DIR" chmod 777 "$LOG_DIR" 2>/dev/null || true @@ -23,6 +26,74 @@ log_message() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" } +safe_int() { + local raw="$1" + if [[ "$raw" =~ ^[0-9]+$ ]]; then + echo "$raw" + else + echo "0" + fi +} + +acquire_update_lock() { + exec 9>"$LOCK_FILE" + if ! flock -n 9; then + log_message "Another update process is already running. Exiting." + exit 0 + fi +} + +compute_update_buffer_seconds() { + if [ "${INVENTAR_SKIP_UPDATE_BUFFER:-0}" = "1" ]; then + echo "0" + return 0 + fi + + # Manual interactive runs should not wait by default. + if [ -t 1 ] && [ "${INVENTAR_FORCE_UPDATE_BUFFER:-0}" != "1" ]; then + echo "0" + return 0 + fi + + local explicit_seconds + explicit_seconds="$(safe_int "${INVENTAR_UPDATE_BUFFER_SECONDS:-0}")" + if [ "$explicit_seconds" -gt 0 ]; then + echo "$explicit_seconds" + return 0 + fi + + local window_minutes window_seconds + window_minutes="$(safe_int "$DEFAULT_UPDATE_BUFFER_MINUTES")" + if [ "$window_minutes" -le 0 ]; then + echo "0" + return 0 + fi + + window_seconds=$((window_minutes * 60)) + + local machine_id instance_id seed hash_hex hash_dec + machine_id="$(cat /etc/machine-id 2>/dev/null || hostname)" + instance_id="${INVENTAR_INSTANCE_ID:-${HOSTNAME:-unknown}}" + seed="$machine_id|$instance_id|$PROJECT_DIR|update-buffer" + + hash_hex="$(printf '%s' "$seed" | sha256sum | awk '{print $1}')" + hash_dec=$((16#${hash_hex:0:8})) + + echo $((hash_dec % window_seconds)) +} + +apply_update_buffer_if_needed() { + local delay_seconds + delay_seconds="$(compute_update_buffer_seconds)" + if [ "$delay_seconds" -le 0 ]; then + log_message "Update buffer: no delay applied" + return 0 + fi + + log_message "Update buffer active: delaying start by ${delay_seconds}s to reduce cross-instance peak load" + sleep "$delay_seconds" +} + require_cmd() { if ! command -v "$1" >/dev/null 2>&1; then log_message "ERROR: Required command not found: $1" @@ -395,6 +466,9 @@ verify_stack_health() { } main() { + acquire_update_lock + apply_update_buffer_if_needed + ensure_runtime_dependencies ensure_tls_certificates ensure_nginx_config_mount_source