Ask a seat once more when the controller refused as handing over (hq issue 289)
mesh/merge-gate pass: builds mesh-tools, node-tools → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of …
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/a-verb-survives-the-handover delivered: every member is delivered

A controller being replaced refuses a call it cannot serve, marked retry:
handing-over; the one after it answers the same call.
This commit is contained in:
jochen
2026-10-07 02:13:17 +02:00
parent 01c98db0a4
commit 2f045e042f
2 changed files with 106 additions and 6 deletions
+33 -6
View File
@@ -578,28 +578,55 @@ func (c *Conn) Ask(key string, body any, on string) (Answered, error) {
if err != nil {
return Answered{}, err
}
answered, handingOver, err := c.askOnce(subject, data)
if !handingOver {
return answered, err
}
// **Refused because the controller is handing over: asked once more** (novox/hq issue 289). The
// controller being replaced did nothing and said so; the one after it answers the same call. Once:
// a second refusal is a controller that is not coming back, and the caller is told.
time.Sleep(HandoverPause)
answered, again, err := c.askOnce(subject, data)
if err != nil && again {
return Answered{}, fmt.Errorf("asked twice, %s apart, and both times refused as a handover: %w", HandoverPause, err)
}
return answered, err
}
// HandoverPause is how long a call refused as a handover waits before it is asked once more: the
// supervisor's restart of the controller, which stops the one being replaced and starts the next. A
// variable so a test need not wait.
var HandoverPause = 3 * time.Second
// RetryHandingOver is the mark the controller puts on a call it refused because it is being replaced
// (mesh-controller internal/link, `"retry": "handing-over"`).
const RetryHandingOver = "handing-over"
// askOnce asks once, and says whether the answer was a refusal marked to be asked again.
func (c *Conn) askOnce(subject string, data []byte) (Answered, bool, error) {
msg, err := c.nc.Request(subject, data, RequestTimeout)
if err != nil {
if errors.Is(err, nats.ErrNoResponders) {
return Answered{}, errors.New("503 no responders")
return Answered{}, false, errors.New("503 no responders")
}
if errors.Is(err, nats.ErrTimeout) {
return Answered{}, errors.New("timeout")
return Answered{}, false, errors.New("timeout")
}
return Answered{}, err
return Answered{}, false, err
}
var r struct {
Result json.RawMessage `json:"result"`
Error string `json:"error"`
Node string `json:"node"`
Retry string `json:"retry"`
}
if err := json.Unmarshal(msg.Data, &r); err != nil {
return Answered{}, err
return Answered{}, false, err
}
if r.Error != "" {
return Answered{}, errors.New(r.Error)
return Answered{}, r.Retry == RetryHandingOver, errors.New(r.Error)
}
return Answered{Result: r.Result, Node: r.Node}, nil
return Answered{Result: r.Result, Node: r.Node}, false, nil
}
// PublishAs emits an event as a module: published into JetStream and awaited, de-duplicated by its