feat(mail): ING-07 einheitliche Fehlerbehandlung & Wiederverbindung IMAP/POP3
Neues Paket mail/internal/protoguard kapselt die für IMAP- und POP3-Sessions gemeinsam benötigte Timeout- und Backoff-Logik einer einzelnen Verbindung: - Pro Protokollphase konfigurierbarer Idle-Read-Timeout (POP3: Authorization/Transaction, IMAP: NotAuthenticated/Selected), vor jedem Lesevorgang neu gesetzt. - Sich verdoppelnder Backoff bei wiederholten Anmeldefehlversuchen einer Verbindung (BackoffBase bis BackoffMax), Verbindungstrennung nach konfigurierbarer Höchstzahl statt Dauerschleife. Server.NewServer bleibt unverändert (Standardkonfiguration); NewServerWithGuardConfig erlaubt abweichende Werte. Ressourcenaufräumung bei Verbindungsabbruch war bereits durch defer conn.Close() strukturell gegeben — der Timeout sorgt dafür, dass dieser Pfad auch bei hängenden oder böswilligen Gegenstellen zuverlässig erreicht wird. Alle drei Pflichtprüfungen mit echten Nachweisen durchgeführt: Chaos-Test mit 30 hart gekappten Verbindungen während aktiver Übertragung (kein Goroutine-Leck), Timeout-Auslösung in jeder Protokollphase beider Server, steigender Backoff mit definierter Verbindungstrennung nach Höchstzahl an Fehlversuchen. go build/go vet/golangci-lint clean, gesamtes Mail-Modul (~24 Pakete) regressionsfrei getestet.
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
// Package protoguard bündelt die Fehlerbehandlungs- und
|
||||
// Wiederverbindungslogik, die IMAP- und POP3-Sessions gemeinsam
|
||||
// brauchen (ING-07): pro Protokollphase konfigurierbare Idle-Timeouts
|
||||
// und Backoff statt Dauerschleife bei wiederholten Anmeldefehlern.
|
||||
// Ressourcenaufräumung selbst passiert bereits strukturell durch
|
||||
// defer conn.Close() in den Sessions — Guard sorgt dafür, dass dieser
|
||||
// Pfad auch bei hängenden oder böswilligen Gegenstellen zuverlässig
|
||||
// erreicht wird.
|
||||
package protoguard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Phase identifiziert eine Protokollphase, für die ein eigener
|
||||
// Idle-Timeout gilt.
|
||||
type Phase string
|
||||
|
||||
// Config steuert Timeout- und Backoff-Verhalten einer Verbindung.
|
||||
type Config struct {
|
||||
// PhaseTimeout liefert den Idle-Timeout je Phase. Fehlt ein Eintrag,
|
||||
// gilt DefaultTimeout.
|
||||
PhaseTimeout map[Phase]time.Duration
|
||||
// DefaultTimeout gilt, wenn für die aktuelle Phase kein eigener Wert
|
||||
// gesetzt ist. 0 bedeutet: kein Timeout.
|
||||
DefaultTimeout time.Duration
|
||||
// MaxAuthFailures ist die Anzahl fehlgeschlagener Anmeldeversuche,
|
||||
// nach der eine Verbindung getrennt wird. 0 bedeutet: unbegrenzt
|
||||
// (kein Trennen, nur Backoff).
|
||||
MaxAuthFailures int
|
||||
// BackoffBase ist die Wartezeit vor der Antwort nach dem ersten
|
||||
// Fehlversuch, verdoppelt sich je weiterem Fehlversuch bis
|
||||
// BackoffMax.
|
||||
BackoffBase time.Duration
|
||||
BackoffMax time.Duration
|
||||
}
|
||||
|
||||
// DefaultConfig liefert praxistaugliche Werte für Produktionsbetrieb.
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
DefaultTimeout: 5 * time.Minute,
|
||||
MaxAuthFailures: 5,
|
||||
BackoffBase: 200 * time.Millisecond,
|
||||
BackoffMax: 5 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// Guard kapselt den Fehlerbehandlungszustand EINER Verbindung: aktuell
|
||||
// angewandte Phase-Timeouts und Zahl der Anmeldefehlversuche.
|
||||
type Guard struct {
|
||||
cfg Config
|
||||
authFailures int
|
||||
}
|
||||
|
||||
// New erstellt einen Guard für eine einzelne Session.
|
||||
func New(cfg Config) *Guard {
|
||||
return &Guard{cfg: cfg}
|
||||
}
|
||||
|
||||
// ApplyReadDeadline setzt die Lese-Deadline von conn passend zur
|
||||
// angegebenen Protokollphase (Akzeptanzkriterium 2).
|
||||
func (g *Guard) ApplyReadDeadline(conn net.Conn, phase Phase) error {
|
||||
d := g.cfg.DefaultTimeout
|
||||
if pd, ok := g.cfg.PhaseTimeout[phase]; ok {
|
||||
d = pd
|
||||
}
|
||||
if d <= 0 {
|
||||
return conn.SetReadDeadline(time.Time{})
|
||||
}
|
||||
return conn.SetReadDeadline(time.Now().Add(d))
|
||||
}
|
||||
|
||||
// RecordAuthFailure zählt einen fehlgeschlagenen Anmeldeversuch dieser
|
||||
// Verbindung und liefert die Backoff-Wartezeit vor der Fehlerantwort
|
||||
// sowie ob die Verbindung danach getrennt werden muss (Akzeptanzkriterium
|
||||
// 3: klar definierter Backoff statt Dauerschleife).
|
||||
func (g *Guard) RecordAuthFailure() (backoff time.Duration, disconnect bool) {
|
||||
g.authFailures++
|
||||
backoff = g.backoffFor(g.authFailures)
|
||||
disconnect = g.cfg.MaxAuthFailures > 0 && g.authFailures >= g.cfg.MaxAuthFailures
|
||||
return backoff, disconnect
|
||||
}
|
||||
|
||||
// ResetAuthFailures setzt den Fehlversuchszähler nach erfolgreicher
|
||||
// Anmeldung zurück.
|
||||
func (g *Guard) ResetAuthFailures() { g.authFailures = 0 }
|
||||
|
||||
func (g *Guard) backoffFor(failures int) time.Duration {
|
||||
if g.cfg.BackoffBase <= 0 {
|
||||
return 0
|
||||
}
|
||||
d := g.cfg.BackoffBase
|
||||
for i := 1; i < failures; i++ {
|
||||
d *= 2
|
||||
if g.cfg.BackoffMax > 0 && d >= g.cfg.BackoffMax {
|
||||
return g.cfg.BackoffMax
|
||||
}
|
||||
}
|
||||
if g.cfg.BackoffMax > 0 && d > g.cfg.BackoffMax {
|
||||
return g.cfg.BackoffMax
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// Wait wartet d, bricht aber bei ctx-Abbruch sofort ab, damit ein
|
||||
// Server-Shutdown nicht auf eine laufende Backoff-Pause warten muss.
|
||||
func (g *Guard) Wait(ctx context.Context, d time.Duration) {
|
||||
if d <= 0 {
|
||||
return
|
||||
}
|
||||
timer := time.NewTimer(d)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user