Serve a module from a runtime on its own account instead of switching users, keep bus words from bundles, and never give up a channel's work (hq ADR 0259 revision)
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: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery delivered
mesh/delivery-group group feat/asks-answered-on-any-channel stopped: a member was stopped
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: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery delivered
mesh/delivery-group group feat/asks-answered-on-any-channel stopped: a member was stopped
This commit is contained in:
@@ -64,6 +64,20 @@ type SeatWorker struct {
|
||||
// ProofTimeout bounds how long a proof waits for its answer.
|
||||
var ProofTimeout = 15 * time.Second
|
||||
|
||||
// WorkHeartbeat is how often work a bundle is still doing is said to be in progress, well inside the
|
||||
// worker's ack wait.
|
||||
var WorkHeartbeat = 15 * time.Second
|
||||
|
||||
// WorkBackoff is how long a piece of work the bundle did not take waits before it is offered again: from
|
||||
// five seconds, doubling, to ten minutes.
|
||||
func WorkBackoff(delivered uint64) time.Duration {
|
||||
wait := 5 * time.Second
|
||||
for i := uint64(1); i < delivered && wait < 10*time.Minute; i++ {
|
||||
wait *= 2
|
||||
}
|
||||
return min(wait, 10*time.Minute)
|
||||
}
|
||||
|
||||
// SubjectMatches is a subject against a permission pattern, in the server's wildcards.
|
||||
func SubjectMatches(pattern, subject string) bool {
|
||||
p, s := strings.Split(pattern, "."), strings.Split(subject, ".")
|
||||
@@ -122,18 +136,29 @@ func newID() string {
|
||||
// SeatPublish submits an accept or says an event on a seat, as the module, into the stream that keeps it,
|
||||
// awaited and de-duplicated by its id: the same id twice is one message. A proof is never published here.
|
||||
func (c *Conn) SeatPublish(module, subject string, body json.RawMessage, id string) (uint64, error) {
|
||||
seq, _, err := c.SeatPublishSaid(module, subject, body, id)
|
||||
return seq, err
|
||||
}
|
||||
|
||||
// SeatPublishSaid is SeatPublish, also saying whether the bus took it as a duplicate of one published before
|
||||
// under the same id: the same message, kept once.
|
||||
func (c *Conn) SeatPublishSaid(module, subject string, body json.RawMessage, id string) (uint64, bool, error) {
|
||||
if strings.Contains(subject, ".proof.") {
|
||||
return 0, fmt.Errorf("%s is a proof, asked and answered, never kept: ask it as one", subject)
|
||||
return 0, false, fmt.Errorf("%s is a proof, asked and answered, never kept: ask it as one", subject)
|
||||
}
|
||||
if err := c.mayPublish(module, subject); err != nil {
|
||||
return 0, err
|
||||
return 0, false, err
|
||||
}
|
||||
if len(body) == 0 || !json.Valid(body) {
|
||||
return 0, fmt.Errorf("what is said on %s is JSON", subject)
|
||||
return 0, false, fmt.Errorf("what is said on %s is JSON", subject)
|
||||
}
|
||||
if id == "" {
|
||||
id = newID()
|
||||
}
|
||||
// **The publisher's name is part of the id the bus de-duplicates by** (security review 2026-10-08): ids
|
||||
// are the publisher's own, and one module could otherwise suppress another's warrant by publishing first
|
||||
// under the same id.
|
||||
id = module + "." + id
|
||||
msg := nats.NewMsg(subject)
|
||||
msg.Header.Set("x-event-id", id)
|
||||
msg.Header.Set("x-source", module)
|
||||
@@ -143,9 +168,9 @@ func (c *Conn) SeatPublish(module, subject string, body json.RawMessage, id stri
|
||||
msg.Data = body
|
||||
ack, err := c.js.PublishMsg(msg, nats.MsgId(id))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, false, err
|
||||
}
|
||||
return ack.Sequence, nil
|
||||
return ack.Sequence, ack.Duplicate, nil
|
||||
}
|
||||
|
||||
// SeatProve asks a proof and answers its reply: core request and reply, on no stream.
|
||||
@@ -263,12 +288,38 @@ func (c *Conn) SeatTake(module, consumer string, deliver func(Work) error) (func
|
||||
for k := range msg.Headers() {
|
||||
headers[k] = msg.Headers().Get(k)
|
||||
}
|
||||
if err := deliver(Work{Worker: w.Consumer, Subject: msg.Subject(), Body: json.RawMessage(msg.Data()), Headers: headers}); err != nil {
|
||||
_ = msg.NakWithDelay(NakDelay)
|
||||
// Kept alive while the bundle works on it, however long that takes: the ack wait is for a holder
|
||||
// that died, not one that is slow.
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
tick := time.NewTicker(WorkHeartbeat)
|
||||
defer tick.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-tick.C:
|
||||
_ = msg.InProgress()
|
||||
}
|
||||
}
|
||||
}()
|
||||
err := deliver(Work{Worker: w.Consumer, Subject: msg.Subject(), Body: json.RawMessage(msg.Data()), Headers: headers})
|
||||
close(done)
|
||||
if err != nil {
|
||||
// Offered again later and later, never given up on: a channel that is away keeps its work
|
||||
// (security and correctness reviews of 2026-10-08).
|
||||
delivered := uint64(1)
|
||||
if meta, merr := msg.Metadata(); merr == nil {
|
||||
delivered = meta.NumDelivered
|
||||
}
|
||||
_ = msg.NakWithDelay(WorkBackoff(delivered))
|
||||
return
|
||||
}
|
||||
_ = msg.Ack()
|
||||
})
|
||||
}, jetstream.ConsumeErrHandler(func(_ jetstream.ConsumeContext, err error) {
|
||||
// A worker deleted, or the bus refusing it, is said — never a holder that silently takes nothing.
|
||||
c.Logf("[mesh-tools] %s's worker %s: %v", module, w.Consumer, err)
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading %s's worker %s: %w", module, w.Consumer, err)
|
||||
}
|
||||
@@ -286,6 +337,20 @@ func (c *Conn) SeatRecord(module, bucket, key string) (*StateEntry, error) {
|
||||
if err := checkKey(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if bucket == "" {
|
||||
// The one record its membership lists, when it lists one: an asker reads the router's asks without
|
||||
// knowing the router's name.
|
||||
var named []string
|
||||
for _, r := range t.Records {
|
||||
if b, _, ok := strings.Cut(strings.TrimPrefix(r, "$JS.API.DIRECT.GET.KV_"), "."); ok {
|
||||
named = append(named, b)
|
||||
}
|
||||
}
|
||||
if len(named) != 1 {
|
||||
return nil, fmt.Errorf("%s reads %d records under its name; name the one meant", module, len(named))
|
||||
}
|
||||
bucket = named[0]
|
||||
}
|
||||
subject := "$JS.API.DIRECT.GET.KV_" + bucket + ".$KV." + bucket + "." + module + "." + key
|
||||
if !listed(t.Records, subject) {
|
||||
return nil, fmt.Errorf("%s reads no record of %s under its name: its membership lists %s (novox/hq ADR 0259)",
|
||||
@@ -319,6 +384,10 @@ func (c *Conn) StateCreate(module, name, key string, value json.RawMessage) (uin
|
||||
// StateUpdate writes one key only when its revision is still the one given, and answers the new revision;
|
||||
// a key changed meanwhile is refused, so of two writers that read one value only the first writes.
|
||||
func (c *Conn) StateUpdate(module, name, key string, value json.RawMessage, revision uint64) (uint64, error) {
|
||||
if revision == 0 {
|
||||
// Revision zero would be read by the server as "the key is new": an update names the value it read.
|
||||
return 0, fmt.Errorf("an update names the revision it read; zero is none — create the key instead")
|
||||
}
|
||||
return c.stateWrite(module, name, key, value, func(ctx context.Context, kv jetstream.KeyValue) (uint64, error) {
|
||||
return kv.Update(ctx, key, value, revision)
|
||||
})
|
||||
@@ -327,6 +396,13 @@ func (c *Conn) StateUpdate(module, name, key string, value json.RawMessage, revi
|
||||
// ErrStateChanged is a compare-and-set that lost: the key was made or changed by another writer first.
|
||||
var ErrStateChanged = errors.New("the key was written by another writer first")
|
||||
|
||||
// changed is ErrStateChanged naming the key, with the code a bundle is answered it by.
|
||||
type changed struct{ key string }
|
||||
|
||||
func (e changed) Error() string { return e.key + ": " + ErrStateChanged.Error() }
|
||||
func (e changed) Is(target error) bool { return target == ErrStateChanged }
|
||||
func (e changed) ErrorCode() int { return -32010 }
|
||||
|
||||
func (c *Conn) stateWrite(module, name, key string, value json.RawMessage,
|
||||
write func(context.Context, jetstream.KeyValue) (uint64, error)) (uint64, error) {
|
||||
s, err := c.writable(module, name)
|
||||
@@ -351,7 +427,7 @@ func (c *Conn) stateWrite(module, name, key string, value json.RawMessage,
|
||||
}
|
||||
rev, err := write(ctx, kv)
|
||||
if errors.Is(err, jetstream.ErrKeyExists) || isWrongSequence(err) {
|
||||
return 0, fmt.Errorf("%s.%s: %w", name, key, ErrStateChanged)
|
||||
return 0, changed{name + "." + key}
|
||||
}
|
||||
return rev, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user