cli: read the engine's answer after a failed request write, so a refusal is not lost as a broken pipe (hq issue 444)
mesh/merge-gate pass: builds mesh-sdk, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript), go/merge-check.sh (go)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds mesh-sdk, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript), go/merge-check.sh (go)
mesh/delivery superseded: a newer head of the same pull request
This commit is contained in:
+9
-1
@@ -140,7 +140,15 @@ func AskWith(ctx context.Context, socket string, req Request) (Answer, error) {
|
|||||||
_ = conn.SetDeadline(deadline)
|
_ = conn.SetDeadline(deadline)
|
||||||
}
|
}
|
||||||
if err := WriteRequest(conn, req); err != nil {
|
if err := WriteRequest(conn, req); err != nil {
|
||||||
return Answer{}, fmt.Errorf("asking the node-engine: %w", err)
|
// The engine may have answered before it read the request — a refusal (busy, an unknown account) written
|
||||||
|
// and the connection closed — and the write then fails on the close. That answer is already waiting, and
|
||||||
|
// it says why where the write error says only that the pipe broke (novox/hq issue 444, found under issue
|
||||||
|
// 434). So read it, within the deadline the connection already has, and only when none came say the write.
|
||||||
|
a, rerr := ReadAnswer(conn)
|
||||||
|
if rerr != nil {
|
||||||
|
return Answer{}, fmt.Errorf("asking the node-engine: %w; and no answer came (%v)", err, rerr)
|
||||||
|
}
|
||||||
|
return a, nil
|
||||||
}
|
}
|
||||||
a, err := ReadAnswer(conn)
|
a, err := ReadAnswer(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// anEngineThatClosesFirst listens like the node-engine before mesh-host#80: it writes its answer, if any, and
|
||||||
|
// closes without reading the request (novox/hq issue 434). It answers one connection.
|
||||||
|
func anEngineThatClosesFirst(t *testing.T, answer *Answer) string {
|
||||||
|
t.Helper()
|
||||||
|
socket := filepath.Join(t.TempDir(), "cli.sock")
|
||||||
|
l, err := net.Listen("unix", socket)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("no unix socket here: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { l.Close() })
|
||||||
|
go func() {
|
||||||
|
conn, err := l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if answer != nil {
|
||||||
|
_ = WriteAnswer(conn, *answer)
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
}()
|
||||||
|
return socket
|
||||||
|
}
|
||||||
|
|
||||||
|
// aLineTheSocketCannotHold is a request larger than a unix socket buffers, so its write cannot finish before
|
||||||
|
// the engine reads it — and an engine that closes first makes it fail every time, not only when it loses a race.
|
||||||
|
func aLineTheSocketCannotHold() []string { return []string{"status", strings.Repeat("x", 4<<20)} }
|
||||||
|
|
||||||
|
// An engine that refuses and closes before reading the request is heard: its refusal reaches the caller, not
|
||||||
|
// the broken pipe its close left the client's write (novox/hq issue 444).
|
||||||
|
func TestARefusalWrittenBeforeTheRequestIsReadReachesTheCaller(t *testing.T) {
|
||||||
|
busy := Refusal("the node-engine is busy: too many lines at once")
|
||||||
|
socket := anEngineThatClosesFirst(t, &busy)
|
||||||
|
a, err := Ask(context.Background(), socket, aLineTheSocketCannotHold())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("the engine said %q and the caller heard: %v", busy.Refused, err)
|
||||||
|
}
|
||||||
|
if a.Refused != busy.Refused || a.Exit != 1 {
|
||||||
|
t.Fatalf("the engine said %q and the caller heard %+v", busy.Refused, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the write fails and no answer is waiting, the error says both: what failed, and that no answer came.
|
||||||
|
func TestAFailedWriteWithNoAnswerSaysBoth(t *testing.T) {
|
||||||
|
socket := anEngineThatClosesFirst(t, nil)
|
||||||
|
start := time.Now()
|
||||||
|
_, err := Ask(context.Background(), socket, aLineTheSocketCannotHold())
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("a request the engine never read, and never answered, came back without an error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "asking the node-engine") || !strings.Contains(err.Error(), "no answer came") {
|
||||||
|
t.Fatalf("the error does not say what failed and that no answer came: %v", err)
|
||||||
|
}
|
||||||
|
if waited := time.Since(start); waited > 30*time.Second {
|
||||||
|
t.Fatalf("waited %s on a connection the engine had closed", waited)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user