diff --git a/.env.example b/.env.example index 85ff94c..6fc5da4 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,9 @@ CRONTAB="00 06 * * *" DELETE_AFTER=10 KEEP_LAST=10 TZ=Asia/Novosibirsk +SQLITE_TIMEOUT=60000 +BACKUP_RETRIES=5 +BACKUP_RETRY_DELAY=30 MINIO_PATH=myminio://sqlite/master/ MINIO_ACCOUNT_ID=account MINIO_APPLICATION_KEY=key diff --git a/README.md b/README.md index 18f0ac2..922625f 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ services: - DELETE_AFTER - KEEP_LAST - TZ + - SQLITE_TIMEOUT + - BACKUP_RETRIES + - BACKUP_RETRY_DELAY - MINIO_PATH - MINIO_ACCOUNT_ID - MINIO_APPLICATION_KEY @@ -31,6 +34,9 @@ CRONTAB="00 06 * * *" DELETE_AFTER=10 KEEP_LAST=10 TZ=Asia/Novosibirsk +SQLITE_TIMEOUT=60000 +BACKUP_RETRIES=5 +BACKUP_RETRY_DELAY=30 MINIO_PATH=myminio://sqlite/master/ MINIO_ACCOUNT_ID=account MINIO_APPLICATION_KEY=key @@ -58,6 +64,22 @@ HEALTHCHECK_UUID=00000000-0000-0000-0000-000000000000 `KEEP_LAST` по умолчанию `10`, если переменная не задана. +## Заблокированная база + +Если базу пишет живой процесс (grafana, или не добитый старый под), `sqlite3 .backup` падает с `Error: database is locked`. + +Бекап это переживает: соединение открывается с `.timeout SQLITE_TIMEOUT` (мс) — sqlite ждёт освобождения блокировки вместо мгновенной ошибки. Если за это время писатель не отпустил базу, попытка повторяется `BACKUP_RETRIES` раз с паузой `BACKUP_RETRY_DELAY` секунд. + +| Переменная | Default | Что делает | +|---|---|---| +| `SQLITE_TIMEOUT` | `60000` | сколько мс ждать блокировку в рамках одной попытки | +| `BACKUP_RETRIES` | `5` | сколько попыток снять дамп | +| `BACKUP_RETRY_DELAY` | `30` | пауза между попытками, сек | + +Максимальное время ожидания с дефолтами: `60s * 5 + 30s * 4 = 7 минут`. После этого — `exit 1` и пинг `/fail`. + +Если база залочена *постоянно* — это не про бекап, а про то, что старый под не умер. Ретраи только прикрывают короткие окна. + ## Healthchecks Если `HEALTHCHECK_URL` пуст — пинги не отправляются, бекап работает как обычно. diff --git a/VERSION b/VERSION index cb174d5..d2d61a7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.1 \ No newline at end of file +1.2.2 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 9d8e058..960b97e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,9 @@ services: - DELETE_AFTER - KEEP_LAST - TZ + - SQLITE_TIMEOUT + - BACKUP_RETRIES + - BACKUP_RETRY_DELAY - MINIO_PATH - MINIO_ACCOUNT_ID - MINIO_APPLICATION_KEY diff --git a/scripts/backup.sh b/scripts/backup.sh index 10c9a81..a82da19 100644 --- a/scripts/backup.sh +++ b/scripts/backup.sh @@ -27,7 +27,7 @@ hc_ping() { || log "WARN: healthcheck ping failed: ${url}" } -cleanup() { rm -f "$DUMP" "$ARCHIVE"; } +cleanup() { rm -f "$DUMP" "${DUMP}-journal" "${DUMP}-wal" "${DUMP}-shm" "$ARCHIVE"; } fail() { log "ERROR: $*" @@ -43,10 +43,34 @@ fail() { hc_ping start log "backup started: $DB_FILE -> ${DEST}${BACKUP_FILE}" -# 1. Consistent snapshot -rm -f "$DUMP" -sqlite3 "$DB_FILE" ".backup '$DUMP'" || fail "sqlite3 .backup failed" -[ -s "$DUMP" ] || fail "dump is empty" +# 1. Consistent snapshot. +# A live writer (grafana, or a previous pod that has not died yet) holds the +# write lock, so `.backup` gets "database is locked". `.timeout` makes sqlite +# wait instead of failing instantly; the loop covers longer write bursts. +SQLITE_TIMEOUT="${SQLITE_TIMEOUT:-60000}" +BACKUP_RETRIES="${BACKUP_RETRIES:-5}" +BACKUP_RETRY_DELAY="${BACKUP_RETRY_DELAY:-30}" + +ATTEMPT=1 +while : ; do + rm -f "$DUMP" "${DUMP}-journal" "${DUMP}-wal" "${DUMP}-shm" + ERR=$(sqlite3 -cmd ".timeout $SQLITE_TIMEOUT" "$DB_FILE" ".backup '$DUMP'" 2>&1) + RC=$? + + if [ "$RC" -eq 0 ] && [ -s "$DUMP" ]; then + break + fi + + [ -n "$ERR" ] || ERR="dump is empty" + + if [ "$ATTEMPT" -ge "$BACKUP_RETRIES" ]; then + fail "sqlite3 .backup failed after ${ATTEMPT} attempts: $ERR" + fi + + log "WARN: .backup attempt ${ATTEMPT}/${BACKUP_RETRIES} failed: ${ERR}; retrying in ${BACKUP_RETRY_DELAY}s" + sleep "$BACKUP_RETRY_DELAY" + ATTEMPT=$((ATTEMPT + 1)) +done # 2. Verify the snapshot before shipping it anywhere INTEGRITY=$(sqlite3 "$DUMP" "PRAGMA integrity_check;" 2>&1) \