Terminate an ask only for an answer the state refuses as too large; retry any other failed answer write (issue 1535)
mesh/merge-gate head db581abb0570 merged into trunk 3811ac69ed3e — pass: builds brother → shanks; no bus step; every machine composes with the change as it…
mesh/repo-check head db581abb0570 merged into trunk 3811ac69ed3e — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered

This commit is contained in:
2026-10-21 00:35:26 +02:00
parent 61796cd3dd
commit db581abb05
4 changed files with 41 additions and 6 deletions
+16 -3
View File
@@ -19,6 +19,7 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go" stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-sdk/go/process"
"github.com/nats-io/nats.go/jetstream"
"brother/cmd/brother/internal/secret" "brother/cmd/brother/internal/secret"
) )
@@ -79,10 +80,14 @@ func (w *deviceWorker) logf(format string, a ...any) {
} }
} }
// tooLargeCode is the bus's error code for a message over the maximum allowed (the state refuses a value so).
const tooLargeCode = 10054
// Handle answers one queued ask. An ask that cannot be read is terminated; an ask too old for anyone to wait for // Handle answers one queued ask. An ask that cannot be read is terminated; an ask too old for anyone to wait for
// is acknowledged and the device not touched; every other outcome, a failure included, is written as the answer // is acknowledged and the device not touched; every other outcome, a failure included, is written as the answer
// under the ask's id and the message acknowledged; only an answer that could not be written is redelivered (the // under the ask's id and the message acknowledged; an answer the state refuses as too large is
// bus's limit ends that). early acknowledges the message; it is called before a scan or a print, which are not // replaced by a small error answer and the message terminated; an answer that could not be written for another
// reason (the store away, a timeout) is redelivered (the bus's limit ends that). early acknowledges the message; it is called before a scan or a print, which are not
// done twice. // done twice.
func (w *deviceWorker) Handle(body []byte, early func()) (action, time.Duration) { func (w *deviceWorker) Handle(body []byte, early func()) (action, time.Duration) {
var ask Ask var ask Ask
@@ -109,7 +114,15 @@ func (w *deviceWorker) Handle(body []byte, early func()) (action, time.Duration)
} }
if _, err := w.Answers.Create(ask.ID, answer); err != nil && !errors.Is(err, stdio.ErrChanged) { if _, err := w.Answers.Create(ask.ID, answer); err != nil && !errors.Is(err, stdio.ErrChanged) {
w.logf("[brother] device: the answer to %s could not be written: %v", ask.ID, err) w.logf("[brother] device: the answer to %s could not be written: %v", ask.ID, err)
// The ask ends here: a refused answer would be refused again, and a redelivery would do the work again. var api *jetstream.APIError
if !errors.As(err, &api) || api.ErrorCode != tooLargeCode {
// The store is away, timed out or cut: the answer may be written next time (a scan is settled already).
if settled {
return actionSettled, 0
}
return actionRetry, 5 * time.Second
}
// The ask ends here: a refused value would be refused again.
// Say the failure as a small error answer, so the tool's wait ends with the reason (issue 1535). // Say the failure as a small error answer, so the tool's wait ends with the reason (issue 1535).
msg := fmt.Sprintf("the answer to this ask could not be written in the state %s: %v", AnswersState, err) msg := fmt.Sprintf("the answer to this ask could not be written in the state %s: %v", AnswersState, err)
if _, err2 := w.Answers.Create(ask.ID, map[string]any{"error": msg}); err2 != nil && !errors.Is(err2, stdio.ErrChanged) { if _, err2 := w.Answers.Create(ask.ID, map[string]any{"error": msg}); err2 != nil && !errors.Is(err2, stdio.ErrChanged) {
@@ -6,6 +6,7 @@ import (
"encoding/base64" "encoding/base64"
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@@ -518,3 +519,23 @@ func TestARefusedAnswerEndsTheAskWithAnErrorAndNoRedelivery(t *testing.T) {
t.Errorf("no small error answer: %s", raw) t.Errorf("no small error answer: %s", raw)
} }
} }
// 1535: only a refused value (error code 10054, message too large) ends the ask; a store that is away, a timeout
// or a cut connection is redelivered, a document fetch being safe to repeat.
func TestOnlyATooLargeAnswerEndsTheAskATransientFailureIsRetried(t *testing.T) {
r := newRig(t, `{}`, "pw")
big := pdfOf(300 << 10)
key := sha256Hex(big) + ".pdf"
r.objects.Put(t.Context(), key, "application/pdf", big)
ask, _ := json.Marshal(Ask{ID: "t1", Op: OpDocument, Object: key, Part: 1, At: fixedNow})
r.answers.fail = fmt.Errorf("nats: timeout")
if act, d := r.worker.Handle(ask, nil); act != actionRetry || d <= 0 {
t.Errorf("a timeout: action %v delay %v, want retry", act, d)
}
r.answers.fail = nil
r.worker.answerPart = PartSize
ask2, _ := json.Marshal(Ask{ID: "t2", Op: OpDocument, Object: key, Part: 1, At: fixedNow})
if act, _ := r.worker.Handle(ask2, nil); act != actionTerm {
t.Errorf("a too large answer: action %v, want term", act)
}
}
+2 -2
View File
@@ -207,8 +207,8 @@ func TestTheAdminWorkerSettlesEachAsk(t *testing.T) {
_ = conns _ = conns
r.answers.fail = fmt.Errorf("the bus is away") r.answers.fail = fmt.Errorf("the bus is away")
again, _ := json.Marshal(Ask{ID: "a2", Op: OpSettings, Page: DefaultPage, At: fixedNow}) again, _ := json.Marshal(Ask{ID: "a2", Op: OpSettings, Page: DefaultPage, At: fixedNow})
if act, delay := w.Handle(again, nil); act != actionTerm || delay != 0 { if act, delay := w.Handle(again, nil); act != actionRetry || delay <= 0 {
t.Errorf("an answer that cannot be written: action %v delay %v, want term (issue 1535: no redelivery)", act, delay) t.Errorf("an answer that cannot be written for want of the bus: action %v delay %v, want retry", act, delay)
} }
} }
+2 -1
View File
@@ -3,6 +3,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/nats-io/nats.go/jetstream"
"os" "os"
"path/filepath" "path/filepath"
"sync" "sync"
@@ -39,7 +40,7 @@ func (a *memAnswers) Create(key string, v any) (uint64, error) {
} }
raw, _ := json.Marshal(v) raw, _ := json.Marshal(v)
if len(raw) > StateMaxValue { if len(raw) > StateMaxValue {
return 0, fmt.Errorf("nats: API error: code=400 err_code=10054 description=message size exceeds maximum allowed (%d bytes)", len(raw)) return 0, fmt.Errorf("brother's state device-answers, key %s: nats: %w", key, &jetstream.APIError{Code: 400, ErrorCode: jetstream.ErrorCode(10054), Description: "message size exceeds maximum allowed"})
} }
a.m[key] = raw a.m[key] = raw
return 1, nil return 1, nil