Exponential Backoff & Retry für Auth Claims Trigger¶
Übersicht¶
Die Funktion onShopUserPermissionChanged wurde mit Exponential Backoff + Retry ausgestattet, um Race Conditions bei Batch-Approvals zu verhindern.
Problem¶
Firebase Auth hat Rate Limits bei setCustomUserClaims(). Wenn ein ERP-Admin viele Shop-User gleichzeitig freigibt, feuert der Trigger für jeden customerUsers-Doc und macht parallele Auth-Claim-Updates. Dies kann zu auth/quota-exceeded Fehlern führen.
Lösung¶
1. Retry-Helper mit Exponential Backoff¶
Eine neue Hilfsfunktion retryWithExponentialBackoff() wurde implementiert:
Retry-Strategie (Equal Jitter): - Versuch 1: sofort - Versuch 2: nach 50–100ms (zufällig) - Versuch 3: nach 100–200ms (zufällig) - Versuch 4: nach 200–400ms (zufällig) - Versuch 5: nach 400–800ms (zufällig) - Max. 5 Versuche (default)
Jitter-Strategie: Equal Jitter
Um das Thundering-Herd-Problem bei parallelen Batch-Operationen zu vermeiden, wird Equal Jitter verwendet. Math.ceil(maxDelay/2 + random * maxDelay/2) stellt sicher, dass ein Mindest-Backoff von 50% gewahrt bleibt, während die zufällige Komponente die Retry-Zeitpunkte paralleler Requests auseinanderstreut.
Retryable Fehler:
- auth/quota-exceeded
- auth/internal-error
- Error-Message enthält "quota", "rate" oder "resource_exhausted" (case-insensitive)
Nicht-retryable Fehler:
- auth/user-not-found
- Alle anderen Fehler → sofortiger Abbruch ohne Retry
2. Angepasste Trigger¶
Beide Trigger verwenden jetzt Retry:
onErpUserDocumentCreated¶
await retryWithExponentialBackoff(async () => {
await admin.auth().setCustomUserClaims(uid, { appType: "erp" });
});
onShopUserPermissionChanged¶
await retryWithExponentialBackoff(async () => {
await admin.auth().setCustomUserClaims(uid, {
...existingClaims,
appType,
customerIds,
adminCustomerIds,
});
});
3. Tests¶
13 Unit-Tests validieren die Retry-Logik:
✅ Erfolgreicher erster Versuch
✅ Retry bei auth/quota-exceeded
✅ Retry bei RESOURCE_EXHAUSTED
✅ Retry bei "quota" in Error-Message
✅ Retry bei "rate" in Error-Message
✅ Aufgeben nach maxRetries
✅ Sofortiger Abbruch bei nicht-retryable Errors
✅ Korrekte Exponential Backoff Delays mit Jitter
✅ Custom maxRetries funktioniert
✅ Custom baseDelayMs funktioniert
✅ auth/internal-error wird als retryable behandelt
✅ Reguläre Errors werfen sofort
✅ Jitter-Berechnung (Delay zwischen maxDelay/2 und maxDelay)
Alle Tests bestehen: ✅
Änderungen¶
Geänderte Dateien¶
core/functions/src/triggers/auth-claims.triggers.js- Neue
retryWithExponentialBackoff()Funktion -
Beide
setCustomUserClaims()Aufrufe nutzen Retry -
core/functions/test/unit/triggers/auth-claims.test.js(neu) - 13 Unit-Tests für Retry-Logik inkl. Jitter-Validierung
- Fake timers für deterministische Tests
Vorteile¶
✅ Robustheit: Batch-Approvals schlagen nicht mehr bei Rate-Limits fehl
✅ Automatisch: Keine manuelle Intervention bei temporären Fehlern
✅ Intelligent: Nur retryable Fehler werden wiederholt
✅ Performant: Exponential Backoff mit Jitter verhindert Überlastung und Thundering Herd
✅ Testbar: Vollständige Unit-Test-Abdeckung
Monitoring¶
Die Retry-Funktion loggt jeden Retry-Versuch:
secureLogger.info("Retry mit Exponential Backoff", {
attempt: attempt + 1,
maxRetries,
delayMs,
error: err.message,
});
Monitoring in Cloud Logging:
severity = INFO
jsonPayload.message = "Retry mit Exponential Backoff"
jsonPayload.attempt = 2
jsonPayload.delayMs = 100
Best Practices¶
- Batch-Approvals: Können jetzt sicher ausgeführt werden
- Fehlerbehandlung: Nicht-retryable Errors schlagen sofort fehl (z.B. User nicht gefunden)
- Rate Limits: Exponential Backoff mit Equal Jitter verhindert API-Überlastung und Thundering Herd
- Monitoring: Überwachen Sie Retry-Logs für Capacity-Planning
Zukünftige Erweiterungen¶
Optional könnten folgende Verbesserungen erwogen werden:
- ~~Jitter: Zufälliger Offset zu Delays, um "Thundering Herd" zu vermeiden~~ ✅ Implementiert (Equal Jitter)
- Max Backoff: Cap bei z.B. 5 Sekunden für sehr viele Retries
- Konfigurierbar:
maxRetriesundbaseDelayMsüber Firestore Settings
Audit-Empfehlung¶
✅ Vollständig umgesetzt
"Exponential Backoff + Retry in onShopUserPermissionChanged einbauen" wurde implementiert und getestet.