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
|
|
@ -17,6 +17,7 @@ from artifacts.models import ApiKey, Artifact
|
|||
User = get_user_model()
|
||||
|
||||
|
||||
@override_settings(AGENTFEED_ASYNC_PURGE=False) # DELETE triggers purge; run it inline in tests
|
||||
class ArtifactAPITestCase(APITestCase):
|
||||
"""Exercises the ingest/list/detail API against a real (temp) filesystem
|
||||
and a real (test) database, the way an agent posting over HTTP would."""
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import shutil
|
||||
import tempfile
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
|
|
@ -8,11 +9,13 @@ from django.urls import reverse
|
|||
from django.utils import timezone
|
||||
|
||||
from artifacts.models import Artifact
|
||||
from artifacts.storage import user_usage_bytes
|
||||
from artifacts.storage import purge_expired_trash, user_usage_bytes
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
# Run the purge inline (no background thread) so its effects are deterministic.
|
||||
@override_settings(AGENTFEED_ASYNC_PURGE=False)
|
||||
class TrashKeepViewTestCase(TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
|
|
@ -105,3 +108,23 @@ class TrashKeepViewTestCase(TestCase):
|
|||
page = self.client.get("/?folder=keep").content.decode()
|
||||
self.assertIn("KeptOne", page)
|
||||
self.assertNotIn("NormalOne", page)
|
||||
|
||||
def test_purge_expired_trash_removes_old_only(self):
|
||||
old = self.make()
|
||||
recent = self.make()
|
||||
Artifact.objects.filter(pk=old.id).update(deleted_at=timezone.now() - timedelta(days=4))
|
||||
Artifact.objects.filter(pk=recent.id).update(deleted_at=timezone.now())
|
||||
purged = purge_expired_trash()
|
||||
self.assertEqual(purged, 1)
|
||||
self.assertFalse(Artifact.objects.filter(pk=old.id).exists())
|
||||
self.assertTrue(Artifact.objects.filter(pk=recent.id).exists())
|
||||
|
||||
def test_delete_triggers_expired_purge(self):
|
||||
old = self.make()
|
||||
Artifact.objects.filter(pk=old.id).update(deleted_at=timezone.now() - timedelta(days=4))
|
||||
fresh = self.make()
|
||||
# Deleting fresh runs the purge (inline here) -> the expired one goes,
|
||||
# the freshly-trashed one stays.
|
||||
self.client.post(reverse("artifact-delete", args=[fresh.id]))
|
||||
self.assertFalse(Artifact.objects.filter(pk=old.id).exists())
|
||||
self.assertTrue(Artifact.objects.filter(pk=fresh.id, deleted_at__isnull=False).exists())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue