Bound what a refusal costs the engine, and let the busy tests fail instead of hang
A refusal waiting for its request parsed it into a 64 KiB reader for up to 5 s, on a socket every account may open. It now drains at most 4 KiB through a small buffer for at most 1 s and never past the accept-time deadline, only 32 wait at once, and an unknown account is refused before it takes a slot. The busy tests wait for the first line with a timeout and say what came of it (novox/hq issue 434).
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/user"
|
||||
@@ -172,6 +173,25 @@ func keysOf(t *testing.T, body []byte) string {
|
||||
return strings.Join(keys, " ")
|
||||
}
|
||||
|
||||
// holdALine asks one line and returns once the engine is asking it, so it holds its account's slot. A first line
|
||||
// that ends or never arrives fails the test at once, with what came of it, instead of hanging the suite (novox/hq
|
||||
// issue 434).
|
||||
func holdALine(t *testing.T, socket string, started <-chan struct{}) {
|
||||
t.Helper()
|
||||
first := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := cli.Ask(context.Background(), socket, []string{"push"})
|
||||
first <- err
|
||||
}()
|
||||
select {
|
||||
case <-started:
|
||||
case err := <-first:
|
||||
t.Fatalf("the first line ended before it was asked: %v", err)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("the first line was never asked")
|
||||
}
|
||||
}
|
||||
|
||||
// **One account cannot hold the socket** (review of ADR 0272): its lines beyond its own bound are answered busy at
|
||||
// once, nothing is asked, and the engine goes on accepting.
|
||||
func TestOneAccountsLinesBeyondItsBoundAreAnsweredBusy(t *testing.T) {
|
||||
@@ -187,8 +207,7 @@ func TestOneAccountsLinesBeyondItsBoundAreAnsweredBusy(t *testing.T) {
|
||||
})
|
||||
// Released before the engine is stopped: cleanups run last first, and the engine waits for its lines.
|
||||
t.Cleanup(func() { close(release) })
|
||||
go func() { _, _ = cli.Ask(context.Background(), socket, []string{"push"}) }()
|
||||
<-started
|
||||
holdALine(t, socket, started)
|
||||
type asked struct {
|
||||
a cli.Answer
|
||||
err error
|
||||
@@ -229,8 +248,7 @@ func TestARefusalWaitsForTheRequestItAnswers(t *testing.T) {
|
||||
return cli.Answer{}
|
||||
})
|
||||
t.Cleanup(func() { close(release) })
|
||||
go func() { _, _ = cli.Ask(context.Background(), socket, []string{"push"}) }()
|
||||
<-started
|
||||
holdALine(t, socket, started)
|
||||
conn, err := net.Dial("unix", socket)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -256,6 +274,62 @@ func TestARefusalWaitsForTheRequestItAnswers(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// **Refusals that wait are bounded** (novox/hq issue 434, review of ADR 0272): with refusalsWaiting already full, a
|
||||
// further refused line is answered at once, without waiting for its request.
|
||||
func TestARefusalOverTheWaitingBoundIsAnsweredAtOnce(t *testing.T) {
|
||||
was, wasWithin, wasWaiting := perAccount, refusedWithin, refusalsWaiting
|
||||
perAccount, refusedWithin = 1, time.Minute
|
||||
refusalsWaiting = make(chan struct{}, 1)
|
||||
refusalsWaiting <- struct{}{}
|
||||
t.Cleanup(func() { perAccount, refusedWithin, refusalsWaiting = was, wasWithin, wasWaiting })
|
||||
release := make(chan struct{})
|
||||
started := make(chan struct{}, 2)
|
||||
socket := serving(t, func(context.Context, Asked) cli.Answer {
|
||||
started <- struct{}{}
|
||||
<-release
|
||||
return cli.Answer{}
|
||||
})
|
||||
t.Cleanup(func() { close(release) })
|
||||
holdALine(t, socket, started)
|
||||
conn, err := net.Dial("unix", socket)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
// Nothing is written: a refusal that waited would wait refusedWithin, a minute, for it.
|
||||
_ = conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
a, err := cli.ReadAnswer(conn)
|
||||
if err != nil || !strings.Contains(a.Refused, "busy") {
|
||||
t.Fatalf("a refusal over the waiting bound reads %+v (%v)", a, err)
|
||||
}
|
||||
}
|
||||
|
||||
// countingReader is an endless request with no line end, counting what was read of it.
|
||||
type countingReader struct{ read int }
|
||||
|
||||
func (c *countingReader) Read(p []byte) (int, error) {
|
||||
for i := range p {
|
||||
p[i] = 'x'
|
||||
}
|
||||
c.read += len(p)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// **A refused request is drained, not kept** (novox/hq issue 434): what a refused client writes is read up to
|
||||
// drainCap bytes and no further, however much it sends.
|
||||
func TestARefusedRequestIsDrainedNoFurtherThanItsCap(t *testing.T) {
|
||||
r := &countingReader{}
|
||||
drain(r)
|
||||
if r.read != drainCap {
|
||||
t.Fatalf("drained %d bytes of an endless request; the cap is %d", r.read, drainCap)
|
||||
}
|
||||
after := &countingReader{}
|
||||
drain(io.MultiReader(strings.NewReader("{\"line\":[\"status\"]}\n"), after))
|
||||
if after.read != 0 {
|
||||
t.Fatalf("draining read %d bytes past the end of the request line", after.read)
|
||||
}
|
||||
}
|
||||
|
||||
// **A process in another namespace is not this machine's account** (review of ADR 0272): a container running with
|
||||
// the operator's user id shares the id and not the machine; refused, and nothing is asked.
|
||||
func TestAPeerInAnotherNamespaceIsRefused(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user