Polycrate API 0.14.17¶
Release-Datum: 19. März 2026
Typ: Fix
Highlights¶
- Celery Task-Result Bloat beseitigt –
django_celery_results_taskresultwuchs auf 56 GB / 68 Mio. Rows, verursacht durch fehlendeignore_result=Trueauf 21+ Tasks. Nach diesem Release werden fast keine neuen Rows mehr in diese Tabelle geschrieben. - Reconciliation-Loop schreibt keine Results mehr –
reconcile_class()dispatchtreconcile_objectviaapply_async(ignore_result=True)statt.delay(), was den mit Abstand häufigsten Task-Result-Schreiber eliminiert. - REST API Reconcile-Endpoint ohne result.get() – polling auf
reconciliation_runningersetzt die blockierenderesult.get()-Barriere ohne Verhaltensänderung. - Stündlicher Backend-Cleanup –
celery.backend_cleanupläuft jetzt stündlich statt täglich.
Artefakte¶
Docker Image¶
Block¶
Hintergrund¶
Nach dem FTS-Release (0.14.13, 16.03.) und den anschließenden Hotfixes 0.14.15/0.14.16 waren Block I/O und Tuple I/O wieder normalisiert. Die Datenbankgröße wuchs jedoch weiterhin unkontrolliert mit ~1 GB/Tag.
Analyse ergab: django_celery_results_taskresult war mit 56 GB die bei weitem größte Tabelle — 68 Mio. Live-Rows, davon ein Großteil von Reconciliation-Tasks die im Sekundentakt laufen. Kein Task hatte ignore_result=True gesetzt, obwohl fast keiner der Tasks seinen Return-Value tatsächlich konsumiert.
Zusätzlich erzeugte CELERY_TASK_TRACK_STARTED=True einen extra DB-Write pro Task-Start, und CELERY_RESULT_EXTENDED=True speicherte Args/Kwargs in jeder Row.
Fixes¶
Fix 1: ignore_result=True auf allen Fire-and-Forget Tasks¶
Problem: Alle Tasks schrieben Results in django_celery_results_taskresult, obwohl die meisten nie ausgelesen werden.
21 Tasks in polycrate_api/tasks.py und alle Tasks in organizations/tasks.py, apm/tasks.py, backups/tasks.py, maintenances/tasks.py, workspaces/tasks.py, downtime/tasks.py erhielten ignore_result=True.
Nicht geändert (genutzter Return-Value oder explizite result.get()-Nutzung): - reconcile_object — HTMX-Endpoint liest Result für Toast-Feedback aus - reload_object — Result wird konsumiert - run_action — Result wird konsumiert
Fix 2: reconcile_class() – apply_async mit ignore_result=True¶
Problem: reconcile_class() wurde per Beat-Schedule millionenfach pro Tag ausgeführt und dispatcht für jedes Objekt einen reconcile_object-Task via .delay() — der häufigste Result-Schreiber überhaupt.
# Vorher: .delay() — speichert Result in DB
reconcile_object.delay(app_name=..., model_name=..., object_id=...)
# Nachher: apply_async mit ignore_result=True
reconcile_object.apply_async(
kwargs={'app_name': app_name, 'model_name': model_name, 'object_id': instance.id},
ignore_result=True
)
Fix 3: REST API Reconcile-Endpoint – polling statt result.get()¶
Problem: ManagedObjectViewSet.reconcile() blockierte via result.get() auf den Task, obwohl der Return-Value nicht genutzt wurde. Das erzwang Result-Speicherung in der DB.
# Vorher: result.get() — erzwingt Result-Speicherung
result = reconcile_object.apply_async(...)
result.get(timeout=10)
# Nachher: polling auf reconciliation_running — kein Result nötig
reconcile_object.apply_async(..., ignore_result=True)
time.sleep(0.3)
obj.refresh_from_db(fields=['reconciliation_running'])
deadline = time.time() + 10
while obj.reconciliation_running and time.time() < deadline:
time.sleep(0.5)
obj.refresh_from_db(fields=['reconciliation_running'])
Das HTMX-Endpoint (ReconcileView.post()) bleibt unverändert — dort wird result.get() für den Toast (Erfolg/Fehler) benötigt.
Fix 4: CELERY_TASK_TRACK_STARTED=False¶
Reduziert DB-Writes um 1 pro Task-Execution (kein STARTED-State mehr).
Fix 5: CELERY_RESULT_EXTENDED=False¶
Args und Kwargs werden nicht mehr in jeder taskresult-Row gespeichert — kleinere Rows für die Tasks die weiterhin Results schreiben.
Fix 6: Result-Expiry auf 1 Stunde¶
Fix 7: Stündlicher Backend-Cleanup¶
celery.backend_cleanup läuft jetzt stündlich (statt täglich) um abgelaufene Results zeitnah zu entfernen.
# celery.py
"celery-backend-cleanup": {
"task": "celery.backend_cleanup",
"schedule": 3600,
"options": {"expires": 3600},
},
Nach dem Deployment¶
Manuelle DB-Bereinigung (empfohlen)¶
Die bestehenden ~68 Mio. Rows in django_celery_results_taskresult werden vom Backend-Cleanup schrittweise abgebaut, aber das dauert bei 1h-Expiry und stündlichem Cleanup Tage bis Wochen. Für schnellere Entlastung empfiehlt sich ein manuelles Batch-Delete:
-- Alte Results löschen (in Batches um Lock-Contention zu vermeiden)
DO $$
DECLARE
deleted INT;
BEGIN
LOOP
DELETE FROM django_celery_results_taskresult
WHERE id IN (
SELECT id FROM django_celery_results_taskresult
WHERE date_done < NOW() - INTERVAL '1 hour'
LIMIT 10000
);
GET DIAGNOSTICS deleted = ROW_COUNT;
EXIT WHEN deleted = 0;
PERFORM pg_sleep(0.1);
END LOOP;
END $$;
-- Danach Speicher zurückgewinnen (erzeugt kurzen Access Exclusive Lock)
VACUUM FULL django_celery_results_taskresult;
Hinweis:
VACUUM FULLblockiert die Tabelle. In einem Maintenance-Fenster oder zu einem Zeitpunkt mit wenig Last durchführen.