0.1.13: one rule for taking an event, and no handler left by a refused subscribe (hq issue 276)

A handler takes an event by returning and asks for it again by throwing;
any mesh/event answer but an error is taken. Written down in the README
where event handling is, with what follows for a handler: throw only when
the work was not done, and be safe to run twice.

Both SDKs added the handler before asking the runtime and kept it when the
runtime refused, so a module that asks again until it is taken (every Go
consumer does) ran its handler once per attempt for every event. A refused
subscribe now removes its handler; one the Go runtime did not answer in
time is kept, since the runtime may have bound it.
This commit is contained in:
jochen
2026-10-06 18:14:58 +02:00
parent a5fc8308d4
commit ec80dcfc22
10 changed files with 223 additions and 11 deletions
+38 -5
View File
@@ -60,11 +60,14 @@ type channel struct {
next int
waiting map[string]chan message
listeners []listener
// subscribed numbers the listeners, so one whose subscribe failed is the one removed.
subscribed int
// watches are this bundle's state watches by the id each was asked with (ADR 0201).
watches map[string]func(StateChange) error
}
type listener struct {
n int
pattern string
handle func(Envelope) error
}
@@ -108,7 +111,7 @@ func (c *channel) askAs(id, method string, params any, timeout time.Duration) (j
select {
case m := <-answer:
if m.Error != nil {
return nil, errors.New(m.Error.Message)
return nil, &refused{m.Error.Message}
}
return m.Result, nil
case <-time.After(timeout):
@@ -119,6 +122,11 @@ func (c *channel) askAs(id, method string, params any, timeout time.Duration) (j
}
}
// refused is the runtime's error answer, as opposed to no answer: what it refused, it did not do.
type refused struct{ message string }
func (r *refused) Error() string { return r.message }
var (
current *channel
currentMu sync.Mutex
@@ -287,18 +295,43 @@ func live() (*channel, error) {
}
// Subscribe asks the runtime for this module's events whose type matches the pattern (`*` one
// segment, `#` any), and hands each to handle (ADR 0198). The runtime acknowledges an event on the bus
// only after every matching handler returned nil; an error has it delivered again. Call it from a
// goroutine started before Serve, or from a tool, once Serve runs.
// segment, `#` any), and hands each to handle (ADR 0198).
//
// **Taking an event** (see the repository's README): handle returning nil takes the event — the
// runtime acknowledges it once every matching handler did; an error is the one way to have it offered
// again, after a few seconds and a bounded number of times, after which the bus gives up and the
// controller raises a condition. So return an error only when the work was not done, never for an
// answer that went right but read oddly; and since delivery is at-least-once, make the work safe to
// do twice, or skip an event whose x-event-id was already done.
//
// A subscribe the runtime refused leaves no handler behind, so asking again — as a module does until
// the runtime takes it — does not hand one event to the same handler twice. (One the runtime did not
// answer in time is kept, as the runtime may have bound it.) Call it from a goroutine
// started before Serve, or from a tool, once Serve runs.
func Subscribe(pattern string, handle func(Envelope) error) error {
c, err := live()
if err != nil {
return err
}
c.mu.Lock()
c.listeners = append(c.listeners, listener{pattern: pattern, handle: handle})
c.subscribed++
n := c.subscribed
c.listeners = append(c.listeners, listener{n: n, pattern: pattern, handle: handle})
c.mu.Unlock()
_, err = c.ask("mesh/subscribe", map[string]any{"pattern": pattern}, 30*time.Second)
// Removed only when the runtime answered that it refused: one that did not answer in time may
// have bound the consumer, and a handler removed then would have its events taken unhandled.
var no *refused
if errors.As(err, &no) {
c.mu.Lock()
for i, l := range c.listeners {
if l.n == n {
c.listeners = append(c.listeners[:i:i], c.listeners[i+1:]...)
break
}
}
c.mu.Unlock()
}
return err
}