mirror of
https://github.com/hanzoai/kms.git
synced 2026-10-17 11:18:16 +00:00
Hanzo CI/CD / cicd (push) Failing after 54m52s
CI/CD / cicd (push) Failing after 54m52s
zip's registration is a verb method on the group, so an operation reads as the group answering it rather than a function taking the group, and a route that is not a typed operation says which method it answers with. The package-level form and the Router interface it existed to accept any target through are both gone. The toolchain and the pin move together, deliberately. Below zip v1.36.50 the JSON encoder was selected on a build tag, so a repo whose go directive rises first writes a wire that is neither the old release's nor the new one's until the pin catches up. Co-authored-by: Hanzo Dev <[email protected]>
182 lines
5.0 KiB
Go
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.Raw(http.MethodGet, "/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.Raw(http.MethodGet, "/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])
|
|
}
|