Files
mesh-delivery/sdk/views/emit.go
T
jschoubben ccfca79c90 DLV-F1r, DLV-F3r (indexer): the indexer reads the placement through the assigner's first view
The indexer no longer reads the assigner's placement bucket (ADR 0615 §1):
it asks mesh-delivery-assigner.placement at its start, waiting at most the
two-minute bound so a normal start computes the fingerprints once, then
follows assigner-views on its durable consumer, asking once more on a gap.
Past the bound it raises its finding naming the assigner and placement,
cleared by the answer. Its status is kept in its state first-views, read
by indexer_first_views; indexer_first_view_again asks again by hand.
sdk/views gains the event's write time, the status hook, and the status
state and tools a reader's bundle serves.

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:22:26 +01:00

197 lines
6.7 KiB
Go

package views
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
)
// Emitter is an owner's process saying each change of its own states on its views family (ADR 0615 §5): it follows
// each state it is given from the revision its mark says it said up to, publishes one Event per change, and writes the
// mark once it has said every change the bus had handed it. A part watches only its own state (ADR 0596 §4): an owner
// follows its own buckets, never another's.
type Emitter struct {
// Owner is the owner module, said in every event.
Owner string
// Subject is where an event of the given local name lands on the owner's own stream (process.Account.StreamSubject).
Subject func(event string) string
// Publish sends one message to the owner's stream, its id the bus's dedup id.
Publish func(ctx context.Context, msg *nats.Msg, id string) error
// Marks is the owner's state MarksState; nil says nothing is marked, and every start says from the bucket's end.
Marks jetstream.KeyValue
// TooLarge is told of a value too large for an event (said as a gap): the owner's finding naming the key. Nil
// only logs it.
TooLarge func(state, key string, size int)
Logf func(string, ...any)
Now func() time.Time
}
// JetStreamPublish is Emitter.Publish through js, with the bus's dedup id.
func JetStreamPublish(js jetstream.JetStream) func(context.Context, *nats.Msg, string) error {
return func(ctx context.Context, msg *nats.Msg, id string) error {
pub, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
defer cancel()
_, err := js.PublishMsg(pub, msg, jetstream.WithMsgID(id))
return err
}
}
func (e Emitter) now() time.Time {
if e.Now != nil {
return e.Now().UTC()
}
return time.Now().UTC()
}
func (e Emitter) logf(format string, args ...any) {
if e.Logf != nil {
e.Logf(format, args...)
}
}
// created is when the bucket's stream was made, and its fill now.
func created(ctx context.Context, kv jetstream.KeyValue) (time.Time, uint64, Fill, error) {
st, err := kv.Status(ctx)
if err != nil {
return time.Time{}, 0, Fill{}, err
}
b, ok := st.(*jetstream.KeyValueBucketStatus)
if !ok || b.StreamInfo() == nil {
return time.Time{}, 0, Fill{}, fmt.Errorf("the bus answered no stream behind %s", st.Bucket())
}
info := b.StreamInfo()
return info.Created.UTC(), info.State.LastSeq, Fill{Keys: uint64(info.State.NumSubjects), Bytes: info.State.Bytes, MaxBytes: info.Config.MaxBytes}, nil
}
// Start follows state's bucket kv: from the revision after its mark when the mark is of the same bucket (the same
// Created), otherwise from the bucket's end, writing a mark there first, so a reader's first view taken now misses
// nothing. It returns once the watch is set; the changes are said on a goroutine until ctx ends, and a watch that ends
// is sent on errs.
func (e Emitter) Start(ctx context.Context, state string, kv jetstream.KeyValue, errs chan<- error) error {
made, last, fill, err := created(ctx, kv)
if err != nil {
return fmt.Errorf("the state %s cannot be followed for its views: %w", state, err)
}
from := last + 1
if m, ok := e.mark(ctx, state); ok && m.Created.Equal(made) && m.Revision <= last {
from = m.Revision + 1
} else {
e.writeMark(ctx, state, Mark{State: state, Revision: last, Created: made, Fill: fill})
}
w, err := kv.WatchAll(ctx, jetstream.ResumeFromRevision(from))
if err != nil {
return fmt.Errorf("the state %s cannot be watched for its views: %w", state, err)
}
go func() {
defer w.Stop()
said := uint64(0)
for {
select {
case <-ctx.Done():
return
case u, ok := <-w.Updates():
if !ok {
if errs != nil {
errs <- fmt.Errorf("the watch of %s for its views ended", state)
}
return
}
if u != nil {
if err := e.say(ctx, state, made, u); err != nil {
// Not marked: the next start says it again from the mark, and the bus keeps it once by its id.
e.logf("views: %s.%s at %d is not said: %v", state, u.Key(), u.Revision(), err)
continue
}
said = u.Revision()
}
// Marked once every change handed so far is said, so a busy state writes one mark per burst.
if said > 0 && len(w.Updates()) == 0 {
_, _, fill, err := created(ctx, kv)
if err != nil {
e.logf("views: the fill of %s is not read: %v", state, err)
}
e.writeMark(ctx, state, Mark{State: state, Revision: said, Created: made, Fill: fill})
said = 0
}
}
}
}()
return nil
}
// Event is the event saying u, a change of state made at made.
func (e Emitter) Event(state string, made time.Time, u jetstream.KeyValueEntry) Event {
ev := Event{V: Version, Owner: e.Owner, State: state, Key: u.Key(), Revision: u.Revision(), Created: made, At: u.Created().UTC()}
if u.Operation() != jetstream.KeyValuePut {
ev.Deleted = true
return ev
}
if n := len(u.Value()); n > MaxValueBytes {
ev.Gap = true
e.logf("views: %s.%s at %d holds %d bytes, more than an event carries: said as a gap, so a reader asks its first view", state, u.Key(), u.Revision(), n)
if e.TooLarge != nil {
e.TooLarge(state, u.Key(), n)
}
return ev
}
ev.Value = json.RawMessage(u.Value())
if !json.Valid(ev.Value) {
ev.Value, _ = json.Marshal(string(u.Value()))
}
return ev
}
func (e Emitter) say(ctx context.Context, state string, made time.Time, u jetstream.KeyValueEntry) error {
ev := e.Event(state, made, u)
data, err := ev.Encode()
if err != nil {
return err
}
msg := nats.NewMsg(e.Subject(EventName(state)))
msg.Header.Set("x-event-id", ev.ID())
msg.Header.Set("x-source", e.Owner)
msg.Header.Set("x-time", e.now().Format(time.RFC3339Nano))
msg.Header.Set("content-type", "application/json")
msg.Data = data
return e.Publish(ctx, msg, ev.ID())
}
func (e Emitter) mark(ctx context.Context, state string) (Mark, bool) {
if e.Marks == nil {
return Mark{}, false
}
got, err := e.Marks.Get(ctx, state)
if err != nil {
if !errors.Is(err, jetstream.ErrKeyNotFound) {
e.logf("views: the mark of %s is not read (%v): said from the bucket's end", state, err)
}
return Mark{}, false
}
m, err := DecodeMark(got.Value())
if err != nil {
e.logf("views: the mark of %s is not read (%v): said from the bucket's end", state, err)
return Mark{}, false
}
return m, true
}
func (e Emitter) writeMark(ctx context.Context, state string, m Mark) {
if e.Marks == nil {
return
}
m.V, m.At = Version, e.now()
data, err := json.Marshal(m)
if err != nil {
e.logf("views: the mark of %s is not written: %v", state, err)
return
}
if _, err := e.Marks.Put(ctx, state, data); err != nil {
e.logf("views: the mark of %s at %d is not written (%v): a first view answers the one before", state, m.Revision, err)
}
}