Neues Paket mail/internal/loadtest: parallele Sessions ausführen, Latenz-Perzentile (p50/p95/max) und Durchsatz messen, Ressourcen- Stichprobe (Heap über runtime.MemStats, kumulierte CPU-Zeit über /proc/self/stat). k6/vegeta sind auf diesem Rechner nicht installierbar (Projektregel) — echter Ersatz ohne externe Abhängigkeit: reale nebenläufige TCP-Sessions über die Standardbibliothek gegen die echten, in dieser Sitzung gebauten Protokollserver, kein Mock. Je ein TestLoadtest_<Protokoll>ParallelSessionsMeetTargets in imap/, pop3/, smtp/: 200 parallele Worker, 2000 vollständige realistische Sessions je Protokoll (POP3 USER/PASS/STAT/RETR/QUIT, IMAP LOGIN/SELECT/FETCH/LOGOUT, SMTP EHLO/MAIL/RCPT/DATA/QUIT). Zielwerte für p95-Latenz und Mindestdurchsatz vor dem entscheidenden Testlauf im Code festgelegt, auf Basis einer separaten Vorab-Messung mit Sicherheitsabstand. Reale Messwerte auf 192.168.1.131: POP3 6652 Sessions/s (p95 42,9ms), IMAP 5354 Sessions/s (p95 54,9ms), SMTP 6328 Sessions/s (p95 44,0ms) — alle Zielwerte deutlich unterboten/überboten, 0 Fehler über 6000 Sessions insgesamt, Heap-Wachstum je Protokoll im niedrigen einstelligen MiB-Bereich (kein Ressourcenleck). go build/go vet/golangci-lint clean, gesamtes Mail-Modul (~30 Pakete) regressionsfrei getestet.
141 lines
4.5 KiB
Go
141 lines
4.5 KiB
Go
package pop3
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/mail/internal/loadtest"
|
|
)
|
|
|
|
// pop3LoadtestConcurrency/pop3LoadtestSessions sind die für
|
|
// Akzeptanzkriterium 1 (QA-07) simulierte "realistische" parallele
|
|
// Last: 200 gleichzeitige POP3-Clients, insgesamt 2000 vollständige
|
|
// Sessions — vergleichbar mit einem kleinen bis mittleren
|
|
// Mailserver-Betrieb (deutlich mehr als jede reale Einzelinstanz
|
|
// dieser Größenklasse gleichzeitig bedienen müsste).
|
|
const (
|
|
pop3LoadtestConcurrency = 200
|
|
pop3LoadtestSessions = 2000
|
|
)
|
|
|
|
// Zielwerte (Akzeptanzkriterium 3, ING-... QA-07): festgelegt VOR dem
|
|
// entscheidenden Testlauf, auf Basis einer separaten Vorab-Messung auf
|
|
// dem Zielsystem (192.168.1.131) mit großzügigem Sicherheitsabstand zum
|
|
// tatsächlich gemessenen Wert — siehe Prüfprotokoll QA-07 für die
|
|
// Vorab-Messwerte.
|
|
const (
|
|
pop3TargetP95Latency = 100 * time.Millisecond
|
|
pop3TargetThroughputMin = 800.0 // Sessions/Sekunde
|
|
)
|
|
|
|
// TestLoadtest_POP3ParallelSessionsMeetTargets ist die geforderte
|
|
// Pflichtprüfung 1+2 (QA-07): Lasttest-Lauf mit Ergebnisprotokoll,
|
|
// Vergleich Ist- vs. Zielwert.
|
|
func TestLoadtest_POP3ParallelSessionsMeetTargets(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("lasttest übersprungen (-short)")
|
|
}
|
|
addr, _, stop := startTestServer(t)
|
|
defer stop()
|
|
|
|
resourcesBefore := loadtest.SampleResources()
|
|
result := loadtest.Run(context.Background(), "pop3", pop3LoadtestConcurrency, pop3LoadtestSessions, func(ctx context.Context) error {
|
|
return runPOP3LoadSession(addr)
|
|
})
|
|
resourcesAfter := loadtest.SampleResources()
|
|
|
|
p50 := result.Percentile(0.50)
|
|
p95 := result.Percentile(0.95)
|
|
maxLatency := result.Max()
|
|
throughput := result.Throughput()
|
|
heapDeltaMB := float64(resourcesAfter.HeapAllocBytes-resourcesBefore.HeapAllocBytes) / (1024 * 1024)
|
|
cpuUsed := resourcesAfter.CPUTimeSeconds - resourcesBefore.CPUTimeSeconds
|
|
|
|
// Ergebnisprotokoll (Pflichtprüfung 1) — reale Messwerte, keine
|
|
// Schätzung.
|
|
fmt.Printf("QA-07 POP3-Lasttest: %d Sessions, %d parallel, Dauer %v\n", result.Sessions, result.Concurrency, result.Duration)
|
|
fmt.Printf(" Fehler: %d\n", result.Errors)
|
|
fmt.Printf(" Durchsatz: %.1f Sessions/s (Ziel: >= %.1f)\n", throughput, pop3TargetThroughputMin)
|
|
fmt.Printf(" Latenz p50=%v p95=%v (Ziel: <= %v) max=%v\n", p50, p95, pop3TargetP95Latency, maxLatency)
|
|
fmt.Printf(" Ressourcen: Heap-Delta=%.1f MiB, CPU-Zeit=%.2fs\n", heapDeltaMB, cpuUsed)
|
|
|
|
if result.Errors != 0 {
|
|
t.Fatalf("QA-07 POP3: %d von %d sessions fehlgeschlagen", result.Errors, result.Sessions)
|
|
}
|
|
if p95 > pop3TargetP95Latency {
|
|
t.Fatalf("QA-07 POP3: p95-latenz %v überschreitet zielwert %v", p95, pop3TargetP95Latency)
|
|
}
|
|
if throughput < pop3TargetThroughputMin {
|
|
t.Fatalf("QA-07 POP3: durchsatz %.1f sessions/s unter zielwert %.1f", throughput, pop3TargetThroughputMin)
|
|
}
|
|
// Akzeptanzkriterium/Pflichtprüfung 3: Ressourcenverbrauch im
|
|
// erwarteten Rahmen — 2000 kurzlebige Sessions dürfen den Heap
|
|
// nicht in Größenordnungen wachsen lassen, die auf ein Leck
|
|
// hindeuten (großzügige Grenze: 100 MiB).
|
|
if heapDeltaMB > 100 {
|
|
t.Fatalf("QA-07 POP3: heap-wachstum %.1f MiB deutet auf ressourcenleck hin", heapDeltaMB)
|
|
}
|
|
}
|
|
|
|
func runPOP3LoadSession(addr string) error {
|
|
conn, err := net.DialTimeout("tcp", addr, 3*time.Second)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = conn.Close() }()
|
|
_ = conn.SetDeadline(time.Now().Add(5 * time.Second))
|
|
reader := bufio.NewReader(conn)
|
|
|
|
if _, err := reader.ReadString('\n'); err != nil {
|
|
return err
|
|
}
|
|
if err := writeAndExpectOK(conn, reader, "USER alice"); err != nil {
|
|
return err
|
|
}
|
|
if err := writeAndExpectOK(conn, reader, "PASS geheim123"); err != nil {
|
|
return err
|
|
}
|
|
if err := writeAndExpectOK(conn, reader, "STAT"); err != nil {
|
|
return err
|
|
}
|
|
if _, err := conn.Write([]byte("RETR 1\r\n")); err != nil {
|
|
return err
|
|
}
|
|
status, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !strings.HasPrefix(status, "+OK") {
|
|
return fmt.Errorf("RETR: unerwartete antwort %q", status)
|
|
}
|
|
for {
|
|
line, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if strings.TrimRight(line, "\r\n") == "." {
|
|
break
|
|
}
|
|
}
|
|
return writeAndExpectOK(conn, reader, "QUIT")
|
|
}
|
|
|
|
func writeAndExpectOK(conn net.Conn, reader *bufio.Reader, cmd string) error {
|
|
if _, err := conn.Write([]byte(cmd + "\r\n")); err != nil {
|
|
return err
|
|
}
|
|
resp, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !strings.HasPrefix(resp, "+OK") {
|
|
return fmt.Errorf("%s: unerwartete antwort %q", cmd, resp)
|
|
}
|
|
return nil
|
|
}
|