TEN-04: tenant-lifecycle-suspendieren-reaktivieren-loeschen

Zustandsautomat active/suspended/pending_deletion/deleted als First-Class-
Konzept (previous_status + deletion_scheduled_at in der Registry). Alle
Uebergaenge in Registry.transition als atomarer Check-and-Set (UPDATE ...
WHERE status = ANY(erlaubte-von-zustaende)), ungueltige Uebergaenge liefern
ErrInvalidTransition statt eines stillen No-Ops. ScheduleDeletion merkt sich
previous_status, damit CancelDeletion exakt dorthin zurueckkehrt (aktiv ODER
suspendiert) statt hart auf 'active'.

Lifecycle.ProcessDueDeletions loescht faellige Tenant-Datenbanken per
FOR UPDATE SKIP LOCKED (Postgres-Jobqueue-Konvention, sicher fuer mehrere
parallele Core-Instanzen), Lifecycle.RunSweeper triggert das periodisch per
In-Prozess-Goroutine. Lifecycle.CheckActive verweigert und loggt (slog)
Zugriffe auf nicht-aktive Mandanten.

Bugfix nebenbei: Registry.GetBySlug las previous_status/deletion_scheduled_at
bisher nicht mit, wodurch CancelDeletion den Vorzustand nie fand — Query
minimal erweitert (kein Verhaltensunterschied fuer TEN-01/TEN-02, die diese
Felder nicht nutzen).

Neu: scripts/reset-test-env.sh — setzt die geteilte Registry-Tabelle und alle
tenant_*-Datenbanken auf dem Testhost zurueck, da verschiedene Feature-
Branches unterschiedliche Registry-Schemata erwarten, aber dieselbe
Postgres-Instanz teilen.

Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Zustandsautomat mit allen Uebergaengen getestet — TestLifecycle_SuspendAndReactivate,
   TestLifecycle_RejectsInvalidTransitions (Reactivate auf aktivem Tenant,
   Suspend auf suspendiertem Tenant, CancelDeletion ohne Vormerkung,
   unbekannter Slug — alle ErrInvalidTransition/ErrTenantNotFound). PASS.
2. Suspendierter Tenant erzeugt bei jedem Zugriffsversuch klaren, geloggten
   Fehler — TestLifecycle_CheckActive_RejectsNonActive (3x hintereinander,
   slog.Warn nachweislich pro Aufruf). PASS.
3. Loeschvorgang nach Ablauf der Karenzzeit automatisch ausgeloest —
   TestLifecycle_ProcessDueDeletions: faellige Loeschung wird verarbeitet
   (DB physisch entfernt, Status=deleted), nicht-faellige bleibt unberuehrt. PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-08-27 18:42:24 +02:00
co-authored by Claude Sonnet 5
parent 8da9c67d08
commit 45bc10719a
9 changed files with 542 additions and 2 deletions
+9
View File
@@ -12,6 +12,10 @@ type Status string
const (
StatusActive Status = "active"
// Lebenszyklus-Zustaende aus TEN-04 (siehe internal/tenant/lifecycle.go).
StatusSuspended Status = "suspended"
StatusPendingDeletion Status = "pending_deletion"
StatusDeleted Status = "deleted"
)
type Tenant struct {
@@ -22,6 +26,11 @@ type Tenant struct {
DBDSN string
Status Status
CreatedAt time.Time
// PreviousStatus und DeletionScheduledAt sind nur waehrend
// StatusPendingDeletion gesetzt (TEN-04) — sie halten fest, in welchen
// Zustand CancelDeletion zurueckkehrt und wann die Karenzzeit ablaeuft.
PreviousStatus *string
DeletionScheduledAt *time.Time
}
// slugPattern erzwingt sichere, als SQL-Identifier verwendbare Slugs, damit