Files
mesh-sdk/go/views/process_reader.go
T
2026-11-12 06:11:26 +01:00

894 lines
30 KiB
Go

package views
// A process's pictures of the owners' states it used to read (novox/hq ADR 0615 §1, design 25 § 12.2), for the parts
// that run as processes: each state is asked once through the owner's ordinary read tool with `values` (the first
// view), then kept from the owner's views family, asked again only on a gap or by hand (Follower).
//
// A Picture reads as the key-value bucket the process used to bind (jetstream.KeyValue: Get, Keys, ListKeys, Watch,
// WatchAll), so a reader's code is unchanged; every write is refused, for the owner's state is the owner's alone. The
// picture is kept in memory: the owner's state is the truth and a restart asks again (ADR 0615 §4). ProcessReader is
// the process side of Reader (the stdio side).
import (
"context"
"encoding/json"
"errors"
"fmt"
"maps"
"slices"
"strings"
"sync"
"time"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/process"
)
// ErrReadOnly is every write to a picture: the state is its owner's.
var ErrReadOnly = errors.New("a first view is read-only: the state is its owner's")
// ProcessReader is one process's pictures of its owners' states.
type ProcessReader struct {
// Module is the reading module, named in the finding of a first view unanswered past its bound.
Module string
// Call asks a module's tool (process.Conn.Call): the read tools are asked with `values`.
Call func(key string, body any) (json.RawMessage, error)
// Findings is the reader's own state `findings`, where the condition keeper raises and clears the condition of a
// first view unanswered past its bound (ADR 0615 §6); nil says it in the log only.
Findings func() WireWriter
Logf func(string, ...any)
// Now, Bound and Wait are for tests: zero are the clock, Bound and five seconds.
Now func() time.Time
Bound time.Duration
Wait time.Duration
// 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)
// Hub is the module's one pull of its durable consumer (process.EventHub), when other parts of the module hear events
// too: the reader registers its views events on it and does not pull; the host starts the hub once, after every part
// registered. Nil: the reader pulls the durable itself and takes every message (Other the rest), for a module that
// hears nothing else.
Hub *process.EventHub
mu sync.Mutex
pics map[string]*Picture // by bucket name <owner>_<state>
order []string
// adopted are the handlers of the adopted followers' seat events (Adopt), by the event's subject.
// Several followers adopt one event (two branches' views of the catalog half), each hearing it (design 47 G §532).
adopted map[string][]adoptedHear
// After Start and Ask (design 47 G §532): the hub the reader's events come through when it is the module's (Hub) and
// its handler, the time the bus made the consumer, the asks' life and Said, and of each first view begun, the cancel
// of its asks, by bucket; status is the state `first-views` each status is kept in.
hub *process.EventHub
handle func(jetstream.Msg)
created time.Time
ctx context.Context
said func(Status)
asked bool
begun map[string]context.CancelFunc
status *process.AwaitedBucket
}
// adoptedHear is one adopted follower's handler of its seat event, by its picture's bucket.
type adoptedHear struct {
bucket string
hear func(context.Context, []byte) error
}
func (r *ProcessReader) logf(format string, args ...any) {
if r.Logf != nil {
r.Logf(format, args...)
}
}
// 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. Called after Start, the view takes its first view at once and then follows
// (design 47 G §532).
func (r *ProcessReader) Follow(owner, state, tool string) (*Picture, error) {
p, fresh, err := r.follow(owner, state, tool)
if err == nil && fresh {
r.begin(p.bucket)
}
return p, err
}
func (r *ProcessReader) follow(owner, state, tool string) (*Picture, bool, error) {
r.mu.Lock()
defer r.mu.Unlock()
if owner == "" || state == "" || !strings.HasPrefix(tool, owner+".") {
return nil, false, 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 = map[string]*Picture{}
}
bucket := owner + "_" + state
if p := r.pics[bucket]; p != nil {
return p, false, nil
}
p := &Picture{bucket: bucket, ctx: context.Background()}
call := r.Call
ask := AskVia(func(key string, body any) (json.RawMessage, error) {
m, _ := body.(map[string]any)
if m == nil {
m = map[string]any{}
}
m["values"] = true
if call == nil {
return nil, errors.New("this reader asks no tool")
}
return call(key, m)
}, tool)
p.f = &Follower{Owner: owner, Tool: tool, State: state, Ask: ask, Logf: r.Logf, Now: r.Now, Bound: r.Bound, Wait: r.Wait,
Raise: func(s Status) {
if r.holds(p) {
r.finding(s, true)
}
}, Clear: func(s Status) { r.finding(s, false) },
Changed: p.changed}
r.pics[bucket] = p
r.order = append(r.order, bucket)
return p, true, nil
}
// holds says whether p is still a picture of the reader: a dropped view writes no finding after its drop cleared them.
func (r *ProcessReader) holds(p *Picture) bool {
r.mu.Lock()
defer r.mu.Unlock()
return r.pics[p.bucket] == p
}
// FollowBranch is Follow for a state kept per branch: the view of one branch is named `<owner>.<state>.<branch>` (the
// branch escaped as a key's name is, BranchState) and its picture `<owner>_<state>.<branch>`, so a process follows
// several branches of one state, each its own view (design 47 G §479). Called after Start, it begins at once (§532).
func (r *ProcessReader) FollowBranch(owner, state, branch, tool string) (*Picture, error) {
return r.Follow(owner, BranchState(state, branch), tool)
}
// Adopt takes a follower a seat's client made (its Ask a seat's verb, such as the catalog half's view) as one of the
// process's first views, so the client's callers write no glue: the reader sets its conditions (the unanswered first
// view and view-waits, in the reader's findings), its log and clock, and its picture (`<owner>_<state>`) hands each
// change on before the follower's own Changed, set before. Start keeps its status, hears its hand asks, and asks it with
// the others; its events are the seat's event (seatdef's EventSubject of seat and event, on the module's durable
// consumer, as its `consumes` names it), handed to hear, the client's handler, and acknowledged; a state followed already
// is refused. Adopted after Start, as a part adopts a watched branch added while it runs, the view takes its first view
// at once and then follows (design 47 G §532).
func (r *ProcessReader) Adopt(f *Follower, seat, event string, hear func(context.Context, []byte) error) (*Picture, error) {
if f == nil || f.Owner == "" || f.State == "" || seat == "" || event == "" || hear == nil {
return nil, errors.New("views: an adopted follower names its owner and state, and its seat's event and handler")
}
p, subject, err := r.adopt(f, seat, event, hear)
if err != nil {
return nil, err
}
r.mu.Lock()
hub, handle := r.hub, r.handle
r.mu.Unlock()
if subject != "" && hub != nil {
// one subscription per event, with every further part (design 47 G §532)
if err := hub.Handle(subject+".>", handle); err != nil {
r.logf("views: %s's event %s is not heard (%v): the view %s is its first view's alone, asked again by hand", r.Module, subject, err, p.bucket)
}
}
r.begin(p.bucket)
return p, nil
}
// AdoptEvent hears a seat's event on the one subscription the followers adopted on that event share, with no picture and no
// first view (design 47 G A-185, A-199): hear is handed each message of the event beside every adopted follower's handler,
// and the message is acknowledged, unless hear fails: then it is left unacknowledged and the bus asks again (A-222). The subscription is made at once when no follower made it, so a part that follows no
// view of the seat still hears the event; adopted before Start, it is made at Start.
func (r *ProcessReader) AdoptEvent(seat, event string, hear func(context.Context, []byte) error) error {
if seat == "" || event == "" || hear == nil {
return errors.New("views: an adopted event names its seat, its event and its handler")
}
subject := "mesh.seat." + seat + ".event." + event
r.mu.Lock()
if r.adopted == nil {
r.adopted = map[string][]adoptedHear{}
}
fresh := len(r.adopted[subject]) == 0
r.adopted[subject] = append(r.adopted[subject], adoptedHear{hear: hear})
hub, handle := r.hub, r.handle
r.mu.Unlock()
if fresh && hub != nil {
// one subscription per event, with every further part (design 47 G §532)
if err := hub.Handle(subject+".>", handle); err != nil {
r.logf("views: %s's event %s is not heard (%v)", r.Module, subject, err)
}
}
return nil
}
// adopt is Adopt's part under the lock; subject is the event's subject when no follower adopted it before.
func (r *ProcessReader) adopt(f *Follower, seat, event string, hear func(context.Context, []byte) error) (*Picture, string, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.pics == nil {
r.pics = map[string]*Picture{}
}
bucket := f.Owner + "_" + f.State
if r.pics[bucket] != nil {
return nil, "", fmt.Errorf("views: %s's %s is followed already", f.Owner, f.State)
}
p := &Picture{bucket: bucket, ctx: context.Background(), f: f}
if f.Logf == nil {
f.Logf = r.Logf
}
if f.Now == nil {
f.Now = r.Now
}
if f.Bound == 0 {
f.Bound = r.Bound
}
if f.Wait == 0 {
f.Wait = r.Wait
}
f.Raise, f.Clear = func(s Status) {
if r.holds(p) {
r.finding(s, true)
}
}, func(s Status) { r.finding(s, false) }
f.RaiseWaits, f.ClearWaits = func(s Status) {
if r.holds(p) {
r.waits(s, true)
}
}, func(s Status) { r.waits(s, false) }
own := f.Changed
f.Changed = func(key string, value json.RawMessage, deleted bool, at time.Time) {
p.changed(key, value, deleted, at)
if own != nil {
own(key, value, deleted, at)
}
}
r.pics[bucket] = p
r.order = append(r.order, bucket)
if r.adopted == nil {
r.adopted = map[string][]adoptedHear{}
}
subject := "mesh.seat." + seat + ".event." + event
fresh := len(r.adopted[subject]) == 0
r.adopted[subject] = append(r.adopted[subject], adoptedHear{bucket: bucket, hear: hear})
if !fresh {
subject = ""
}
return p, subject, nil
}
// waits writes (raised) or clears the reader's view-waits finding of s (design 47 *A part with no view says it waits*).
func (r *ProcessReader) waits(s Status, raised bool) {
if raised {
r.logf("%s has waited for %s's view %s since %s", r.Module, s.Owner, s.WaitsFor, s.WaitingSince.Format(time.RFC3339))
}
if r.Findings == nil {
return
}
kv := r.Findings()
if kv == nil {
return
}
at := time.Now()
if r.Now != nil {
at = r.Now()
}
f := viewWaitsFinding(r.Module, s, at)
var err error
if raised {
err = f.put(kv)
} else {
err = f.drop(kv)
}
if err != nil {
r.logf("views: the view-waits finding of %s is not written: %v", s.Tool, err)
}
}
// Picture is the picture of the bucket the process used to bind, by its bus name (`<owner>_<state>`); nil when not
// followed.
func (r *ProcessReader) Picture(bucket string) *Picture {
r.mu.Lock()
defer r.mu.Unlock()
return r.pics[bucket]
}
// KeyValue opens a followed state's picture by its bus name, where a process opened the bucket (process.BucketOpener,
// as buildrun.FollowPlacement takes it); any other bucket is refused, for the reader reads its owners' states by their
// first views alone.
func (r *ProcessReader) KeyValue(_ context.Context, bucket string) (jetstream.KeyValue, error) {
if p := r.Picture(bucket); p != nil {
return p, nil
}
return nil, fmt.Errorf("%s reads %s by its owner's first view alone, and does not follow it", r.Module, bucket)
}
// Followers are the followers, in the order they were added.
func (r *ProcessReader) Followers() []*Follower {
r.mu.Lock()
defer r.mu.Unlock()
out := make([]*Follower, 0, len(r.order))
for _, b := range r.order {
out = append(out, r.pics[b].f)
}
return out
}
func (r *ProcessReader) finding(s Status, raised bool) {
if raised {
r.logf("the first view %s of %s's %s is unanswered past its bound: %s", s.Tool, s.Owner, s.State, s.Error)
}
if r.Findings == nil {
return
}
kv := r.Findings()
if kv == nil {
return
}
at := time.Now()
if r.Now != nil {
at = r.Now()
}
f := unansweredFinding(r.Module, s, at)
var err error
if raised {
err = f.put(kv)
} else {
err = f.drop(kv)
}
if err != nil {
r.logf("views: the finding of the first view %s is not written: %v", s.Tool, err)
}
}
// 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. The followers are read at each message, so a view followed or adopted
// after Start hears its events and a dropped one no longer does (design 47 G §532).
func (r *ProcessReader) Hear(ctx context.Context) func(jetstream.Msg) {
return func(m jetstream.Msg) {
r.mu.Lock()
hears := adoptedHears(r.adopted, m.Subject())
var owned map[string]*Follower
if owner, ok := OwnerOfEvent(m.Subject()); ok && hears == nil {
for _, b := range r.order {
if f := r.pics[b].f; f.Owner == owner {
if owned == nil {
owned = map[string]*Follower{}
}
owned[f.State] = f
}
}
}
r.mu.Unlock()
if hears != nil {
failed := false
for _, h := range hears {
if err := h.hear(ctx, m.Data()); err != nil {
r.logf("views: %s is not taken: %v", m.Subject(), err)
// an event adopted with no picture is asked again by the bus (design 47 G A-222)
failed = failed || h.bucket == ""
}
}
if !failed {
_ = m.Ack()
}
return
}
if owned != nil {
Hear(ctx, owned, r.logf)(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()
}
}
// Owners are the owners followed, sorted.
func (r *ProcessReader) Owners() []string {
var out []string
for _, f := range r.Followers() {
if !slices.Contains(out, f.Owner) {
out = append(out, f.Owner)
}
}
slices.Sort(out)
return out
}
// Ask asks every first view until answered, each on a goroutine of its own, so the process starts at once and waits for
// an owner that is down, saying so (ADR 0615 §6). Said, when set, keeps each status (KeepStatus). A view followed or
// adopted after it is asked at once (design 47 G §532).
func (r *ProcessReader) Ask(ctx context.Context, said func(Status)) {
r.mu.Lock()
r.ctx, r.said, r.asked = ctx, said, true
buckets := slices.Clone(r.order)
r.mu.Unlock()
for _, b := range buckets {
r.begin(b)
}
}
// begin asks bucket's first view once Ask ran, on a goroutine of its own, until answered or the view is dropped.
func (r *ProcessReader) begin(bucket string) {
r.mu.Lock()
p := r.pics[bucket]
if !r.asked || p == nil || r.begun[bucket] != nil {
r.mu.Unlock()
return
}
if r.begun == nil {
r.begun = map[string]context.CancelFunc{}
}
ctx, cancel := context.WithCancel(r.ctx)
r.begun[bucket] = cancel
f, said, created := p.f, r.said, r.created
r.mu.Unlock()
if said != nil {
f.Said = func(s Status) {
if r.holds(p) {
said(s)
}
}
}
if !created.IsZero() {
f.ConsumerMade(ctx, created)
}
go func() {
if err := f.Start(ctx); err != nil && ctx.Err() == nil {
r.logf("the first view %s was not answered: %v", f.Tool, err)
}
}()
}
// Drop ends the view of owner's state, as a part drops a watched branch removed while it runs (design 47 G §532, §614):
// it asks it no more and hears none of its events, its status leaves the state `first-views` and so its row leaves
// `<module>_first_views`, and every finding it raised (its unanswered first view, its view-waits) is cleared through
// the reader's findings, the drop and each cleared finding said in the log. A state not followed is an error.
func (r *ProcessReader) Drop(owner, state string) error {
bucket := owner + "_" + state
r.mu.Lock()
p := r.pics[bucket]
if p == nil {
r.mu.Unlock()
return fmt.Errorf("%s does not follow %s's %s", r.Module, owner, state)
}
cancel := r.begun[bucket]
delete(r.pics, bucket)
delete(r.begun, bucket)
r.order = slices.DeleteFunc(r.order, func(b string) bool { return b == bucket })
for subject, hs := range r.adopted {
if hs = slices.DeleteFunc(hs, func(h adoptedHear) bool { return h.bucket == bucket }); len(hs) == 0 {
delete(r.adopted, subject)
} else {
r.adopted[subject] = hs
}
}
status := r.status
r.mu.Unlock()
if cancel != nil {
cancel()
}
r.logf("views: %s drops its view %s.%s", r.Module, owner, state)
if status != nil {
if kv := status.KV(); kv != nil {
if err := kv.Delete(context.Background(), StatusKey(owner, state)); err != nil {
r.logf("views: the status of the dropped view %s.%s is not removed from %s: %v", owner, state, StatusState, err)
}
}
}
s := p.f.Status()
if s.Raised {
r.finding(s, false)
r.logf("views: %s clears its finding %s of the dropped view %s.%s", r.Module, UnansweredKind, owner, state)
}
if s.WaitsRaised {
r.waits(s, false)
r.logf("views: %s clears its finding %s of the dropped view %s.%s", r.Module, ViewWaitsKind, owner, state)
}
return nil
}
// Ready is closed once every followed state had its first view.
func (r *ProcessReader) Ready() <-chan struct{} {
fs := r.Followers()
done := make(chan struct{})
go func() {
for _, f := range fs {
<-f.Ready()
}
close(done)
}()
return done
}
// Picture is one owner's state as the process reads it: a read-only jetstream.KeyValue over a Follower. Its
// revisions are the picture's own, one per change it took, so a watcher sees them rise; they are not the owner's. An
// entry's Created is when the owner's bus wrote the key (Follower.Changed), zero when the owner did not say it.
type Picture struct {
jetstream.KeyValue // never set: every method a reader may call is the picture's own
bucket string
ctx context.Context
f *Follower
mu sync.Mutex
seq uint64
revs map[string]uint64
ats map[string]time.Time // when the owner's bus wrote each key, as the follower handed it (zero unsaid)
watchers []*watcher
}
// Follower is the picture's follower (its status, a gap or a hand ask).
func (p *Picture) Follower() *Follower { return p.f }
func (p *Picture) changed(key string, value json.RawMessage, deleted bool, at time.Time) {
p.mu.Lock()
p.seq++
if p.revs == nil {
p.revs = map[string]uint64{}
}
p.revs[key] = p.seq
if p.ats == nil {
p.ats = map[string]time.Time{}
}
p.ats[key] = at
op := jetstream.KeyValuePut
if deleted {
op, value = jetstream.KeyValueDelete, nil
}
e := entry{bucket: p.bucket, key: key, value: value, rev: p.seq, at: at, op: op}
ws := slices.Clone(p.watchers)
p.mu.Unlock()
for _, w := range ws {
w.send(e)
}
}
// answered is nil once the first view is answered, else the error a read before it gets (never a wait).
func (p *Picture) answered() error {
select {
case <-p.f.Ready():
return nil
default:
return fmt.Errorf("no first view of %s yet: %s has not answered", p.bucket, p.f.Tool)
}
}
func (p *Picture) ready(ctx context.Context) error {
select {
case <-p.f.Ready():
return nil
case <-ctx.Done():
return fmt.Errorf("no first view of %s yet (%s has not answered): %w", p.bucket, p.f.Tool, ctx.Err())
}
}
// Bucket is the bus name of the owner's state the picture stands for.
func (p *Picture) Bucket() string { return p.bucket }
// Get is one key; before the first view is answered it is an error, never a wait.
func (p *Picture) Get(_ context.Context, key string) (jetstream.KeyValueEntry, error) {
if err := p.answered(); err != nil {
return nil, err
}
v, ok := p.f.Get(key)
if !ok {
return nil, jetstream.ErrKeyNotFound
}
p.mu.Lock()
rev, at := p.revs[key], p.ats[key]
p.mu.Unlock()
return entry{bucket: p.bucket, key: key, value: v, rev: rev, at: at, op: jetstream.KeyValuePut}, nil
}
// Keys is every key; none is jetstream.ErrNoKeysFound, as the bus says it; before the first view is answered it is an
// error, never a wait.
func (p *Picture) Keys(_ context.Context, _ ...jetstream.WatchOpt) ([]string, error) {
if err := p.answered(); err != nil {
return nil, err
}
var keys []string
for k := range p.f.Values() {
keys = append(keys, k)
}
if len(keys) == 0 {
return nil, jetstream.ErrNoKeysFound
}
slices.Sort(keys)
return keys, nil
}
// ListKeys is Keys on a channel.
func (p *Picture) ListKeys(ctx context.Context, opts ...jetstream.WatchOpt) (jetstream.KeyLister, error) {
keys, err := p.Keys(ctx, opts...)
if err != nil && !errors.Is(err, jetstream.ErrNoKeysFound) {
return nil, err
}
ch := make(chan string, len(keys))
for _, k := range keys {
ch <- k
}
close(ch)
return lister{ch}, nil
}
// WatchAll is Watch of every key.
func (p *Picture) WatchAll(ctx context.Context, opts ...jetstream.WatchOpt) (jetstream.KeyWatcher, error) {
return p.Watch(ctx, ">", opts...)
}
// Watch hands, once the first view is answered, every current value of the keys matching (a subject pattern: `*` one
// token, `>` the rest), then nil, then each change, as the bus's watch does. Options are not read: a picture holds no
// history, and deletes are always handed.
func (p *Picture) Watch(ctx context.Context, keys string, _ ...jetstream.WatchOpt) (jetstream.KeyWatcher, error) {
w := &watcher{ch: make(chan jetstream.KeyValueEntry, 64), match: keys, wake: make(chan struct{}, 1), stop: make(chan struct{})}
go func() {
defer close(w.ch)
if p.ready(ctx) != nil {
return
}
p.mu.Lock()
p.watchers = append(p.watchers, w)
p.mu.Unlock()
defer p.unwatch(w)
vals := p.f.Values()
names := make([]string, 0, len(vals))
for k := range vals {
names = append(names, k)
}
slices.Sort(names)
p.mu.Lock()
first := make([]jetstream.KeyValueEntry, 0, len(names)+1)
for _, k := range names {
if matches(keys, k) {
first = append(first, entry{bucket: p.bucket, key: k, value: vals[k], rev: p.revs[k], at: p.ats[k], op: jetstream.KeyValuePut})
}
}
p.mu.Unlock()
first = append(first, nil)
w.run(ctx, first)
}()
return w, nil
}
func (p *Picture) unwatch(w *watcher) {
p.mu.Lock()
defer p.mu.Unlock()
p.watchers = slices.DeleteFunc(p.watchers, func(x *watcher) bool { return x == w })
}
// The writes and the reads a picture cannot answer are refused, never handed to a bucket.
func (p *Picture) Put(context.Context, string, []byte) (uint64, error) { return 0, ErrReadOnly }
func (p *Picture) PutString(context.Context, string, string) (uint64, error) { return 0, ErrReadOnly }
func (p *Picture) Create(context.Context, string, []byte, ...jetstream.KVCreateOpt) (uint64, error) {
return 0, ErrReadOnly
}
func (p *Picture) Update(context.Context, string, []byte, uint64) (uint64, error) {
return 0, ErrReadOnly
}
func (p *Picture) Delete(context.Context, string, ...jetstream.KVDeleteOpt) error { return ErrReadOnly }
func (p *Picture) Purge(context.Context, string, ...jetstream.KVDeleteOpt) error { return ErrReadOnly }
func (p *Picture) PurgeDeletes(context.Context, ...jetstream.KVPurgeOpt) error { return ErrReadOnly }
func (p *Picture) GetRevision(context.Context, string, uint64) (jetstream.KeyValueEntry, error) {
return nil, errors.New("a first view holds no history")
}
func (p *Picture) History(context.Context, string, ...jetstream.WatchOpt) ([]jetstream.KeyValueEntry, error) {
return nil, errors.New("a first view holds no history")
}
func (p *Picture) WatchFiltered(ctx context.Context, keys []string, opts ...jetstream.WatchOpt) (jetstream.KeyWatcher, error) {
if len(keys) != 1 {
return nil, errors.New("a first view watches one pattern")
}
return p.Watch(ctx, keys[0], opts...)
}
func (p *Picture) ListKeysFiltered(ctx context.Context, filters ...string) (jetstream.KeyLister, error) {
keys, err := p.Keys(ctx)
if err != nil && !errors.Is(err, jetstream.ErrNoKeysFound) {
return nil, err
}
ch := make(chan string, len(keys))
for _, k := range keys {
if len(filters) == 0 || slices.ContainsFunc(filters, func(f string) bool { return matches(f, k) }) {
ch <- k
}
}
close(ch)
return lister{ch}, nil
}
func (p *Picture) Status(context.Context) (jetstream.KeyValueStatus, error) {
return nil, errors.New("a first view has no bucket status: the owner's read tool says its fill")
}
// matches says whether key matches a subject pattern of the bus's watch.
func matches(pattern, key string) bool {
if pattern == "" || pattern == ">" {
return true
}
pt, kt := strings.Split(pattern, "."), strings.Split(key, ".")
for i, t := range pt {
if t == ">" {
return len(kt) > i
}
if i >= len(kt) || (t != "*" && t != kt[i]) {
return false
}
}
return len(pt) == len(kt)
}
// watcher hands a watch's entries in order: the current values first, then each change heard, never blocking the
// follower (the changes wait in a queue of its own).
type watcher struct {
ch chan jetstream.KeyValueEntry
match string
wake chan struct{}
stop chan struct{}
once sync.Once
mu sync.Mutex
queue []jetstream.KeyValueEntry
}
func (w *watcher) send(e entry) {
if !matches(w.match, e.key) {
return
}
w.mu.Lock()
w.queue = append(w.queue, e)
w.mu.Unlock()
select {
case w.wake <- struct{}{}:
default:
}
}
func (w *watcher) run(ctx context.Context, first []jetstream.KeyValueEntry) {
out := func(e jetstream.KeyValueEntry) bool {
select {
case w.ch <- e:
return true
case <-ctx.Done():
return false
case <-w.stop:
return false
}
}
for _, e := range first {
if !out(e) {
return
}
}
for {
w.mu.Lock()
q := w.queue
w.queue = nil
w.mu.Unlock()
for _, e := range q {
if !out(e) {
return
}
}
select {
case <-w.wake:
case <-ctx.Done():
return
case <-w.stop:
return
}
}
}
func (w *watcher) Updates() <-chan jetstream.KeyValueEntry { return w.ch }
func (w *watcher) Stop() error {
w.once.Do(func() { close(w.stop) })
return nil
}
type lister struct{ ch chan string }
func (l lister) Keys() <-chan string { return l.ch }
func (l lister) Stop() error { return nil }
type entry struct {
bucket, key string
value []byte
rev uint64
at time.Time
op jetstream.KeyValueOp
}
func (e entry) Bucket() string { return e.bucket }
func (e entry) Key() string { return e.key }
func (e entry) Value() []byte { return e.value }
func (e entry) Revision() uint64 { return e.rev }
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 }
// EventsPattern is the subjects of every owner's views events on the shared events stream.
const EventsPattern = "mesh.mod.*.event." + EventPrefix + "*"
// 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) {
hub, pattern, own := r.Hub, EventsPattern, false
if hub == nil {
hub, pattern, own = process.NewEventHub(r.logf), ">", true
}
hub.OnBound(func(created time.Time) {
r.mu.Lock()
r.created = created
r.mu.Unlock()
for _, f := range r.Followers() {
f.ConsumerMade(ctx, created)
}
})
hear := r.Hear(ctx)
err := hub.Handle(pattern, hear)
r.mu.Lock()
r.status = status
if !own {
// an event adopted after the start is handled on the module's hub (Adopt); the reader's own hub takes every one
r.hub, r.handle = hub, hear
}
r.mu.Unlock()
if !own {
r.mu.Lock()
subjects := slices.Sorted(maps.Keys(r.adopted))
r.mu.Unlock()
for _, subject := range subjects {
// one subscription per event, with every further part (design 47 G §532)
if err == nil {
err = hub.Handle(subject+".>", hear)
}
}
}
if err == nil && own {
err = hub.Start(ctx, js, cred)
}
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 {
status.Ready(func(kv jetstream.KeyValue) {
if err := hearAgain(ctx, kv, r.Followers, r.logf); err != nil {
r.logf("views: the hand asks of %s's first views are not heard (%v) until it starts again", r.Module, err)
}
})
said = func(s Status) {
if kv := status.KV(); kv != nil {
KeepStatus(ctx, kv, r.logf)(s)
}
}
}
r.Ask(ctx, said)
}
// adoptedHears are the handlers of every follower adopted on the event whose subject is subject or has further parts
// after it (`<event>.<module>.<branch>.<commit or group>`, design 47 G §521, §532), or nil.
func adoptedHears(adopted map[string][]adoptedHear, subject string) []adoptedHear {
if h := adopted[subject]; h != nil {
return h
}
for i := strings.LastIndex(subject, "."); i > 0; i = strings.LastIndex(subject, ".") {
subject = subject[:i]
if h := adopted[subject]; h != nil {
return h
}
}
return nil
}