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.
129 lines
4.1 KiB
Go
129 lines
4.1 KiB
Go
package smtp
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/mail/internal/loadtest"
|
|
)
|
|
|
|
// smtpLoadtestConcurrency/smtpLoadtestSessions — siehe pop3-Pendant für
|
|
// die Begründung der Größenordnung (Akzeptanzkriterium 1, QA-07).
|
|
const (
|
|
smtpLoadtestConcurrency = 200
|
|
smtpLoadtestSessions = 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 (
|
|
smtpTargetP95Latency = 100 * time.Millisecond
|
|
smtpTargetThroughputMin = 500.0 // Sessions/Sekunde
|
|
)
|
|
|
|
// TestLoadtest_SMTPParallelSessionsMeetTargets ist die geforderte
|
|
// Pflichtprüfung 1+2 (QA-07).
|
|
func TestLoadtest_SMTPParallelSessionsMeetTargets(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("lasttest übersprungen (-short)")
|
|
}
|
|
sink := &fakeSink{}
|
|
addr, stop := startTestServer(t, sink, defaultMaxMessageBytes)
|
|
defer stop()
|
|
|
|
resourcesBefore := loadtest.SampleResources()
|
|
result := loadtest.Run(context.Background(), "smtp", smtpLoadtestConcurrency, smtpLoadtestSessions, func(ctx context.Context) error {
|
|
return runSMTPLoadSession(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 SMTP-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, smtpTargetThroughputMin)
|
|
fmt.Printf(" Latenz p50=%v p95=%v (Ziel: <= %v) max=%v\n", p50, p95, smtpTargetP95Latency, maxLatency)
|
|
fmt.Printf(" Ressourcen: Heap-Delta=%.1f MiB, CPU-Zeit=%.2fs\n", heapDeltaMB, cpuUsed)
|
|
fmt.Printf(" Angenommene Nachrichten (Sink): %d\n", sink.count())
|
|
|
|
if result.Errors != 0 {
|
|
t.Fatalf("QA-07 SMTP: %d von %d sessions fehlgeschlagen", result.Errors, result.Sessions)
|
|
}
|
|
if sink.count() != smtpLoadtestSessions {
|
|
t.Fatalf("QA-07 SMTP: erwartete %d angenommene nachrichten, habe %d", smtpLoadtestSessions, sink.count())
|
|
}
|
|
if p95 > smtpTargetP95Latency {
|
|
t.Fatalf("QA-07 SMTP: p95-latenz %v überschreitet zielwert %v", p95, smtpTargetP95Latency)
|
|
}
|
|
if throughput < smtpTargetThroughputMin {
|
|
t.Fatalf("QA-07 SMTP: durchsatz %.1f sessions/s unter zielwert %.1f", throughput, smtpTargetThroughputMin)
|
|
}
|
|
if heapDeltaMB > 100 {
|
|
t.Fatalf("QA-07 SMTP: heap-wachstum %.1f MiB deutet auf ressourcenleck hin", heapDeltaMB)
|
|
}
|
|
}
|
|
|
|
func runSMTPLoadSession(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 { // 220-Begrüßung
|
|
return err
|
|
}
|
|
if _, err := conn.Write([]byte("EHLO client.example.com\r\n")); err != nil {
|
|
return err
|
|
}
|
|
for {
|
|
line, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if strings.HasPrefix(line, "250 ") {
|
|
break
|
|
}
|
|
}
|
|
if err := sendAndExpectCode(conn, reader, "MAIL FROM:<a@example.com>", "250"); err != nil {
|
|
return err
|
|
}
|
|
if err := sendAndExpectCode(conn, reader, "RCPT TO:<b@example.com>", "250"); err != nil {
|
|
return err
|
|
}
|
|
if err := sendAndExpectCode(conn, reader, "DATA", "354"); err != nil {
|
|
return err
|
|
}
|
|
if err := sendAndExpectCode(conn, reader, "Subject: last\r\n\r\nHallo\r\n.", "250"); err != nil {
|
|
return err
|
|
}
|
|
return sendAndExpectCode(conn, reader, "QUIT", "221")
|
|
}
|
|
|
|
func sendAndExpectCode(conn net.Conn, reader *bufio.Reader, cmd, wantCode 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 code(resp) != wantCode {
|
|
return fmt.Errorf("%q: erwartete %s, habe %q", cmd, wantCode, resp)
|
|
}
|
|
return nil
|
|
}
|