Answer a panicking verb with an error, say flag errors in the answer, and leave refused logins out of D15

Read verbs now run in the serving process, where a panic would end every
call; refused logins are nobody's reconnect loop (review of hq issue 327).
This commit is contained in:
jochen
2026-10-08 21:08:34 +02:00
parent 1e04670052
commit cc7fb99f29
9 changed files with 110 additions and 33 deletions
+14
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"log"
"regexp"
"runtime/debug"
"sort"
"strconv"
"strings"
@@ -575,6 +576,19 @@ func (l *CallLog) serveCallWithin(seat, verb string, args json.RawMessage, reply
done := make(chan outcome, 1)
go func() {
var body []byte
// **A handler that panics is an answer, not a dead controller.** A verb answered in the serving
// process (novox/hq issue 327: conditions, hand-acts, queue, dead-letters) runs on this goroutine,
// where a panic would take every other call and the controller with it.
defer func() {
if p := recover(); p != nil {
if logger != nil {
logger.Printf("%s.%s: call %s panicked: %v\n%s", seat, verb, c.ID, p, debug.Stack())
}
body, _ = json.Marshal(map[string]any{"error": fmt.Sprintf("%s failed inside the controller and "+
"answered nothing: %v. It is in the controller's log", verb, p)})
done <- outcome{body, true}
}
}()
result, err := handle(ctx, args)
failed := err != nil
if errors.Is(err, ErrHandingOver) {
+30
View File
@@ -11,6 +11,10 @@ import (
"sync"
"testing"
"time"
"github.com/nats-io/nats.go"
"github.com/novox/mesh-controller/internal/testbus"
)
// answers collects what a call answered, and fails a second answer: the bus permits one.
@@ -223,3 +227,29 @@ func TestAJoinTokenIsShownToItsCallerAndNotKept(t *testing.T) {
}
}
}
// A verb whose handler panics answers an error, and the controller serves the next call (review of
// novox/hq issue 327: read verbs are answered in the serving process now).
func TestAHandlerThatPanicsAnswersAnError(t *testing.T) {
conn, err := nats.Connect(testbus.URL(t))
if err != nil {
t.Fatal(err)
}
defer conn.Close()
stop, err := OverNATS{Conn: conn}.ServeSeatTools("panicky", map[string]ToolHandler{
"boom": func(context.Context, json.RawMessage) (any, error) { panic("nil map") },
"fine": func(context.Context, json.RawMessage) (any, error) { return "ok", nil },
}, nil)
if err != nil {
t.Fatal(err)
}
defer stop()
answer, err := AskMeshSeatTool(context.Background(), conn, "panicky", "boom", map[string]any{}, 5*time.Second)
if err != nil || !strings.Contains(answer.Error, "failed inside the controller") {
t.Fatalf("a panic answered %+v (%v)", answer, err)
}
answer, err = AskMeshSeatTool(context.Background(), conn, "panicky", "fine", map[string]any{}, 5*time.Second)
if err != nil || answer.Error != "" {
t.Fatalf("the call after a panic answered %+v (%v)", answer, err)
}
}