views: an owner's changes are ordinary events on the shared events stream, heard on the reader's one durable consumer; Follow takes no stream, Other takes the rest (ADR 0635 §5, D-25; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
+53
-40
@@ -21,7 +21,6 @@ import (
|
||||
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
)
|
||||
|
||||
@@ -43,10 +42,14 @@ type ProcessReader struct {
|
||||
Bound time.Duration
|
||||
Wait time.Duration
|
||||
|
||||
mu sync.Mutex
|
||||
pics map[string]*Picture // by bucket name <owner>_<state>
|
||||
order []string
|
||||
streams map[string]string // each owner's views stream, by owner
|
||||
// Other is told of each message of the module's durable consumer on the shared events stream that is not an owner's
|
||||
// views event, so a module that hears other events through the same consumer (a module has one) handles them there.
|
||||
// Nil says them in the log, acknowledged: the module hears views events alone.
|
||||
Other func(jetstream.Msg)
|
||||
|
||||
mu sync.Mutex
|
||||
pics map[string]*Picture // by bucket name <owner>_<state>
|
||||
order []string
|
||||
}
|
||||
|
||||
func (r *ProcessReader) logf(format string, args ...any) {
|
||||
@@ -55,23 +58,19 @@ func (r *ProcessReader) logf(format string, args ...any) {
|
||||
}
|
||||
}
|
||||
|
||||
// Follow adds the owner's state to follow, as the owner declares them (design 25 § 12.2): tool is its ordinary read tool
|
||||
// of state as the reader's `invokes` names it (`<owner>.<tool>`, from the owner's manifest's tools), stream its views
|
||||
// family as the owner's manifest's `streams` names it (its bus name is catalogue.StreamBusName's). views names no owner:
|
||||
// the caller gives both. An owner has one views stream; a second name for it is refused. Call it before Start.
|
||||
func (r *ProcessReader) Follow(owner, state, tool, stream string) (*Picture, error) {
|
||||
// Follow adds the owner's state to follow, as the owner declares it (design 25 § 12.2): tool is its ordinary read tool
|
||||
// of state as the reader's `invokes` names it (`<owner>.<tool>`, from the owner's manifest's tools). The owner's changes
|
||||
// are ordinary events (`<owner>.views.<state>`), which the reader's manifest `consumes` one entry per followed state.
|
||||
// views names no owner: the caller gives it. Call it before Start.
|
||||
func (r *ProcessReader) Follow(owner, state, tool string) (*Picture, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if owner == "" || state == "" || !strings.HasPrefix(tool, owner+".") || stream == "" {
|
||||
return nil, fmt.Errorf("%s follows %s's %s by its read tool <owner>.<tool> and its views stream, not %q and %q", r.Module, owner, state, tool, stream)
|
||||
}
|
||||
if have, ok := r.streams[owner]; ok && have != stream {
|
||||
return nil, fmt.Errorf("%s's views stream is %s, not %s", owner, have, stream)
|
||||
if owner == "" || state == "" || !strings.HasPrefix(tool, owner+".") {
|
||||
return nil, fmt.Errorf("%s follows %s's %s by its read tool <owner>.<tool>, not %q", r.Module, owner, state, tool)
|
||||
}
|
||||
if r.pics == nil {
|
||||
r.pics, r.streams = map[string]*Picture{}, map[string]string{}
|
||||
r.pics = map[string]*Picture{}
|
||||
}
|
||||
r.streams[owner] = stream
|
||||
bucket := owner + "_" + state
|
||||
if p := r.pics[bucket]; p != nil {
|
||||
return p, nil
|
||||
@@ -153,16 +152,35 @@ func (r *ProcessReader) finding(s Status, raised bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// Hear is the handler of the owner's views family on the reader's durable consumer (cred.EventConsumer on the owner's
|
||||
// views stream): each event to the follower of its state.
|
||||
func (r *ProcessReader) Hear(ctx context.Context, owner string) func(jetstream.Msg) {
|
||||
fs := map[string]*Follower{}
|
||||
// Hear is the handler of the module's durable consumer on the shared events stream (cred.EventConsumer on
|
||||
// process.EventsStream): an owner's views event (`mesh.mod.<owner>.event.views.<state>`) goes to the follower of that
|
||||
// owner's state, any other message to Other.
|
||||
func (r *ProcessReader) Hear(ctx context.Context) func(jetstream.Msg) {
|
||||
fs := map[string]map[string]*Follower{}
|
||||
for _, f := range r.Followers() {
|
||||
if f.Owner == owner {
|
||||
fs[f.State] = f
|
||||
if fs[f.Owner] == nil {
|
||||
fs[f.Owner] = map[string]*Follower{}
|
||||
}
|
||||
fs[f.Owner][f.State] = f
|
||||
}
|
||||
views := map[string]func(jetstream.Msg){}
|
||||
for owner, m := range fs {
|
||||
views[owner] = Hear(ctx, m, r.logf)
|
||||
}
|
||||
return func(m jetstream.Msg) {
|
||||
if owner, ok := OwnerOfEvent(m.Subject()); ok {
|
||||
if h := views[owner]; h != nil {
|
||||
h(m)
|
||||
return
|
||||
}
|
||||
}
|
||||
if r.Other != nil {
|
||||
r.Other(m)
|
||||
return
|
||||
}
|
||||
r.logf("views: %s is no views event of an owner followed, acknowledged", m.Subject())
|
||||
_ = m.Ack()
|
||||
}
|
||||
return Hear(ctx, fs, r.logf)
|
||||
}
|
||||
|
||||
// Owners are the owners followed, sorted.
|
||||
@@ -510,23 +528,18 @@ func (e entry) Created() time.Time { return e.at }
|
||||
func (e entry) Delta() uint64 { return 0 }
|
||||
func (e entry) Operation() jetstream.KeyValueOp { return e.op }
|
||||
|
||||
// Start hears each owner's views family on the process's durable consumer of it (its manifest's `consumes`
|
||||
// `<owner>.views.<state>`), keeps each status in the process's state `first-views` and hears the hand asks the tools
|
||||
// bundle's `<reader>_first_view_again` writes there (StatusTools), then asks every first view (Ask). A consumer
|
||||
// the bus does not have yet is said: the picture is then the first view's alone, asked again on a gap or by hand.
|
||||
// Start hears the owners' views events on the process's durable consumer of the shared events stream (its manifest's
|
||||
// `consumes` `<owner>.views.<state>`), keeps each status in the process's state `first-views` and hears the hand asks the
|
||||
// tools bundle's `<reader>_first_view_again` writes there (StatusTools), then asks every first view (Ask). A consumer the
|
||||
// bus does not have yet is said: the picture is then the first view's alone, asked again on a gap or by hand.
|
||||
func (r *ProcessReader) Start(ctx context.Context, js jetstream.JetStream, cred process.Account, status *process.AwaitedBucket) {
|
||||
for _, owner := range r.Owners() {
|
||||
r.mu.Lock()
|
||||
stream := catalogue.StreamBusName(r.streams[owner])
|
||||
r.mu.Unlock()
|
||||
cons, err := cred.EventConsumer(ctx, js, stream)
|
||||
if err == nil {
|
||||
_, err = process.HearEvents(cons, r.Hear(ctx, owner), r.logf)
|
||||
}
|
||||
if err != nil {
|
||||
r.logf("the consumer %s on %s is not heard (%v): %s's states are the first view's alone, asked again by hand, until the %s starts again",
|
||||
cred.EventConsumerName(), stream, err, owner, r.Module)
|
||||
}
|
||||
cons, err := cred.EventConsumer(ctx, js, process.EventsStream)
|
||||
if err == nil {
|
||||
_, err = process.HearEvents(cons, r.Hear(ctx), r.logf)
|
||||
}
|
||||
if err != nil {
|
||||
r.logf("the consumer %s on %s is not heard (%v): the states followed are the first views' alone, asked again by hand, until the %s starts again",
|
||||
cred.EventConsumerName(), process.EventsStream, err, r.Module)
|
||||
}
|
||||
var said func(Status)
|
||||
if status != nil {
|
||||
|
||||
Reference in New Issue
Block a user