End every wait in flight when the tool runner stops (issue 745)
mesh/merge-gate head 21d038d3397c merged into trunk 5d70eb3e35a9 — pass, with a note: builds mesh-tools, node-tools (+39 dependent(s)) → ace, g14, novox, s…
mesh/repo-check head 21d038d3397c merged into trunk 5d70eb3e35a9 — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the ch…
mesh/delivery delivered

Closing the console shut its server down with a context that never ends, so a stop or restart of
the tool runner waited on any mesh_wait in flight for up to ten minutes. The server's base context
is now cancelled first, ending every wait at once, with a five-second shutdown as a backstop
(second review of issue 745).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 00:47:59 +00:00
co-authored by Claude Opus 5.5
parent 5d70eb3e35
commit 21d038d339
12 changed files with 775 additions and 18 deletions
+23 -4
View File
@@ -10,6 +10,7 @@ import (
"net/http"
"strconv"
"strings"
"time"
"github.com/novox/mesh-tools/node-tools/internal/wire"
)
@@ -45,12 +46,30 @@ func Serve(s *Surface, listen string) (*Listening, error) {
if err != nil {
return nil, err
}
server := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { route(w, r, s) })}
// Every request's context ends when the console is closed, so a mesh_wait in flight ends at once rather
// than holding a stop or a restart for up to its ten minutes (the review of issue 745).
base, cancel := context.WithCancel(context.Background())
server := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { route(w, r, s) }),
BaseContext: func(net.Listener) context.Context { return base }}
go func() { _ = server.Serve(ln) }()
bound := ln.Addr().(*net.TCPAddr).Port
return &Listening{Address: host + ":" + strconv.Itoa(bound), Close: func() error { return server.Shutdown(context.Background()) }}, nil
return &Listening{Address: host + ":" + strconv.Itoa(bound), Close: func() error {
cancel()
// A backstop: what is still answering after the cancel is cut off, never waited on for long.
ctx, done := context.WithTimeout(context.Background(), ShutdownWithin)
defer done()
err := server.Shutdown(ctx)
if errors.Is(err, context.DeadlineExceeded) {
return server.Close()
}
return err
}}, nil
}
// ShutdownWithin is how long closing the console waits for the requests still being answered after their
// context was cancelled.
var ShutdownWithin = 5 * time.Second
func route(w http.ResponseWriter, r *http.Request, s *Surface) {
switch r.URL.Path {
case "/":
@@ -89,7 +108,7 @@ func route(w http.ResponseWriter, r *http.Request, s *Surface) {
}
replies := []*Reply{}
for _, req := range batch {
if reply := s.Handle(req); reply != nil {
if reply := s.HandleContext(r.Context(), req); reply != nil {
replies = append(replies, reply)
}
}
@@ -105,7 +124,7 @@ func route(w http.ResponseWriter, r *http.Request, s *Surface) {
writeJSON(w, 400, map[string]any{"jsonrpc": "2.0", "id": nil, "error": map[string]any{"code": -32700, "message": "the body is not JSON"}})
return
}
reply := s.Handle(req)
reply := s.HandleContext(r.Context(), req)
if reply == nil {
w.WriteHeader(202)
return