Hard local maintenance: VACUUM + WAL checkpoint on every retention/forget execute

This commit is contained in:
retoor
2026-10-10 04:03:51 +02:00
parent abb327f6f9
commit 5309e2bbb5
7 changed files with 40 additions and 7 deletions
+8 -3
View File
@@ -774,9 +774,11 @@ def create_app(cfg: Config | None = None, allowed_hosts: set[str] | None = None,
blobs += chunk
await asyncio.sleep(0)
projects = s.repo.drop_empty_projects()
reclaimed = recovery.vacuum_index(s.db)
s.db.audit("forget", path=path, files=files, blobs=blobs)
return {"path": path, "dry_run": False, "files_removed": files,
"versions_removed": counts["versions"], "blobs_removed": blobs, "projects_removed": projects}
"versions_removed": counts["versions"], "blobs_removed": blobs, "projects_removed": projects,
"index_reclaimed": reclaimed}
# remote purge (remote data only; never touches the local index or spool)
@@ -913,9 +915,12 @@ def create_app(cfg: Config | None = None, allowed_hosts: set[str] | None = None,
return {"dry_run": True, **{k: v for k, v in plan.items() if k != "delete_ids"},
"versions_to_delete": len(plan["delete_ids"])}
deleted = await recovery.delete_versions(s.db, plan["delete_ids"])
s.db.audit("retention.run", keep_days=keep_days, deleted=deleted)
reclaimed = recovery.vacuum_index(s.db)
s.db.audit("retention.run", keep_days=keep_days, deleted=deleted,
reclaimed_bytes=reclaimed["bytes_reclaimed"])
return {"dry_run": False, "keep_days": keep_days, "candidates": plan["candidates"],
"kept_daily": plan["kept_daily"], "versions_deleted": deleted}
"kept_daily": plan["kept_daily"], "versions_deleted": deleted,
"index_reclaimed": reclaimed}
@api.post("/admin/gc", tags=["admin"])
async def run_gc(body: GcIn, s: Services = Depends(services)) -> dict[str, Any]:
+3 -2
View File
@@ -94,12 +94,13 @@ async def run_scheduled_pass(s: Any, reason: str) -> dict[str, Any]:
await dav.close()
s.last_maintenance["gc"] = now
s.last_maintenance["retention"] = now
reclaimed = recovery.vacuum_index(s.db)
s.db.audit("maintenance.pass", reason=reason, deleted=deleted,
gc=gc_result["blobs_removed"])
gc=gc_result["blobs_removed"], reclaimed_bytes=reclaimed["bytes_reclaimed"])
await refresh_usage(s)
return {"reason": reason, "keep_days": keep_days,
"versions_deleted": deleted, "kept_daily": plan["kept_daily"],
"gc": gc_result, "pressure": over_limit(s)}
"gc": gc_result, "index_reclaimed": reclaimed, "pressure": over_limit(s)}
async def maintenance_loop(s: Any) -> None:
+20 -1
View File
@@ -13,6 +13,7 @@ from __future__ import annotations
import asyncio
import json
import os
import time
from datetime import datetime, timezone
from typing import Any
@@ -164,12 +165,30 @@ async def delete_versions(db: Database, ids: list[int], chunk: int = 500) -> int
return deleted
def vacuum_index(db: Database) -> dict[str, Any]:
"""Hard reclaim: VACUUM the freelist away, checkpoint+truncate the WAL.
Deleted rows otherwise sit on freelist pages forever and the file never
shrinks. Returns byte counts so callers can report what was reclaimed.
"""
main = db.one("PRAGMA database_list")[0] if False else None
row = db.conn.execute("PRAGMA database_list").fetchone()
path = row[2] if row else ""
freelist_before = db.one("PRAGMA freelist_count")["freelist_count"]
size_before = os.path.getsize(path) if path and os.path.exists(path) else None
db.conn.execute("VACUUM")
db.conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
freelist_after = db.one("PRAGMA freelist_count")["freelist_count"]
size_after = os.path.getsize(path) if path and os.path.exists(path) else None
reclaimed = (size_before - size_after) if size_before is not None and size_after is not None else None
return {"freelist_before": freelist_before, "freelist_after": freelist_after,
"bytes_before": size_before, "bytes_after": size_after, "bytes_reclaimed": reclaimed}
def orphan_blobs(db: Database) -> list[dict[str, Any]]:
return db.all(
"SELECT sha256, stored_size FROM blobs b WHERE NOT EXISTS "
"(SELECT 1 FROM versions v WHERE v.blob_sha256 = b.sha256)")
async def collect_orphans(
dav: WebDAV | None, db: Database, blobs: BlobStore, directory: str, orphans: list[dict[str, Any]],
task: dict[str, Any] | None = None,