feat(mail): QA-07 Last- & Leistungstest für IMAP/POP3/SMTP
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.
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Package loadtest implementiert die für QA-07 gemeinsam genutzte
|
||||
// Last- und Leistungsmessung: parallele Sessions ausführen, Latenzen
|
||||
// aufzeichnen, Durchsatz und Ressourcenverbrauch bestimmen. k6/vegeta
|
||||
// sind auf diesem Rechner nicht installierbar (Projektregel: keine
|
||||
// zusätzlichen Toolchains/Dienste installieren) — echter Ersatz ohne
|
||||
// externe Abhängigkeit: reale nebenläufige TCP-Sessions über die
|
||||
// Go-Standardbibliothek, gegen die echten, in dieser Sitzung gebauten
|
||||
// Protokollserver.
|
||||
package loadtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Result fasst Latenz- und Durchsatzwerte eines Lasttest-Laufs
|
||||
// zusammen (Akzeptanzkriterium 2).
|
||||
type Result struct {
|
||||
Protocol string
|
||||
Concurrency int
|
||||
Sessions int
|
||||
Errors int
|
||||
Duration time.Duration
|
||||
Latencies []time.Duration // aufsteigend sortiert
|
||||
}
|
||||
|
||||
// Throughput liefert Sessions pro Sekunde über die Gesamtlaufzeit.
|
||||
func (r Result) Throughput() float64 {
|
||||
if r.Duration <= 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(r.Sessions) / r.Duration.Seconds()
|
||||
}
|
||||
|
||||
// Percentile liefert die Latenz am p-ten Perzentil (0.0–1.0).
|
||||
func (r Result) Percentile(p float64) time.Duration {
|
||||
if len(r.Latencies) == 0 {
|
||||
return 0
|
||||
}
|
||||
idx := int(p * float64(len(r.Latencies)-1))
|
||||
if idx < 0 {
|
||||
idx = 0
|
||||
}
|
||||
if idx >= len(r.Latencies) {
|
||||
idx = len(r.Latencies) - 1
|
||||
}
|
||||
return r.Latencies[idx]
|
||||
}
|
||||
|
||||
// Max liefert die höchste gemessene Latenz.
|
||||
func (r Result) Max() time.Duration {
|
||||
if len(r.Latencies) == 0 {
|
||||
return 0
|
||||
}
|
||||
return r.Latencies[len(r.Latencies)-1]
|
||||
}
|
||||
|
||||
// Run führt totalSessions Sessions über maximal concurrency parallele
|
||||
// Worker aus (Akzeptanzkriterium 1: realistische Anzahl paralleler
|
||||
// Sessions) und misst je Session die Latenz von sessionFunc.
|
||||
func Run(ctx context.Context, protocol string, concurrency, totalSessions int, sessionFunc func(ctx context.Context) error) Result {
|
||||
var mu sync.Mutex
|
||||
latencies := make([]time.Duration, 0, totalSessions)
|
||||
errCount := 0
|
||||
|
||||
sem := make(chan struct{}, concurrency)
|
||||
var wg sync.WaitGroup
|
||||
start := time.Now()
|
||||
for i := 0; i < totalSessions; i++ {
|
||||
sem <- struct{}{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }()
|
||||
t0 := time.Now()
|
||||
err := sessionFunc(ctx)
|
||||
elapsed := time.Since(t0)
|
||||
mu.Lock()
|
||||
latencies = append(latencies, elapsed)
|
||||
if err != nil {
|
||||
errCount++
|
||||
}
|
||||
mu.Unlock()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
duration := time.Since(start)
|
||||
|
||||
sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] })
|
||||
return Result{
|
||||
Protocol: protocol,
|
||||
Concurrency: concurrency,
|
||||
Sessions: totalSessions,
|
||||
Errors: errCount,
|
||||
Duration: duration,
|
||||
Latencies: latencies,
|
||||
}
|
||||
}
|
||||
|
||||
// ResourceSample ist eine Momentaufnahme des Ressourcenverbrauchs
|
||||
// DIESES Prozesses (Akzeptanzkriterium/Pflichtprüfung 3): Heap über die
|
||||
// Go-Runtime, kumulierte CPU-Zeit über /proc/self/stat (Linux, kein
|
||||
// externes Werkzeug nötig).
|
||||
type ResourceSample struct {
|
||||
HeapAllocBytes uint64
|
||||
CPUTimeSeconds float64
|
||||
}
|
||||
|
||||
func SampleResources() ResourceSample {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
return ResourceSample{HeapAllocBytes: m.HeapAlloc, CPUTimeSeconds: readSelfCPUTimeSeconds()}
|
||||
}
|
||||
|
||||
// readSelfCPUTimeSeconds liefert die kumulierte User+System-CPU-Zeit
|
||||
// dieses Prozesses in Sekunden. Liefert 0, wenn /proc/self/stat nicht
|
||||
// lesbar ist (z. B. auf Nicht-Linux-Systemen) — Aufrufer behandeln das
|
||||
// als "keine CPU-Messung verfügbar", kein harter Fehler.
|
||||
func readSelfCPUTimeSeconds() float64 {
|
||||
data, err := os.ReadFile("/proc/self/stat")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
fields := strings.Fields(string(data))
|
||||
if len(fields) < 15 {
|
||||
return 0
|
||||
}
|
||||
utime, err1 := strconv.ParseFloat(fields[13], 64)
|
||||
stime, err2 := strconv.ParseFloat(fields[14], 64)
|
||||
if err1 != nil || err2 != nil {
|
||||
return 0
|
||||
}
|
||||
const clockTicksPerSecond = 100 // Linux-Standard-USER_HZ
|
||||
return (utime + stime) / clockTicksPerSecond
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user