Files
Marek Miklewicz bbc1afb5e1 Fix worker.sh lock permanently stalling after a crash
The mkdir-based worker lock had no staleness detection at all, unlike
BackupQueueService::withQueueLock()'s 120s override for its own queue
lock. If the root cron-driven worker was ever killed ungracefully (OOM,
kill -9, reboot mid-job), the lock directory was left behind forever -
every subsequent cron tick would silently exit without processing any
job, so backups/restores would queue forever with no visible error.

Track the owning PID inside the lock directory and reclaim the lock
when that PID is no longer running, or when the lock has no PID file
and is older than WORKER_LOCK_STALE_SECONDS (default 300s). Log to
stderr on both the "held by a live worker" and "reclaiming stale lock"
paths instead of exiting silently either way. cleanup() now uses rm -rf
since the lock directory holds a pid file, not just an empty marker.
2026-07-04 21:56:52 +02:00

144 lines
3.6 KiB
Bash

#!/bin/bash
set -euo pipefail
PLUGIN_DIR="${PLUGIN_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
DATA_DIR="$PLUGIN_DIR/data"
JOB_DIR="$DATA_DIR/jobs/pending"
PROCESSING_DIR="$DATA_DIR/jobs/processing"
DONE_DIR="$DATA_DIR/jobs/done"
WORKER_LOCK_DIR="$DATA_DIR/worker.lock"
DB_LOCK_DIR="$DATA_DIR/lock"
PID_DIR="$DATA_DIR/pids"
CANCEL_DIR="$DATA_DIR/cancel"
WORKER_LOCK_STALE_SECONDS="${WORKER_LOCK_STALE_SECONDS:-300}"
mkdir -p "$JOB_DIR" "$PROCESSING_DIR" "$DONE_DIR" "$PID_DIR" "$CANCEL_DIR" "$DB_LOCK_DIR"
acquire_worker_lock() {
if mkdir "$WORKER_LOCK_DIR" 2>/dev/null; then
printf '%s' "$$" > "$WORKER_LOCK_DIR/pid" 2>/dev/null || true
return 0
fi
local existing_pid="" lock_mtime=0 now lock_age=0
if [ -r "$WORKER_LOCK_DIR/pid" ]; then
existing_pid="$(cat "$WORKER_LOCK_DIR/pid" 2>/dev/null || true)"
fi
if [ -n "$existing_pid" ]; then
if kill -0 "$existing_pid" 2>/dev/null; then
echo "alt-mysql worker: lock held by running pid $existing_pid, exiting" >&2
return 1
fi
else
lock_mtime="$(stat -c '%Y' "$WORKER_LOCK_DIR" 2>/dev/null || stat -f '%m' "$WORKER_LOCK_DIR" 2>/dev/null || echo 0)"
now="$(date +%s)"
if [ "$lock_mtime" -gt 0 ]; then
lock_age=$((now - lock_mtime))
fi
if [ "$lock_age" -lt "$WORKER_LOCK_STALE_SECONDS" ]; then
echo "alt-mysql worker: lock present without a live pid but younger than ${WORKER_LOCK_STALE_SECONDS}s (age ${lock_age}s), exiting" >&2
return 1
fi
fi
echo "alt-mysql worker: reclaiming stale lock (pid=${existing_pid:-none}, age=${lock_age}s)" >&2
rm -rf "$WORKER_LOCK_DIR"
if mkdir "$WORKER_LOCK_DIR" 2>/dev/null; then
printf '%s' "$$" > "$WORKER_LOCK_DIR/pid" 2>/dev/null || true
return 0
fi
return 1
}
if ! acquire_worker_lock; then
exit 0
fi
cleanup() {
rm -rf "$WORKER_LOCK_DIR" 2>/dev/null || true
}
trap cleanup EXIT
db_lock_path_for_db() {
local db_name="$1"
local safe
safe="$(printf '%s' "$db_name" | tr -c 'A-Za-z0-9_.-' '_')"
printf '%s/%s.lock' "$DB_LOCK_DIR" "$safe"
}
release_db_lock_later() {
local lock_path="$1"
if [ -z "$lock_path" ]; then
return 0
fi
(
sleep 60
rm -f "$lock_path" 2>/dev/null || true
) >/dev/null 2>&1 &
}
while true; do
JOB_FILE="$(ls -1 "$JOB_DIR"/*.env 2>/dev/null | head -n 1 || true)"
if [ -z "${JOB_FILE:-}" ]; then
break
fi
BASE_NAME="$(basename "$JOB_FILE")"
RUN_FILE="$PROCESSING_DIR/$BASE_NAME"
mv "$JOB_FILE" "$RUN_FILE"
JOB_ID="${BASE_NAME%.env}"
PID_FILE="$PID_DIR/${JOB_ID}.pid"
JOB_TYPE="restore"
DB_NAME=""
DB_LOCK_FILE=""
# shellcheck disable=SC1090
source "$RUN_FILE" || true
JOB_TYPE="${JOB_TYPE:-restore}"
DB_NAME="${DB_NAME:-}"
DB_LOCK_FILE="${DB_LOCK_FILE:-}"
LOCK_PATH=""
if [ -n "$DB_LOCK_FILE" ]; then
case "$DB_LOCK_FILE" in
"$DB_LOCK_DIR"/*.lock) LOCK_PATH="$DB_LOCK_FILE" ;;
*) LOCK_PATH="" ;;
esac
fi
if [ -z "$LOCK_PATH" ] && [ -n "$DB_NAME" ]; then
LOCK_PATH="$(db_lock_path_for_db "$DB_NAME")"
fi
RUNNER="$PLUGIN_DIR/scripts/restore_job.sh"
if [ "$JOB_TYPE" = "backup" ]; then
RUNNER="$PLUGIN_DIR/scripts/backup_job.sh"
fi
set +e
"$RUNNER" "$RUN_FILE" &
RUN_PID=$!
echo "PID=${RUN_PID}" >> "$RUN_FILE"
echo "${RUN_PID}" > "$PID_FILE"
wait "$RUN_PID"
EXIT_CODE=$?
set -e
rm -f "$PID_FILE"
CANCEL_FLAG="${CANCEL_DIR}/${JOB_ID}.flag"
if [ -f "$CANCEL_FLAG" ]; then
rm -f "$CANCEL_FLAG"
mv "$RUN_FILE" "$DONE_DIR/${JOB_ID}.cancel"
elif [ "$EXIT_CODE" -eq 0 ]; then
mv "$RUN_FILE" "$DONE_DIR/${JOB_ID}.ok"
else
mv "$RUN_FILE" "$DONE_DIR/${JOB_ID}.fail"
fi
release_db_lock_later "$LOCK_PATH"
done
exit 0