Complete DDS training workflow and delivery package

This commit is contained in:
andreysk0304 2026-09-24 01:10:49 +03:00
commit 4c4b91064f
229 changed files with 11969 additions and 1024 deletions

View 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()