F-tools: the runner's views consumer is proven on the real bus; the heartbeat is asked of the bus only when a bundle wants it, with no race; a seat record says when the bus wrote it; the beat test counts ticks, not sleeps
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -157,3 +159,30 @@ func TestALongRunningBundleThatExitsIsStartedAgain(t *testing.T) {
|
||||
|
||||
func fmtSprintf(format string, args ...any) string { return fmt.Sprintf(format, args...) }
|
||||
func errorf(format string, args ...any) error { return fmt.Errorf(format, args...) }
|
||||
|
||||
// A bundle loaded again sets its heartbeat handler while the consumer's loop hands a word on: no data race (-race),
|
||||
// and the heartbeat is asked of the bus only while some bundle holds a handler.
|
||||
func TestAHeartbeatHandlerSetAgainWhileWordsAreHandedOnIsNotARace(t *testing.T) {
|
||||
m := &moduleEvents{}
|
||||
all := &consumers{logf: t.Logf, of: map[string]*moduleEvents{"mod": m}}
|
||||
slot := &beatSlot{}
|
||||
m.beats = []*beatSlot{slot}
|
||||
if m.beatAsked() {
|
||||
t.Fatal("a heartbeat is asked while no bundle gave a handler")
|
||||
}
|
||||
var got atomic.Int32
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
for i := 0; i < 200; i++ {
|
||||
slot.set(func(json.RawMessage) { got.Add(1) })
|
||||
}
|
||||
}()
|
||||
for i := 0; i < 200; i++ {
|
||||
all.handBeat(m, bus.Beat{At: time.Now()})
|
||||
}
|
||||
<-done
|
||||
if !m.beatAsked() {
|
||||
t.Error("a bundle gave a handler and no heartbeat is asked")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,7 +775,36 @@ type consumers struct {
|
||||
type moduleEvents struct {
|
||||
stop func()
|
||||
delivers []*func(json.RawMessage) error
|
||||
beats []*func(json.RawMessage)
|
||||
beats []*beatSlot
|
||||
}
|
||||
|
||||
// beatSlot is where a bundle's heartbeat handler is kept; a bundle loaded again sets it while the consumer's loop
|
||||
// reads it, so both go through its own lock.
|
||||
type beatSlot struct {
|
||||
mu sync.Mutex
|
||||
f func(json.RawMessage)
|
||||
}
|
||||
|
||||
func (s *beatSlot) get() func(json.RawMessage) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.f
|
||||
}
|
||||
|
||||
func (s *beatSlot) set(f func(json.RawMessage)) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.f = f
|
||||
}
|
||||
|
||||
// beatAsked says whether any bundle of the module asked for the heartbeat; the caller holds c.mu.
|
||||
func (m *moduleEvents) beatAsked() bool {
|
||||
for _, t := range m.beats {
|
||||
if t.get() != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// stopAll unbinds every module's consumer.
|
||||
@@ -823,7 +852,7 @@ type moduleBus struct {
|
||||
module string
|
||||
mu sync.Mutex
|
||||
deliver *func(json.RawMessage) error
|
||||
beat *func(json.RawMessage)
|
||||
beat *beatSlot
|
||||
}
|
||||
|
||||
// Publish emits an event as the module (ADR 0193).
|
||||
@@ -871,11 +900,9 @@ func (b *moduleBus) Subscribe(deliver func(json.RawMessage) error, beat func(jso
|
||||
}
|
||||
mine := b.deliver
|
||||
if b.beat == nil {
|
||||
x := beat
|
||||
b.beat = &x
|
||||
} else {
|
||||
*b.beat = beat
|
||||
b.beat = &beatSlot{}
|
||||
}
|
||||
b.beat.set(beat)
|
||||
mineBeat := b.beat
|
||||
b.mu.Unlock()
|
||||
|
||||
@@ -901,8 +928,13 @@ func (b *moduleBus) Subscribe(deliver func(json.RawMessage) error, beat func(jso
|
||||
return nil
|
||||
}
|
||||
module := b.module
|
||||
stop, err := c.conn.ConsumeAsBeating(module, func(env bus.Envelope) error { return c.handOut(module, m, env) },
|
||||
func(b bus.Beat) { c.handBeat(m, b) })
|
||||
// The bus is asked about the consumer only while a bundle of the module asked for the heartbeat.
|
||||
stop, err := c.conn.ConsumeAsBeatingWhen(module, func(env bus.Envelope) error { return c.handOut(module, m, env) },
|
||||
func(b bus.Beat) { c.handBeat(m, b) }, func() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return m.beatAsked()
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -918,10 +950,10 @@ func (c *consumers) handBeat(m *moduleEvents, b bus.Beat) {
|
||||
return
|
||||
}
|
||||
c.mu.Lock()
|
||||
targets := append([](*func(json.RawMessage))(nil), m.beats...)
|
||||
targets := append([]*beatSlot(nil), m.beats...)
|
||||
c.mu.Unlock()
|
||||
for _, t := range targets {
|
||||
if f := *t; f != nil {
|
||||
if f := t.get(); f != nil {
|
||||
f(raw)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user