Harden runtime and add backup foundation
This commit is contained in:
@@ -24,6 +24,7 @@ class Settings(BaseSettings):
|
||||
project_registry_path: Path = Path("./config/project-registry.json")
|
||||
host: str = "127.0.0.1"
|
||||
port: int = 8100
|
||||
db_busy_timeout_ms: int = 5000
|
||||
|
||||
# Embedding
|
||||
embedding_model: str = (
|
||||
|
||||
@@ -100,9 +100,15 @@ def _column_exists(conn: sqlite3.Connection, table: str, column: str) -> bool:
|
||||
def get_connection() -> Generator[sqlite3.Connection, None, None]:
|
||||
"""Get a database connection with row factory."""
|
||||
_ensure_data_dir()
|
||||
conn = sqlite3.connect(str(_config.settings.db_path))
|
||||
conn = sqlite3.connect(
|
||||
str(_config.settings.db_path),
|
||||
timeout=_config.settings.db_busy_timeout_ms / 1000,
|
||||
)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute("PRAGMA foreign_keys = ON")
|
||||
conn.execute(f"PRAGMA busy_timeout = {_config.settings.db_busy_timeout_ms}")
|
||||
conn.execute("PRAGMA journal_mode = WAL")
|
||||
conn.execute("PRAGMA synchronous = NORMAL")
|
||||
try:
|
||||
yield conn
|
||||
conn.commit()
|
||||
|
||||
1
src/atocore/ops/__init__.py
Normal file
1
src/atocore/ops/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Operational utilities for running AtoCore safely."""
|
||||
70
src/atocore/ops/backup.py
Normal file
70
src/atocore/ops/backup.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""Create safe runtime backups for the AtoCore machine store."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
from datetime import datetime, UTC
|
||||
from pathlib import Path
|
||||
|
||||
import atocore.config as _config
|
||||
from atocore.models.database import init_db
|
||||
from atocore.observability.logger import get_logger
|
||||
|
||||
log = get_logger("backup")
|
||||
|
||||
|
||||
def create_runtime_backup(timestamp: datetime | None = None) -> dict:
|
||||
"""Create a hot backup of the SQLite DB plus registry/config metadata."""
|
||||
init_db()
|
||||
now = timestamp or datetime.now(UTC)
|
||||
stamp = now.strftime("%Y%m%dT%H%M%SZ")
|
||||
|
||||
backup_root = _config.settings.resolved_backup_dir / "snapshots" / stamp
|
||||
db_backup_dir = backup_root / "db"
|
||||
config_backup_dir = backup_root / "config"
|
||||
metadata_path = backup_root / "backup-metadata.json"
|
||||
|
||||
db_backup_dir.mkdir(parents=True, exist_ok=True)
|
||||
config_backup_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
db_snapshot_path = db_backup_dir / _config.settings.db_path.name
|
||||
_backup_sqlite_db(_config.settings.db_path, db_snapshot_path)
|
||||
|
||||
registry_snapshot = None
|
||||
registry_path = _config.settings.resolved_project_registry_path
|
||||
if registry_path.exists():
|
||||
registry_snapshot = config_backup_dir / registry_path.name
|
||||
registry_snapshot.write_text(registry_path.read_text(encoding="utf-8"), encoding="utf-8")
|
||||
|
||||
metadata = {
|
||||
"created_at": now.isoformat(),
|
||||
"backup_root": str(backup_root),
|
||||
"db_snapshot_path": str(db_snapshot_path),
|
||||
"db_size_bytes": db_snapshot_path.stat().st_size,
|
||||
"registry_snapshot_path": str(registry_snapshot) if registry_snapshot else "",
|
||||
"vector_store_note": "Chroma hot backup is not included in this script; use a cold snapshot or rebuild/export workflow.",
|
||||
}
|
||||
metadata_path.write_text(json.dumps(metadata, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
|
||||
|
||||
log.info("runtime_backup_created", backup_root=str(backup_root), db_snapshot=str(db_snapshot_path))
|
||||
return metadata
|
||||
|
||||
|
||||
def _backup_sqlite_db(source_path: Path, dest_path: Path) -> None:
|
||||
source_conn = sqlite3.connect(str(source_path))
|
||||
dest_conn = sqlite3.connect(str(dest_path))
|
||||
try:
|
||||
source_conn.backup(dest_conn)
|
||||
finally:
|
||||
dest_conn.close()
|
||||
source_conn.close()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
result = create_runtime_backup()
|
||||
print(json.dumps(result, indent=2, ensure_ascii=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
from dataclasses import asdict, dataclass
|
||||
from pathlib import Path
|
||||
|
||||
@@ -320,7 +321,15 @@ def _load_registry_payload(registry_path: Path) -> dict:
|
||||
|
||||
def _write_registry_payload(registry_path: Path, payload: dict) -> None:
|
||||
registry_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
registry_path.write_text(
|
||||
json.dumps(payload, indent=2, ensure_ascii=True) + "\n",
|
||||
rendered = json.dumps(payload, indent=2, ensure_ascii=True) + "\n"
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w",
|
||||
encoding="utf-8",
|
||||
)
|
||||
dir=registry_path.parent,
|
||||
prefix=f"{registry_path.stem}.",
|
||||
suffix=".tmp",
|
||||
delete=False,
|
||||
) as tmp_file:
|
||||
tmp_file.write(rendered)
|
||||
temp_path = Path(tmp_file.name)
|
||||
temp_path.replace(registry_path)
|
||||
|
||||
Reference in New Issue
Block a user