v0.5.1: automatic Trash purge (cron-less background thread)
Trashed artifacts past a ~3-day retention window are now purged automatically. - storage.purge_expired_trash(): removes trash older than TRASH_RETENTION (all users), tolerant of already-gone rows (concurrent-safe). - spawn_trash_purge(): runs it on a daemon thread (manages/closes its own DB connection), triggered by each delete so it's cron-less and off the request thread. Inline fallback when AGENTFEED_ASYNC_PURGE is off (tests). - Wired into both delete paths (API + session). Manual Empty Trash unchanged. - Tests: purge removes expired-only; delete triggers the purge. 152 passing; live delete verified with the thread enabled (204, server healthy). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
4254164bef
commit
57ad58ff2d
8 changed files with 87 additions and 4 deletions
|
|
@ -7,9 +7,11 @@ SUM rather than a stat of every file.
|
|||
"""
|
||||
|
||||
import shutil
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
from django.db.models import F, Sum
|
||||
from django.utils import timezone
|
||||
|
||||
|
|
@ -154,6 +156,45 @@ def purge_trashed(user):
|
|||
return len(trashed)
|
||||
|
||||
|
||||
TRASH_RETENTION = timedelta(days=3)
|
||||
|
||||
|
||||
def purge_expired_trash(now=None):
|
||||
"""Permanently remove trashed artifacts older than the retention window
|
||||
(all users). Tolerant of already-gone rows so concurrent purges are safe.
|
||||
Returns the number purged."""
|
||||
now = now or timezone.now()
|
||||
cutoff = now - TRASH_RETENTION
|
||||
expired = list(Artifact.objects.filter(deleted_at__isnull=False, deleted_at__lt=cutoff))
|
||||
purged = 0
|
||||
for artifact in expired:
|
||||
try:
|
||||
delete_artifact_files(artifact)
|
||||
artifact.delete()
|
||||
purged += 1
|
||||
except Exception:
|
||||
# Another concurrent purge got there first, or a file's already
|
||||
# gone — a purge error must never surface to anyone.
|
||||
pass
|
||||
return purged
|
||||
|
||||
|
||||
def spawn_trash_purge():
|
||||
"""Run purge_expired_trash off the request thread (cron-less: triggered by
|
||||
deletes). Falls back to inline when async is disabled (tests)."""
|
||||
if not getattr(settings, "AGENTFEED_ASYNC_PURGE", True):
|
||||
purge_expired_trash()
|
||||
return
|
||||
|
||||
def _run():
|
||||
try:
|
||||
purge_expired_trash()
|
||||
finally:
|
||||
connection.close() # threads get their own connection; don't leak it
|
||||
|
||||
threading.Thread(target=_run, daemon=True).start()
|
||||
|
||||
|
||||
ASSET_ORPHAN_GRACE = timedelta(hours=24)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue