|
|
|
@@ -29,6 +29,13 @@ const (
|
|
|
|
|
|
|
|
|
|
|
|
guardClient = "198.51.100.7"
|
|
|
|
guardClient = "198.51.100.7"
|
|
|
|
guardUser = "admin"
|
|
|
|
guardUser = "admin"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// racePasses is how many times a both-cases-ready select race is
|
|
|
|
|
|
|
|
// run. Each pass is an independent coin flip if the code under
|
|
|
|
|
|
|
|
// test does not settle the race itself, so at this N a
|
|
|
|
|
|
|
|
// regression is caught with probability 1 - 2^-N and the test
|
|
|
|
|
|
|
|
// still waits on nothing.
|
|
|
|
|
|
|
|
racePasses = 1000
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// newGuard builds a guard with production-shaped defaults and the
|
|
|
|
// newGuard builds a guard with production-shaped defaults and the
|
|
|
|
@@ -224,6 +231,13 @@ func TestLoginGuard_SemaphoreBoundsConcurrentVerifications(
|
|
|
|
const (
|
|
|
|
const (
|
|
|
|
concurrency = 2
|
|
|
|
concurrency = 2
|
|
|
|
workers = 12
|
|
|
|
workers = 12
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// rendezvousDeadlock is the deadlock guard described below.
|
|
|
|
|
|
|
|
// It is orders of magnitude longer than any scheduling delay,
|
|
|
|
|
|
|
|
// so it never decides the result, and well inside script/test's
|
|
|
|
|
|
|
|
// 30s timeout, so a wedge fails on the assertion instead of
|
|
|
|
|
|
|
|
// blowing the package timeout.
|
|
|
|
|
|
|
|
rendezvousDeadlock = 5 * time.Second
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
g := newGuard(middleware.LoginFailureMaxKeysConst, concurrency)
|
|
|
|
g := newGuard(middleware.LoginFailureMaxKeysConst, concurrency)
|
|
|
|
@@ -233,8 +247,29 @@ func TestLoginGuard_SemaphoreBoundsConcurrentVerifications(
|
|
|
|
inside int
|
|
|
|
inside int
|
|
|
|
highest int
|
|
|
|
highest int
|
|
|
|
wg sync.WaitGroup
|
|
|
|
wg sync.WaitGroup
|
|
|
|
|
|
|
|
once sync.Once
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Slot holders rendezvous instead of sleeping. A sleep only makes
|
|
|
|
|
|
|
|
// overlap likely — on a host loaded enough to deschedule a
|
|
|
|
|
|
|
|
// goroutine for longer than the sleep, the workers can serialise
|
|
|
|
|
|
|
|
// and the maximum observed comes back as 1. Holding until the
|
|
|
|
|
|
|
|
// concurrency-th holder arrives makes the overlap the assertion
|
|
|
|
|
|
|
|
// needs a fact rather than a race won: the first holder cannot
|
|
|
|
|
|
|
|
// leave until a second one is inside with it.
|
|
|
|
|
|
|
|
overlapped := make(chan struct{})
|
|
|
|
|
|
|
|
closeOverlapped := func() {
|
|
|
|
|
|
|
|
once.Do(func() { close(overlapped) })
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Deadlock guard, not a timing margin: no assertion depends on
|
|
|
|
|
|
|
|
// its length and the healthy path closes overlapped in
|
|
|
|
|
|
|
|
// microseconds. It is here so that a guard which never admits two
|
|
|
|
|
|
|
|
// requests at once fails legibly on the assertion below instead
|
|
|
|
|
|
|
|
// of hanging until the package test timeout.
|
|
|
|
|
|
|
|
abandon := time.AfterFunc(rendezvousDeadlock, closeOverlapped)
|
|
|
|
|
|
|
|
defer abandon.Stop()
|
|
|
|
|
|
|
|
|
|
|
|
for range workers {
|
|
|
|
for range workers {
|
|
|
|
wg.Go(func() {
|
|
|
|
wg.Go(func() {
|
|
|
|
release, ok := g.AcquireForTest(context.Background())
|
|
|
|
release, ok := g.AcquireForTest(context.Background())
|
|
|
|
@@ -251,11 +286,15 @@ func TestLoginGuard_SemaphoreBoundsConcurrentVerifications(
|
|
|
|
highest = inside
|
|
|
|
highest = inside
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
reached := inside == concurrency
|
|
|
|
|
|
|
|
|
|
|
|
mu.Unlock()
|
|
|
|
mu.Unlock()
|
|
|
|
|
|
|
|
|
|
|
|
// Hold the slot long enough that the other workers are
|
|
|
|
if reached {
|
|
|
|
// certainly contending for it.
|
|
|
|
closeOverlapped()
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<-overlapped
|
|
|
|
|
|
|
|
|
|
|
|
mu.Lock()
|
|
|
|
mu.Lock()
|
|
|
|
inside--
|
|
|
|
inside--
|
|
|
|
@@ -278,6 +317,14 @@ func TestLoginGuard_SemaphoreBoundsConcurrentVerifications(
|
|
|
|
// what happens when every slot is taken for longer than the wait: the
|
|
|
|
// what happens when every slot is taken for longer than the wait: the
|
|
|
|
// request is refused, so the caller answers 503 without allocating
|
|
|
|
// request is refused, so the caller answers 503 without allocating
|
|
|
|
// another 64 MB hash.
|
|
|
|
// another 64 MB hash.
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
// Neither half of this rides on the wait being long enough. The
|
|
|
|
|
|
|
|
// refusal holds the only slot across the whole of the second call, so
|
|
|
|
|
|
|
|
// there is no wait it could get lucky with — the wait fixes only how
|
|
|
|
|
|
|
|
// long the refusal takes, not whether it happens. The reuse after
|
|
|
|
|
|
|
|
// release is settled by acquire's non-blocking preamble, which is
|
|
|
|
|
|
|
|
// pinned separately by TestLoginGuard_FreeSlotBeatsAnExpiredWait. So
|
|
|
|
|
|
|
|
// the wait below is sized to keep the test quick, not to win a race.
|
|
|
|
func TestLoginGuard_SaturatedSemaphoreRefusesRatherThanQueueing(
|
|
|
|
func TestLoginGuard_SaturatedSemaphoreRefusesRatherThanQueueing(
|
|
|
|
t *testing.T,
|
|
|
|
t *testing.T,
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
@@ -305,13 +352,58 @@ func TestLoginGuard_SaturatedSemaphoreRefusesRatherThanQueueing(
|
|
|
|
release()
|
|
|
|
release()
|
|
|
|
|
|
|
|
|
|
|
|
release, ok = g.AcquireForTest(context.Background())
|
|
|
|
release, ok = g.AcquireForTest(context.Background())
|
|
|
|
assert.True(
|
|
|
|
|
|
|
|
|
|
|
|
// require, not assert: acquire returns a nil release alongside a
|
|
|
|
|
|
|
|
// false ok, so calling it after a non-fatal assertion turns one
|
|
|
|
|
|
|
|
// failed test into a segfault that takes the whole package test
|
|
|
|
|
|
|
|
// binary down. Every assertion whose value is dereferenced later
|
|
|
|
|
|
|
|
// has to stop the test.
|
|
|
|
|
|
|
|
require.True(
|
|
|
|
t, ok, "the slot must be reusable once released",
|
|
|
|
t, ok, "the slot must be reusable once released",
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
release()
|
|
|
|
release()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// TestLoginGuard_FreeSlotBeatsAnExpiredWait is the determinism this
|
|
|
|
|
|
|
|
// file used to lack. acquire selects over a slot send and a wait
|
|
|
|
|
|
|
|
// timer, and Go chooses among ready cases uniformly at random, so a
|
|
|
|
|
|
|
|
// call made after the timer had already fired was a coin flip: on a
|
|
|
|
|
|
|
|
// loaded host the previous test's third acquire could be refused
|
|
|
|
|
|
|
|
// with its slot standing free, and then dereference the nil release
|
|
|
|
|
|
|
|
// it got back.
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
// The wait here is already elapsed on arrival, which is the worst
|
|
|
|
|
|
|
|
// case that scheduling can produce, so a free slot must still be
|
|
|
|
|
|
|
|
// granted every time. Without acquire's non-blocking preamble each
|
|
|
|
|
|
|
|
// pass is an independent coin flip and the loop fails within a few
|
|
|
|
|
|
|
|
// passes; with it the property holds by construction and no wall
|
|
|
|
|
|
|
|
// clock is involved.
|
|
|
|
|
|
|
|
func TestLoginGuard_FreeSlotBeatsAnExpiredWait(t *testing.T) {
|
|
|
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
g := middleware.NewLoginGuardForTest(
|
|
|
|
|
|
|
|
middleware.LoginRateLimitConst,
|
|
|
|
|
|
|
|
guardInterval,
|
|
|
|
|
|
|
|
middleware.LoginFailureMaxKeysConst,
|
|
|
|
|
|
|
|
1,
|
|
|
|
|
|
|
|
middleware.PasswordVerifyMaxWaitersConst,
|
|
|
|
|
|
|
|
0,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for pass := range racePasses {
|
|
|
|
|
|
|
|
release, ok := g.AcquireForTest(context.Background())
|
|
|
|
|
|
|
|
require.Truef(
|
|
|
|
|
|
|
|
t, ok,
|
|
|
|
|
|
|
|
"pass %d was refused a slot that was free; an expired "+
|
|
|
|
|
|
|
|
"wait must never beat an available slot",
|
|
|
|
|
|
|
|
pass,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
release()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestLoginGuard_AcquireHonoursCancellation proves a client that
|
|
|
|
// TestLoginGuard_AcquireHonoursCancellation proves a client that
|
|
|
|
// disconnects while queued frees its place immediately instead of
|
|
|
|
// disconnects while queued frees its place immediately instead of
|
|
|
|
// holding it for the full wait.
|
|
|
|
// holding it for the full wait.
|
|
|
|
@@ -388,13 +480,20 @@ func TestLoginGuard_ShedsPastTheQueueCap(t *testing.T) {
|
|
|
|
neverElapses = time.Minute
|
|
|
|
neverElapses = time.Minute
|
|
|
|
|
|
|
|
|
|
|
|
// The probe carries its own deadline, so a guard that queues
|
|
|
|
// The probe carries its own deadline, so a guard that queues
|
|
|
|
// the probe instead of shedding it fails on the elapsed time
|
|
|
|
// the probe instead of shedding it fails here rather than
|
|
|
|
// rather than hanging until the package test timeout.
|
|
|
|
// hanging until the package test timeout.
|
|
|
|
probeWait = 200 * time.Millisecond
|
|
|
|
//
|
|
|
|
|
|
|
|
// This is a patience budget, not a margin to be won. A shed
|
|
|
|
// Shedding takes no measurable time; queueing takes the whole
|
|
|
|
// returns in microseconds and a probe that queued instead
|
|
|
|
// probeWait. Anything under half of it is unambiguous.
|
|
|
|
// would not return for neverElapses, so the two are a whole
|
|
|
|
shedFast = probeWait / 2
|
|
|
|
// minute apart and any budget between them separates them. It
|
|
|
|
|
|
|
|
// is set far above any scheduling stall a loaded host can
|
|
|
|
|
|
|
|
// produce, because the previous 200 ms — and the 100 ms
|
|
|
|
|
|
|
|
// elapsed-time assertion it fed — bounded the latency of a
|
|
|
|
|
|
|
|
// goroutine hand-off, which is a false red waiting to happen
|
|
|
|
|
|
|
|
// on the machine this suite runs on. What actually proves the
|
|
|
|
|
|
|
|
// probe was not queued is the queue depth asserted below.
|
|
|
|
|
|
|
|
probePatience = 5 * time.Second
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
g := middleware.NewLoginGuardForTest(
|
|
|
|
g := middleware.NewLoginGuardForTest(
|
|
|
|
@@ -413,7 +512,7 @@ func TestLoginGuard_ShedsPastTheQueueCap(t *testing.T) {
|
|
|
|
defer release()
|
|
|
|
defer release()
|
|
|
|
defer fillQueue(t, g, maxWaiters)()
|
|
|
|
defer fillQueue(t, g, maxWaiters)()
|
|
|
|
|
|
|
|
|
|
|
|
got := probeQueueCap(g, probeWait)
|
|
|
|
got := probeQueueCap(g, probePatience)
|
|
|
|
|
|
|
|
|
|
|
|
require.NotNil(
|
|
|
|
require.NotNil(
|
|
|
|
t, got,
|
|
|
|
t, got,
|
|
|
|
@@ -421,14 +520,9 @@ func TestLoginGuard_ShedsPastTheQueueCap(t *testing.T) {
|
|
|
|
"be queued; it must have been shed",
|
|
|
|
"be queued; it must have been shed",
|
|
|
|
)
|
|
|
|
)
|
|
|
|
assert.False(
|
|
|
|
assert.False(
|
|
|
|
t, got.ok,
|
|
|
|
t, *got,
|
|
|
|
"a request arriving past the queue cap must be shed",
|
|
|
|
"a request arriving past the queue cap must be shed",
|
|
|
|
)
|
|
|
|
)
|
|
|
|
assert.Less(
|
|
|
|
|
|
|
|
t, got.elapsed, shedFast,
|
|
|
|
|
|
|
|
"shedding must be immediate; waiting for a place in the "+
|
|
|
|
|
|
|
|
"queue is the memory growth this bounds",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
assert.Equal(
|
|
|
|
assert.Equal(
|
|
|
|
t, maxWaiters, g.QueuedWaitersForTest(),
|
|
|
|
t, maxWaiters, g.QueuedWaitersForTest(),
|
|
|
|
"a shed request must not have grown the queue",
|
|
|
|
"a shed request must not have grown the queue",
|
|
|
|
@@ -458,10 +552,14 @@ func fillQueue(
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Patience budget, not a margin: the waiters park in microseconds
|
|
|
|
|
|
|
|
// and nothing releases them, so the only way to exhaust this is a
|
|
|
|
|
|
|
|
// guard that never queues. One second is the same order as the
|
|
|
|
|
|
|
|
// scheduling stalls this suite has to survive, so it is not one.
|
|
|
|
require.Eventually(
|
|
|
|
require.Eventually(
|
|
|
|
t,
|
|
|
|
t,
|
|
|
|
func() bool { return g.QueuedWaitersForTest() == n },
|
|
|
|
func() bool { return g.QueuedWaitersForTest() == n },
|
|
|
|
time.Second, time.Millisecond,
|
|
|
|
5*time.Second, time.Millisecond,
|
|
|
|
"the waiters must reach the queue before the cap is tested",
|
|
|
|
"the waiters must reach the queue before the cap is tested",
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@@ -471,35 +569,30 @@ func fillQueue(
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// probeResult is what the queue-cap probe reports: whether it got a
|
|
|
|
// probeQueueCap acquires from another goroutine and reports whether
|
|
|
|
// slot, and how long it took to find out.
|
|
|
|
// it got a slot, or nil if the call was still blocked after wait.
|
|
|
|
type probeResult struct {
|
|
|
|
|
|
|
|
ok bool
|
|
|
|
|
|
|
|
elapsed time.Duration
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// probeQueueCap acquires from another goroutine and reports the
|
|
|
|
|
|
|
|
// result, or nil if the call was still blocked after wait.
|
|
|
|
|
|
|
|
//
|
|
|
|
//
|
|
|
|
// It runs off the test goroutine deliberately. Joining a full queue
|
|
|
|
// It runs off the test goroutine deliberately. Joining a full queue
|
|
|
|
// is not cancellable by context — refusing to join is the property
|
|
|
|
// is not cancellable by context — refusing to join is the property
|
|
|
|
// under test — so a guard that fails this would otherwise hang the
|
|
|
|
// under test — so a guard that fails this would otherwise hang the
|
|
|
|
// package until the test timeout instead of failing here.
|
|
|
|
// package until the test timeout instead of failing here.
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
// It reports no elapsed time. Timing a goroutine hand-off measures
|
|
|
|
|
|
|
|
// the host, not the guard, and the caller distinguishes shedding from
|
|
|
|
|
|
|
|
// queueing by the queue depth instead.
|
|
|
|
func probeQueueCap(
|
|
|
|
func probeQueueCap(
|
|
|
|
g *middleware.LoginGuard,
|
|
|
|
g *middleware.LoginGuard,
|
|
|
|
wait time.Duration,
|
|
|
|
wait time.Duration,
|
|
|
|
) *probeResult {
|
|
|
|
) *bool {
|
|
|
|
probed := make(chan probeResult, 1)
|
|
|
|
probed := make(chan bool, 1)
|
|
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
go func() {
|
|
|
|
start := time.Now()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
release, ok := g.AcquireForTest(context.Background())
|
|
|
|
release, ok := g.AcquireForTest(context.Background())
|
|
|
|
if ok {
|
|
|
|
if ok {
|
|
|
|
release()
|
|
|
|
release()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
probed <- probeResult{ok: ok, elapsed: time.Since(start)}
|
|
|
|
probed <- ok
|
|
|
|
}()
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
select {
|
|
|
|
|