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.
124 lines
4.2 KiB
Go
124 lines
4.2 KiB
Go
package imap
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/mail/internal/loadtest"
|
|
)
|
|
|
|
// imapLoadtestConcurrency/imapLoadtestSessions — siehe pop3-Pendant für
|
|
// die Begründung der Größenordnung (Akzeptanzkriterium 1, QA-07).
|
|
const (
|
|
imapLoadtestConcurrency = 200
|
|
imapLoadtestSessions = 2000
|
|
)
|
|
|
|
// Zielwerte (Akzeptanzkriterium 3, QA-07) — festgelegt VOR dem
|
|
// entscheidenden Testlauf, siehe Prüfprotokoll QA-07 für die
|
|
// Vorab-Messwerte auf 192.168.1.131.
|
|
const (
|
|
imapTargetP95Latency = 100 * time.Millisecond
|
|
imapTargetThroughputMin = 800.0 // Sessions/Sekunde
|
|
)
|
|
|
|
// TestLoadtest_IMAPParallelSessionsMeetTargets ist die geforderte
|
|
// Pflichtprüfung 1+2 (QA-07).
|
|
func TestLoadtest_IMAPParallelSessionsMeetTargets(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(), "imap", imapLoadtestConcurrency, imapLoadtestSessions, func(ctx context.Context) error {
|
|
return runIMAPLoadSession(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
|
|
|
|
fmt.Printf("QA-07 IMAP-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, imapTargetThroughputMin)
|
|
fmt.Printf(" Latenz p50=%v p95=%v (Ziel: <= %v) max=%v\n", p50, p95, imapTargetP95Latency, maxLatency)
|
|
fmt.Printf(" Ressourcen: Heap-Delta=%.1f MiB, CPU-Zeit=%.2fs\n", heapDeltaMB, cpuUsed)
|
|
|
|
if result.Errors != 0 {
|
|
t.Fatalf("QA-07 IMAP: %d von %d sessions fehlgeschlagen", result.Errors, result.Sessions)
|
|
}
|
|
if p95 > imapTargetP95Latency {
|
|
t.Fatalf("QA-07 IMAP: p95-latenz %v überschreitet zielwert %v", p95, imapTargetP95Latency)
|
|
}
|
|
if throughput < imapTargetThroughputMin {
|
|
t.Fatalf("QA-07 IMAP: durchsatz %.1f sessions/s unter zielwert %.1f", throughput, imapTargetThroughputMin)
|
|
}
|
|
if heapDeltaMB > 100 {
|
|
t.Fatalf("QA-07 IMAP: heap-wachstum %.1f MiB deutet auf ressourcenleck hin", heapDeltaMB)
|
|
}
|
|
}
|
|
|
|
// runIMAPLoadSession führt eine vollständige, realistische IMAP-Session
|
|
// über eine ROHE TCP-Verbindung aus (keine testing.T-Hilfsfunktionen,
|
|
// die bei Fehlern t.Fatalf/goexit auslösen — in einer nebenläufigen
|
|
// Lasttest-Worker-Goroutine unzulässig und würde den Fehler stillschweigend
|
|
// verschlucken statt ihn zu zählen).
|
|
func runIMAPLoadSession(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 { // Begrüßung
|
|
return err
|
|
}
|
|
if _, err := sendTaggedRaw(conn, reader, "A1", "LOGIN alice geheim123"); err != nil {
|
|
return fmt.Errorf("LOGIN: %w", err)
|
|
}
|
|
if _, err := sendTaggedRaw(conn, reader, "A2", "SELECT INBOX"); err != nil {
|
|
return fmt.Errorf("SELECT: %w", err)
|
|
}
|
|
if _, err := sendTaggedRaw(conn, reader, "A3", "FETCH 1 (FLAGS)"); err != nil {
|
|
return fmt.Errorf("FETCH: %w", err)
|
|
}
|
|
_, err = sendTaggedRaw(conn, reader, "A4", "LOGOUT")
|
|
return err
|
|
}
|
|
|
|
// sendTaggedRaw sendet ein getaggtes Kommando und liest bis zur
|
|
// getaggten Abschlusszeile; liefert einen Fehler, wenn diese kein "OK"
|
|
// enthält.
|
|
func sendTaggedRaw(conn net.Conn, reader *bufio.Reader, tag, command string) (lines []string, err error) {
|
|
if _, err := conn.Write([]byte(tag + " " + command + "\r\n")); err != nil {
|
|
return nil, err
|
|
}
|
|
for {
|
|
line, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
line = strings.TrimRight(line, "\r\n")
|
|
lines = append(lines, line)
|
|
if strings.HasPrefix(line, tag+" ") {
|
|
if !strings.Contains(line, "OK") {
|
|
return lines, fmt.Errorf("unerwartete abschlussantwort: %q", line)
|
|
}
|
|
return lines, nil
|
|
}
|
|
}
|
|
}
|