Files
aml/pkg/roster/roster_test.go
zeekayandHanzo Dev 90fef65897 planes: a bound in bytes, and a tenant that only ever spends its own
Two shapes, both on the ingest path, both proven by measurement.

A bound over a COUNT of caller-sized things is not a bound. A velocity key
is not a pointer to what the caller sent: it is one ring per window, 444
buckets allocated whole on the first transaction, a measured 22.7 KB. So
MaxKeys=100000 meant 2,163 MiB against a 1 GiB pod — the honest reading was
that the process dies at about 46,000 keys, from one tenant, with ordinary
traffic. The ceiling is now in BYTES and the key count is derived from it in
one place (keyCost), so the two cannot disagree; Store.Ceiling() is an upper
bound a test WEIGHS a full store against.

One store shared by every tenant under one cap is a way for one institution
to switch another's monitoring off. Measured: a victim's 24h window went
count=1 to count=0, and a victim's model went learned=40 to learned=0, purely
from two other tenants' traffic — no error, no alert, and an aggregate that
reads zero is what a clean account also reads. Per-tenant state now lives in
pkg/roster, which ADMITS and has no removal at all: a tenant keeps what it
holds, a tenant arriving with no room is refused by name (anomaly's
ReasonCrowded) and counted, and the only key a tenant can drop is its own
least recently used. internal/source reads both packages to keep the shape
out.

Reaching a bound is allowed; reaching it quietly is not. velocity.Load grades
a tenant clear/crowded/full and counts what it dropped, anomaly.State carries
Crowded and Pressure counts it, and roster.Default is ONE answer to how many
institutions this process holds state for, so the per-tenant bounds multiply
to a figure an operator can hold against a pod.

Closing R5 (cross-tenant model eviction) and R5b (the adopted reload was read
once per TRANSACTION under thrash, now once per admission) from the review,
plus the same two shapes in pkg/velocity, which nothing had reported.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 23:29:18 -07:00

125 lines
3.7 KiB
Go

package roster
import (
"fmt"
"sync"
"testing"
"github.com/luxfi/aml/internal/source"
)
// TestAdmissionNeverTakesAnothersPlace: the property the whole package exists
// for. A roster at its ceiling refuses the arrival; it does not make room.
func TestAdmissionNeverTakesAnothersPlace(t *testing.T) {
r := New[int](2)
for i, org := range []string{"hanzo/one", "hanzo/two"} {
if v, ok := r.Hold(org, func() int { return i }); !ok || v != i {
t.Fatalf("%s was not admitted into an empty roster", org)
}
}
if _, ok := r.Hold("hanzo/three", func() int { return 3 }); ok {
t.Errorf("a third tenant was admitted into a roster of two")
}
if r.Refused() != 1 {
t.Errorf("refused = %d, want 1", r.Refused())
}
for i, org := range []string{"hanzo/one", "hanzo/two"} {
if v, ok := r.Get(org); !ok || v != i {
t.Errorf("%s lost its state to a tenant that was refused: %v %v", org, v, ok)
}
}
}
// TestMakeIsNotPaidForARefusal: building a tenant's state costs — a model is
// megabytes of trees. A roster that built one and then threw it away would let a
// refused tenant spend the memory it was refused for.
func TestMakeIsNotPaidForARefusal(t *testing.T) {
r := New[int](1)
r.Hold("hanzo/one", func() int { return 1 })
built := 0
if _, ok := r.Hold("hanzo/two", func() int { built++; return 2 }); ok {
t.Fatal("admitted past the ceiling")
}
if built != 0 {
t.Errorf("state was built %d times for a tenant that was refused", built)
}
}
// TestPutReplacesOnlyTheTenantNamed: a restore rebuilds one tenant's state. It
// must be bounded like an admission and it must not be able to reach anybody
// else's.
func TestPutReplacesOnlyTheTenantNamed(t *testing.T) {
r := New[int](2)
r.Hold("hanzo/one", func() int { return 1 })
r.Hold("hanzo/two", func() int { return 2 })
if !r.Put("hanzo/one", 11) {
t.Fatal("replacing a held tenant's own state was refused")
}
if v, _ := r.Get("hanzo/one"); v != 11 {
t.Errorf("hanzo/one = %d after its own state was replaced, want 11", v)
}
if v, _ := r.Get("hanzo/two"); v != 2 {
t.Errorf("hanzo/two = %d after another tenant was replaced, want 2", v)
}
if r.Put("hanzo/three", 3) {
t.Error("a new tenant was installed past the ceiling")
}
}
// TestConcurrentArrivalsAdmitOnce: two first transactions from one tenant arrive
// together. One state, one admission, and the ceiling still holds.
func TestConcurrentArrivalsAdmitOnce(t *testing.T) {
r := New[*int](4)
var wg sync.WaitGroup
seen := make([]*int, 32)
for i := range seen {
wg.Add(1)
go func() {
defer wg.Done()
v, _ := r.Hold("hanzo/acme", func() *int { n := 0; return &n })
seen[i] = v
}()
}
wg.Wait()
for i, v := range seen {
if v != seen[0] {
t.Fatalf("goroutine %d got a different state for one tenant", i)
}
}
if r.Held() != 1 {
t.Errorf("held = %d after 32 concurrent arrivals of one tenant", r.Held())
}
}
// TestEachSeesEveryTenant: the state views report what the process is holding
// and for whom, so a refused or absent tenant is visible rather than inferred.
func TestEachSeesEveryTenant(t *testing.T) {
r := New[int](8)
for i := range 5 {
r.Hold(fmt.Sprintf("hanzo/%d", i), func() int { return i })
}
n := 0
r.Each(func(string, int) bool { n++; return true })
if n != 5 {
t.Errorf("Each visited %d of 5", n)
}
n = 0
r.Each(func(string, int) bool { n++; return false })
if n != 1 {
t.Errorf("Each visited %d after the visitor stopped, want 1", n)
}
}
// TestNothingIsRemoved reads this package's own source.
//
// Admission-never-eviction is a property of the operations that EXIST. A Drop
// added here for a good reason is a Drop a cap can be built on, and the shape
// this package was written to make unreachable is reachable again.
func TestNothingIsRemoved(t *testing.T) {
source.NoRemoval(t, "roster.go")
}