Zum Inhalt

Polycrate API 0.14.17

Release-Datum: 19. März 2026
Typ: Fix

Highlights

  • Celery Task-Result Bloat beseitigtdjango_celery_results_taskresult wuchs auf 56 GB / 68 Mio. Rows, verursacht durch fehlende ignore_result=True auf 21+ Tasks. Nach diesem Release werden fast keine neuen Rows mehr in diese Tabelle geschrieben.
  • Reconciliation-Loop schreibt keine Results mehrreconcile_class() dispatcht reconcile_object via apply_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_running ersetzt die blockierende result.get()-Barriere ohne Verhaltensänderung.
  • Stündlicher Backend-Cleanupcelery.backend_cleanup läuft jetzt stündlich statt täglich.

Artefakte

Docker Image

docker pull cargo.ayedo.cloud/polycrate/polycrate-api:0.14.17

Block

polycrate pull cargo.ayedo.cloud/ayedo/k8s/polycrate-api:0.9.14
polycrate run polycrate-api install

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

CELERY_RESULT_EXPIRES = 3600       # 1h statt 24h
CELERY_TASK_RESULT_EXPIRES = 3600  # 1h statt 24h

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 FULL blockiert die Tabelle. In einem Maintenance-Fenster oder zu einem Zeitpunkt mit wenig Last durchführen.