Files
kms/conn_memory_test.go
T
zeekayandHanzo Dev cd15134667 test: put back the process-global state Embed changes, and measure only our goroutines
Four failures, both causes the same shape: state that belongs to the whole
process being read or written as if it belonged to one test.

TestEnvRequired_* (x3) failed with 401 on requests whose tokens were fine.
Embed applies ExpectedIssuer/ExpectedAudience/JWKSURL to the process-global
authConfig — that is what Embed is FOR — and stopping the instance does not put
it back. So after TestEmbed the process no longer accepted sharedIssuer, and
verification reached for a JWKS server TestEmbed had already closed. Every later
test minting a shared-issuer token got 401, and the only reason it was those
three is that they sort after TestEmbed. They passed in isolation, which is
what made it look like their problem.

jwtTestEnv.cleanup already restores exactly this; the embedded path had been
missed. Registered before jwks.Close so it runs last (t.Cleanup is LIFO).

TestConnMemory measured runtime.NumGoroutine() — process-wide — and divided it
by the connections under test. Run alone that ratio is 1.00; run inside the
package it also counts badger compactors, httptest servers and whatever else is
still up, and it read 1.15 against a [0.95, 1.05] band. The test was failing for
goroutines it does not own. It now takes a goroutine baseline and reports the
delta, which is what the heap half of the same function has always done two
lines above.

Package green, twice.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 08:53:08 -07:00

182 lines
5.0 KiB
Go

// Copyright © 2026 Hanzo AI. MIT License.
// Per-connection memory profile for the KMS subsystem — canonical
// regression test per SCALE_STANDARD.md §3 / §8.
//
// We measure zip-on-fasthttp's per-conn overhead with KMS's health
// route registered, NOT the full Embed() server (which boots ZapDB,
// the ZAP node, and the replicator — those are out of scope for the
// per-conn budget). The fasthttp accept-loop and zip Ctx pool are
// the same regardless of which subsystem mounts on top, so this test
// catches the same regressions across services.
//
// Run with:
// go test -mod=mod -run=TestConnMemory -v -conn-count=10000
package kms_test
import (
"context"
"flag"
"fmt"
"net"
"net/http"
"runtime"
"sync"
"sync/atomic"
"testing"
"time"
luxlog "github.com/luxfi/log"
"github.com/zap-proto/zip"
)
var connCount = flag.Int("conn-count", 1000, "concurrent connections to hold")
const (
maxPerConnHeapBytes = 12 * 1024
goroutinesPerConnLow = 0.95
goroutinesPerConnHigh = 1.05
)
func TestConnMemory(t *testing.T) {
if testing.Short() {
t.Skip("skipping memory profile in -short mode")
}
n := *connCount
app := zip.New(zip.Config{
Logger: luxlog.New("test", "kms-conn-memory"),
DisableStartupMessage: true,
AppName: "kms",
})
// Mirror the canonical /v1/kms/health route shape (see mount.go).
app.Get("/v1/kms/health", func(c *zip.Ctx) error {
return c.JSON(http.StatusOK, map[string]any{
"status": "ok",
"service": "kms",
})
})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var holding atomic.Int64
app.Get("/hold", func(c *zip.Ctx) error {
holding.Add(1)
defer holding.Add(-1)
<-ctx.Done()
return nil
})
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
addr := ln.Addr().String()
go func() { _ = app.Fiber().Listener(ln) }()
defer func() { _ = app.Shutdown() }()
time.Sleep(200 * time.Millisecond)
var baseline, peak runtime.MemStats
// Goroutines get a baseline for the same reason heap does. NumGoroutine is
// PROCESS-wide, so when this runs inside the full package it also counts
// badger compactors, httptest servers and the embedded instance other tests
// left running — the ratio read 1.00 alone and 1.15 in the suite, and the
// test failed for goroutines it does not own. The delta is what "per
// connection" can mean.
baselineGoroutines := runtime.NumGoroutine()
runtime.GC()
runtime.ReadMemStats(&baseline)
req := []byte("GET /hold HTTP/1.1\r\nHost: x\r\n\r\n")
conns := make([]net.Conn, 0, n)
var connsMu sync.Mutex
var wg sync.WaitGroup
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
defer wg.Done()
c, err := net.DialTimeout("tcp", addr, 5*time.Second)
if err != nil {
return
}
if _, err := c.Write(req); err != nil {
_ = c.Close()
return
}
connsMu.Lock()
conns = append(conns, c)
connsMu.Unlock()
}()
}
wg.Wait()
defer func() {
connsMu.Lock()
for _, c := range conns {
_ = c.Close()
}
connsMu.Unlock()
}()
deadline := time.Now().Add(30 * time.Second)
for holding.Load() < int64(n) && time.Now().Before(deadline) {
time.Sleep(50 * time.Millisecond)
}
if got := holding.Load(); got < int64(n) {
t.Logf("only %d/%d conns accepted before deadline", got, n)
n = int(got)
}
if n == 0 {
t.Fatal("no conns accepted")
}
runtime.GC()
runtime.ReadMemStats(&peak)
delta := int64(peak.HeapAlloc) - int64(baseline.HeapAlloc)
perConn := float64(delta) / float64(n)
totalGoroutines := runtime.NumGoroutine()
goroutinesPerConn := float64(totalGoroutines-baselineGoroutines) / float64(n)
fmt.Printf("\n=== Per-connection memory profile (hanzoai/kms) ===\n")
fmt.Printf("conns held : %d\n", n)
fmt.Printf("baseline heap : %s\n", humanBytes(int64(baseline.HeapAlloc)))
fmt.Printf("peak heap : %s\n", humanBytes(int64(peak.HeapAlloc)))
fmt.Printf("delta : %s\n", humanBytes(delta))
fmt.Printf("per-conn heap : %.0f B (%.2f KiB)\n", perConn, perConn/1024)
fmt.Printf("goroutines total : %d (baseline %d, attributable %d)\n",
totalGoroutines, baselineGoroutines, totalGoroutines-baselineGoroutines)
fmt.Printf("goroutines / conn: %.2f\n", goroutinesPerConn)
fmt.Printf("==================================================\n\n")
if perConn > maxPerConnHeapBytes {
t.Errorf("per-conn heap %.0f B exceeds budget %d B (SCALE_STANDARD.md §3)",
perConn, maxPerConnHeapBytes)
}
if goroutinesPerConn < goroutinesPerConnLow || goroutinesPerConn > goroutinesPerConnHigh {
t.Errorf("goroutines/conn %.2f outside [%.2f, %.2f] (SCALE_STANDARD.md §3)",
goroutinesPerConn, goroutinesPerConnLow, goroutinesPerConnHigh)
}
cancel()
wg.Wait()
}
func humanBytes(n int64) string {
const unit = 1024
if n < 0 {
return "-" + humanBytes(-n)
}
if n < unit {
return fmt.Sprintf("%d B", n)
}
div, exp := int64(unit), 0
for x := n / unit; x >= unit; x /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.2f %ciB", float64(n)/float64(div), "KMGTPE"[exp])
}