package spindle
import (
"testing"
"time"
)
type slotClock struct{ at time.Time }
func (c *slotClock) now() time.Time { return c.at }
func newSlots() (*notifySlots, *slotClock) {
clock := &slotClock{at: time.Unix(1000, 0)}
return ¬ifySlots{now: clock.now}, clock
}
func shared(pool int) slotLimits { return slotLimits{pool: pool, reserve: slotReserve(pool)} }
func take(n *notifySlots, host string, lim slotLimits, count int) (got int) {
for range count {
if n.acquire(host, lim) {
got++
}
}
return got
}
func fill(t *testing.T, n *notifySlots, lim slotLimits) {
t.Helper()
if got := take(n, "hog.test", lim, lim.pool); got != lim.pool-lim.reserve {
t.Fatalf("setup: the hog got %d of %d, want all but the reserve %d", got, lim.pool, lim.reserve)
}
if got := take(n, "second.test", lim, lim.reserve); got != lim.reserve {
t.Fatalf("setup: the second knot got %d of the reserve of %d", got, lim.reserve)
}
}
func TestNotifySlotsAnIdlePoolKeepsAReserveForANewcomer(t *testing.T) {
for pool, reserve := range map[int]int{64: 8, 16: 2, 8: 2, 3: 2} {
n, _ := newSlots()
lim := shared(pool)
if lim.reserve != reserve {
t.Fatalf("reserve of a pool of %d = %d, want %d", pool, lim.reserve, reserve)
}
got := take(n, "hog.test", lim, pool)
if want := max(1, pool-reserve); got != want {
t.Errorf("pool of %d: the hog got %d, want %d", pool, got, want)
}
if !n.acquire("newcomer.test", lim) {
t.Errorf("pool of %d: a newcomer found no slot on an idle pool", pool)
}
}
}
func TestNotifySlotsKnotsThatWaitGetAnEqualShare(t *testing.T) {
n, _ := newSlots()
lim := shared(16)
fill(t, n, lim)
take(n, "b.test", lim, 1)
take(n, "c.test", lim, 1)
for range 4 {
n.release("hog.test")
}
if n.acquire("hog.test", lim) {
t.Fatal("the hog took a freed slot back while two knots waited")
}
for _, host := range []string{"b.test", "c.test", "b.test", "c.test"} {
if !n.acquire(host, lim) {
t.Fatalf("%s was refused a slot inside its share", host)
}
}
if n.held["b.test"] != 2 || n.held["c.test"] != 2 {
t.Fatalf("held = %v, want the freed slots split between the knots that waited", n.held)
}
}
func TestNotifySlotsAHostileKnotCantGrowPastItsShareWhileAnotherHasWork(t *testing.T) {
n, _ := newSlots()
lim := shared(16)
fill(t, n, lim)
if n.acquire("honest.test", lim) {
t.Fatal("the pool was full, so the honest knot should wait")
}
n.release("hog.test")
if n.acquire("hog.test", lim) {
t.Fatalf("the hostile knot took a freed slot back while holding %d and another knot waited", n.held["hog.test"])
}
if !n.acquire("honest.test", lim) {
t.Fatal("the slot the hostile knot let go of went to nobody")
}
}
func TestNotifySlotsAKnotThatStopsWaitingStopsCounting(t *testing.T) {
n, clock := newSlots()
lim := shared(8)
fill(t, n, lim)
n.acquire("b.test", lim)
clock.at = clock.at.Add(2 * notifyWaitWindow)
n.release("second.test")
n.release("second.test")
n.release("hog.test")
n.release("hog.test")
if got := take(n, "hog.test", lim, 2); got != 2 {
t.Fatalf("the hog got %d slots after b stopped waiting, want both", got)
}
}
func TestNotifySlotsAKnotBackingOffStillLimitsTheHog(t *testing.T) {
n, clock := newSlots()
lim := shared(16)
fill(t, n, lim)
if n.acquire("legit.test", lim) {
t.Fatal("the pool was full, so the legit knot should wait")
}
clock.at = clock.at.Add(30 * time.Second)
for range 6 {
n.release("hog.test")
}
if n.acquire("hog.test", lim) {
t.Fatalf("the hog took a slot back at %d held while the legit knot was only backing off", n.held["hog.test"])
}
clock.at = clock.at.Add(30 * time.Second)
if !n.acquire("legit.test", lim) {
t.Fatal("the legit knot found no slot when its retry came")
}
}
func TestNotifySlotsAPriorityKnotGetsInWhileAHostileKnotPinsThePool(t *testing.T) {
n, _ := newSlots()
lim := shared(16)
lim.dedicated = 4
lim.priority = priorityKnotHosts([]string{"prio.test"})
fill(t, n, lim)
if got := take(n, "prio.test", lim, 4); got != 4 {
t.Fatalf("the priority knot got %d of its 4 dedicated slots with the shared pool full", got)
}
if n.acquire("prio.test", lim) {
t.Fatal("a fifth slot came from a full shared pool")
}
if n.prioTotal != 4 || n.total != 16 {
t.Fatalf("dedicated %d, shared %d, want 4 and 16", n.prioTotal, n.total)
}
}
func TestNotifySlotsANonPriorityKnotCantUseTheDedicatedSlots(t *testing.T) {
n, _ := newSlots()
lim := shared(16)
lim.dedicated = 4
lim.priority = priorityKnotHosts([]string{"prio.test"})
fill(t, n, lim)
if n.acquire("plain.test", lim) {
t.Fatal("a knot that isn't priority took a slot while the shared pool was full")
}
if n.prioTotal != 0 {
t.Fatalf("%d dedicated slots in use, want none", n.prioTotal)
}
}
func TestNotifySlotsAPriorityKnotFallsBackToTheSharedPoolUnderTheNormalShare(t *testing.T) {
n, _ := newSlots()
lim := shared(16)
lim.dedicated = 2
lim.priority = priorityKnotHosts([]string{"prio.test"})
if got := take(n, "prio.test", lim, 20); got != 2+14 {
t.Fatalf("the priority knot got %d, want its 2 dedicated and all but the reserve of the shared pool", got)
}
n.release("prio.test")
if n.held["prio.test"] != 13 || n.prioTotal != 2 {
t.Fatalf("shared held %d, dedicated %d after one release, want 13 and 2", n.held["prio.test"], n.prioTotal)
}
}
func TestPriorityKnotsMatchByHostOrDid(t *testing.T) {
hosts := priorityKnotHosts([]string{"Knot1.Example.com", "did:web:knot2.example.com", " did:web:Knot3.Example.com%3A8443 ", ""})
for _, host := range []string{"knot1.example.com", "KNOT1.example.com.", "knot2.example.com", "Knot3.example.com:8443"} {
if !hosts[canonicalKnotHost(host)] {
t.Errorf("%s isn't a priority knot, want it matched by host and by did", host)
}
}
if hosts[canonicalKnotHost("knot3.example.com")] || hosts[canonicalKnotHost("other.example.com")] || len(hosts) != 3 {
t.Fatalf("hosts = %v, want three, and the port to count", hosts)
}
}
func TestNotifySlotsAreSizedFromTheConfig(t *testing.T) {
h := newHookEnv(t)
if got := h.s.notifySlotCount(); got != defaultNotifySlots {
t.Fatalf("pool = %d with nothing configured, want %d", got, defaultNotifySlots)
}
h.s.cfg.Server.NotifySlots = 5
if got := h.s.notifySlotCount(); got != 5 {
t.Fatalf("pool = %d, want the configured 5", got)
}
var release []func()
for _, host := range []string{"a.test", "b.test", "c.test", "d.test", "e.test"} {
r, ok := h.s.acquireNotifySlot(host)
if !ok {
t.Fatalf("slot refused for %s below the configured pool", host)
}
release = append(release, r)
}
if _, ok := h.s.acquireNotifySlot("f.test"); ok {
t.Fatal("a sixth slot was granted from a pool of 5")
}
for _, r := range release {
r()
}
if got := h.s.notifySlots.inflight(); got != 0 {
t.Fatalf("%d slots held after releasing all", got)
}
}
func TestPriorityNotifySlotsAreSizedFromTheConfig(t *testing.T) {
h := newHookEnv(t)
h.s.cfg.Server.PriorityKnots = []string{"did:web:Prio.Test"}
lim := h.s.notifyLimits()
if lim.dedicated != defaultNotifySlots/4 || !lim.priority["prio.test"] {
t.Fatalf("limits = %+v, want a quarter of the pool dedicated and prio.test priority", lim)
}
h.s.cfg.Server.PriorityNotifySlots = 3
if got := h.s.notifyLimits().dedicated; got != 3 {
t.Fatalf("dedicated = %d, want the configured 3", got)
}
}
func TestNotifyCapsKeyOnOneSpellingOfAKnotHost(t *testing.T) {
n, _ := newSlots()
for _, host := range []string{"Knot.Test", "knot.test.", "KNOT.test"} {
if !n.acquire(host, shared(8)) {
t.Fatalf("slot refused for %s below the pool", host)
}
}
if len(n.held) != 1 || n.held["knot.test"] != 3 {
t.Fatalf("held = %v, want one knot holding 3", n.held)
}
for _, host := range []string{"knot.test", "KNOT.test.", "Knot.Test"} {
n.release(host)
}
if len(n.held) != 0 || n.inflight() != 0 {
t.Fatalf("held = %v after releasing under other spellings", n.held)
}
var unknown notifyAdmission
if !unknown.acquire("Knot.Test") || unknown.acquire("knot.test.") {
t.Fatal("the unknown-repo pool keyed one knot under two spellings")
}
}
func TestCanonicalKnotHostKeepsThePort(t *testing.T) {
for raw, want := range map[string]string{
"Knot.Test:8443": "knot.test:8443",
"knot.test.": "knot.test",
"knot.test": "knot.test",
} {
if got := canonicalKnotHost(raw); got != want {
t.Errorf("canonicalKnotHost(%q) = %q, want %q", raw, got, want)
}
}
}