// Package chaos prüft, ob Störungen ein kontrolliertes Ergebnis liefern // (SYNCOVA_IMPLEMENTATION_PLAN.md §23). // // Die Vorgabe des Plans steht in einem Satz: // // Every failure must produce a controlled result. // // Das ist keine Aufforderung, Fehler zu vermeiden — Platten laufen voll, // Datenbanken fallen aus, Prozesse werden abgeschossen. Es ist die Aufforderung, // dass jeder dieser Fälle **vorhersagbar** endet. // // Vorhersagbar heißt hier vier Dinge, und alle vier sind prüfbar: // // 1. Der Fehler wird gemeldet, nicht verschluckt. // 2. Er ist klassifiziert — sonst kann niemand entscheiden, ob eine // Wiederholung sinnvoll ist. // 3. Es bleibt kein halbes Backup zurück, das gültig aussieht. // 4. Der Zustand danach ist konsistent: Was die Anlage über sich sagt, stimmt. // // Die dritte Bedingung ist die wichtigste. Ein abgebrochener Lauf, der nichts // hinterlässt, ist ein Ärgernis. Ein abgebrochener Lauf, der ein unvollständiges // Backup als gültig hinterlässt, ist ein Datenverlust mit Zeitzünder: Er fällt // erst an dem Tag auf, an dem jemand wiederherstellen will. package chaos import ( "fmt" "strings" ) // Disruption benennt eine Störungsart. type Disruption string const ( // DisruptionProcessKill ist das harte Beenden des Prozesses. DisruptionProcessKill Disruption = "prozess_abgeschossen" // DisruptionServiceRestart ist der Neustart des Dienstes. DisruptionServiceRestart Disruption = "dienst_neugestartet" // DisruptionNetworkLoss ist der Verlust der Netzverbindung. DisruptionNetworkLoss Disruption = "netz_verloren" // DisruptionRepositoryUnavailable ist ein nicht erreichbares Repository. DisruptionRepositoryUnavailable Disruption = "repository_weg" // DisruptionDatabaseUnavailable ist eine nicht erreichbare Datenbank. DisruptionDatabaseUnavailable Disruption = "datenbank_weg" // DisruptionDiskFull ist ein volles Dateisystem. DisruptionDiskFull Disruption = "platte_voll" // DisruptionCorruptedChunk ist ein beschädigter Datenblock. DisruptionCorruptedChunk Disruption = "block_beschaedigt" // DisruptionCorruptedManifest ist ein beschädigtes Manifest. DisruptionCorruptedManifest Disruption = "manifest_beschaedigt" // DisruptionPowerInterruption ist ein simulierter Stromausfall. DisruptionPowerInterruption Disruption = "strom_weg" ) // Outcome beschreibt, wie die Anlage auf eine Störung reagiert hat. type Outcome struct { // Disruption benennt die Störung. Disruption Disruption `json:"disruption"` // Description erklärt, was genau ausgelöst wurde. Description string `json:"description"` // ErrorReported meldet, ob überhaupt ein Fehler gemeldet wurde. // // Der erste und wichtigste Punkt: Ein Fehler, der niemandem auffällt, ist // schlimmer als einer, der die Anlage anhält (verbindliche Regel 2). ErrorReported bool `json:"error_reported"` // ErrorMessage ist die gemeldete Fehlerbeschreibung. ErrorMessage string `json:"error_message,omitempty"` // FailureClass ist die Einstufung des Fehlers. // // Ohne sie kann die Ausführungsschleife nicht entscheiden, ob ein neuer // Versuch sinnvoll ist — und wiederholt entweder alles oder nichts. FailureClass string `json:"failure_class,omitempty"` // VisibleBackupLeftBehind meldet ein zurückgebliebenes, sichtbares Backup. // // Sichtbar heißt: mit gültigem Manifest, also für jede spätere // Wiederherstellung ein Kandidat. VisibleBackupLeftBehind bool `json:"visible_backup_left_behind"` // StateConsistent meldet einen widerspruchsfreien Zustand danach. StateConsistent bool `json:"state_consistent"` // Observations sind die einzelnen Beobachtungen. Observations []string `json:"observations,omitempty"` } // AddObservation vermerkt eine Beobachtung. func (outcome *Outcome) AddObservation(observationFormat string, formatArguments ...any) { outcome.Observations = append(outcome.Observations, fmt.Sprintf(observationFormat, formatArguments...)) } // IsControlled meldet ein kontrolliertes Ergebnis. // // Alle vier Bedingungen müssen zutreffen. Drei von vier genügen ausdrücklich // nicht: Ein Fehler, der gemeldet und klassifiziert wird und trotzdem ein // scheinbar gültiges Backup hinterlässt, ist der gefährlichste von allen. func (outcome *Outcome) IsControlled() bool { return outcome.ErrorReported && outcome.FailureClass != "" && !outcome.VisibleBackupLeftBehind && outcome.StateConsistent } // Verdict beschreibt das Ergebnis in Worten. func (outcome *Outcome) Verdict() string { if outcome.IsControlled() { return "kontrolliert" } problems := make([]string, 0, 4) if !outcome.ErrorReported { problems = append(problems, "der Fehler wurde nicht gemeldet") } if outcome.FailureClass == "" { problems = append(problems, "der Fehler ist nicht klassifiziert") } if outcome.VisibleBackupLeftBehind { problems = append(problems, "es blieb ein sichtbares, unvollständiges Backup zurück") } if !outcome.StateConsistent { problems = append(problems, "der Zustand danach ist widersprüchlich") } return "NICHT kontrolliert: " + strings.Join(problems, "; ") } // Report fasst mehrere Störungsläufe zusammen. type Report struct { // Outcomes sind die einzelnen Ergebnisse. Outcomes []*Outcome `json:"outcomes"` // UntestedDisruptions benennen nicht geprüfte Störungen samt Grund. // // Sie stehen gleichberechtigt neben den Ergebnissen: Ein Chaosbericht, der // verschweigt, was er nicht ausgelöst hat, liest sich vollständig und ist es // nicht. UntestedDisruptions []UntestedDisruption `json:"untested_disruptions,omitempty"` } // UntestedDisruption ist eine nicht ausgelöste Störung. type UntestedDisruption struct { // Disruption benennt die Störung. Disruption Disruption `json:"disruption"` // Reason erklärt, warum sie nicht ausgelöst wurde. Reason string `json:"reason"` } // AddUntested vermerkt eine nicht ausgelöste Störung. func (report *Report) AddUntested(disruption Disruption, reason string) { report.UntestedDisruptions = append(report.UntestedDisruptions, UntestedDisruption{Disruption: disruption, Reason: reason}) } // UncontrolledCount zählt die nicht kontrollierten Ergebnisse. func (report *Report) UncontrolledCount() int { uncontrolledCount := 0 for _, outcome := range report.Outcomes { if !outcome.IsControlled() { uncontrolledCount++ } } return uncontrolledCount }