Complete DDS training workflow and delivery package
This commit is contained in:
parent
68dd83c7c2
commit
4c4b91064f
229 changed files with 11969 additions and 1024 deletions
80
backend/scripts/backup_loop.py
Normal file
80
backend/scripts/backup_loop.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
"""Автоматическая резервная копия PostgreSQL не реже раза в сутки.
|
||||
|
||||
Процесс живёт отдельным Compose-сервисом. Он делает копию сразу, если свежей
|
||||
нет, затем ориентируется на фактическое время последнего успешного файла.
|
||||
После ошибки повторяет попытку чаще, но не удаляет существующие копии.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.admin.backup import BackupError, DIR, create, listing # noqa: E402
|
||||
from app.config import get_settings # noqa: E402
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)s backup-scheduler %(message)s",
|
||||
)
|
||||
log = logging.getLogger("backup-scheduler")
|
||||
|
||||
|
||||
def seconds_until_due(now: datetime, interval: int) -> float:
|
||||
copies = listing()
|
||||
if not copies:
|
||||
return 0.0
|
||||
age = (now - copies[0]["at"]).total_seconds()
|
||||
return max(0.0, interval - age)
|
||||
|
||||
|
||||
def prune(keep: int) -> int:
|
||||
old = listing()[keep:]
|
||||
deleted = 0
|
||||
for item in old:
|
||||
try:
|
||||
(DIR / item["name"]).unlink(missing_ok=True)
|
||||
deleted += 1
|
||||
except OSError as exc:
|
||||
# A locked/readonly old file must not take down the daily scheduler.
|
||||
log.error("не удалось удалить старую копию %s: %s", item["name"], exc)
|
||||
return deleted
|
||||
|
||||
|
||||
def run_forever() -> None:
|
||||
settings = get_settings()
|
||||
interval = max(60, settings.backup_interval_seconds)
|
||||
retry = max(10, min(settings.backup_retry_seconds, interval))
|
||||
keep = max(1, settings.backup_keep)
|
||||
log.info("запущен: интервал=%s с, хранить=%s", interval, keep)
|
||||
|
||||
while True:
|
||||
try:
|
||||
wait = seconds_until_due(datetime.now(timezone.utc), interval)
|
||||
except BackupError as exc:
|
||||
log.error("не удалось проверить резервные копии: %s; повтор через %s с", exc, retry)
|
||||
time.sleep(retry)
|
||||
continue
|
||||
if wait > 0:
|
||||
time.sleep(wait)
|
||||
continue
|
||||
try:
|
||||
made = create()
|
||||
deleted = prune(keep)
|
||||
log.info("готово: %s (%s байт), удалено старых=%s",
|
||||
made["name"], made["size_bytes"], deleted)
|
||||
except BackupError as exc:
|
||||
log.error("цикл резервного копирования не завершён: %s; повтор через %s с", exc, retry)
|
||||
time.sleep(retry)
|
||||
continue
|
||||
time.sleep(interval)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_forever()
|
||||
Loading…
Reference in a new issue