cli: after a failed write take only a refusal, since nothing can have run, and pin that the read keeps the ask's deadline (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 delivered

This commit is contained in:
2026-10-11 02:49:17 +02:00
parent b4900f52b8
commit 24f5ee81bd
2 changed files with 62 additions and 0 deletions
+6
View File
@@ -144,10 +144,16 @@ func AskWith(ctx context.Context, socket string, req Request) (Answer, error) {
// and the connection closed — and the write then fails on the close. That answer is already waiting, and // 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 // 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. // 434). So read it, within the deadline the connection already has, and only when none came say the write.
// Only a refusal is taken: the request was never wholly written, so nothing can have run, and any other
// answer here is not one a correct engine gives — passing it on as success would hide the failure.
a, rerr := ReadAnswer(conn) a, rerr := ReadAnswer(conn)
if rerr != nil { if rerr != nil {
return Answer{}, fmt.Errorf("asking the node-engine: %w; and no answer came (%v)", err, rerr) return Answer{}, fmt.Errorf("asking the node-engine: %w; and no answer came (%v)", err, rerr)
} }
if a.Refused == "" {
return Answer{}, fmt.Errorf("asking the node-engine: %w; and the answer that came is not a refusal, "+
"though the request was never wholly written", err)
}
return a, nil return a, nil
} }
a, err := ReadAnswer(conn) a, err := ReadAnswer(conn)
+56
View File
@@ -2,7 +2,9 @@ package cli
import ( import (
"context" "context"
"errors"
"net" "net"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
@@ -65,3 +67,57 @@ func TestAFailedWriteWithNoAnswerSaysBoth(t *testing.T) {
t.Fatalf("waited %s on a connection the engine had closed", waited) t.Fatalf("waited %s on a connection the engine had closed", waited)
} }
} }
// After a failed write only a refusal is taken: nothing can have run, so an answer saying it ran and exited 0
// is an error, never a success (novox/hq issue 444).
func TestAnAnswerOtherThanARefusalAfterAFailedWriteIsAnError(t *testing.T) {
ran := Answer{Exit: 0, Stdout: []byte("ok")}
socket := anEngineThatClosesFirst(t, &ran)
a, err := Ask(context.Background(), socket, aLineTheSocketCannotHold())
if err == nil {
t.Fatalf("a request never wholly written came back as success: %+v", a)
}
if !strings.Contains(err.Error(), "not a refusal") {
t.Fatalf("the error does not say the answer was not a refusal: %v", err)
}
}
// Reading after a failed write never waits longer than the ask already allowed: a peer that never reads and
// never answers is given up on at the context's deadline, not later (novox/hq issue 444).
func TestAFailedWriteWaitsNoLongerThanTheDeadline(t *testing.T) {
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() })
held := make(chan net.Conn, 1)
go func() {
conn, err := l.Accept()
if err != nil {
return
}
held <- conn // neither read nor answered, nor closed, until the test ends
}()
t.Cleanup(func() {
select {
case c := <-held:
c.Close()
default:
}
})
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
start := time.Now()
_, err = Ask(ctx, socket, aLineTheSocketCannotHold())
waited := time.Since(start)
if err == nil {
t.Fatal("a peer that never read nor answered gave no error")
}
if !errors.Is(err, os.ErrDeadlineExceeded) {
t.Fatalf("the error is not the deadline: %v", err)
}
if waited > 3*time.Second {
t.Fatalf("waited %s with a deadline of 1s", waited)
}
}