Main reverted G's half merge (#189, reverted by8afc3ebin #191) and then took the reader release's S1-S5, which copied G's code. G's form holds where they differ (process_bus.go, seats.go, state.go and their tests); the release's kept names stay so main's callers keep building: SeatViewPrefix beside SeatReadPrefix, and QueueInvoke's empty strings on not-ok (with its test TestQueueInvoke). The tree differs from G's headf30dee54only by those and the release's two tests (objects cap option, seat states and stream kinds). Builds: design 47 G (the milestone's sdk part, as atf30dee54) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
132 lines
4.6 KiB
Go
132 lines
4.6 KiB
Go
package process
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
)
|
|
|
|
// ConsumersState is the name of the state a module keeps its durable consumers' facts in, one key per consumer by its name,
|
|
// history 1 (design 47 G A-166, A-176): what a module's tool reads of a consumer, since a tool runs in a runtime of its own and
|
|
// cannot read the consumer's information.
|
|
const ConsumersState = "consumers"
|
|
|
|
// ConsumerFacts is one durable consumer as ConsumerLedger keeps it.
|
|
type ConsumerFacts struct {
|
|
// Stream and Name are the consumer's stream and its name.
|
|
Stream string `json:"stream"`
|
|
Name string `json:"name"`
|
|
// Acknowledged is the stream sequence of the last message applied.
|
|
Acknowledged uint64 `json:"acknowledged"`
|
|
// Pending is the messages the consumer has not been given yet, as the last message said.
|
|
Pending uint64 `json:"pending"`
|
|
// LastEvent is the subject of the last event applied and LastAt its own `at`, or when the bus stored it where it has none.
|
|
LastEvent string `json:"last-event,omitempty"`
|
|
LastAt time.Time `json:"last-at,omitempty"`
|
|
// OldestUnacknowledged is when the oldest message delivered and not yet applied was delivered; zero when none is.
|
|
OldestUnacknowledged time.Time `json:"oldest-unacknowledged,omitempty"`
|
|
}
|
|
|
|
// ConsumerSink stores one key of the module's state consumers.
|
|
type ConsumerSink func(ctx context.Context, key string, value []byte) error
|
|
|
|
// ConsumerLedger writes a durable consumer's facts into the module's state consumers after each message delivered and each message
|
|
// applied, so a tool reads them and nothing is read on a timer (design 47 G A-166, A-176). One ledger serves one consumer.
|
|
type ConsumerLedger struct {
|
|
stream, name string
|
|
sink ConsumerSink
|
|
now func() time.Time
|
|
logf func(string, ...any)
|
|
|
|
mu sync.Mutex
|
|
facts ConsumerFacts
|
|
unacks map[uint64]time.Time // delivered and not applied, by stream sequence
|
|
}
|
|
|
|
// NewConsumerLedger is the ledger of the consumer name on stream, writing through sink; a nil now is the clock and a nil logf says
|
|
// nothing.
|
|
func NewConsumerLedger(stream, name string, sink ConsumerSink, now func() time.Time, logf func(string, ...any)) *ConsumerLedger {
|
|
if now == nil {
|
|
now = time.Now
|
|
}
|
|
if logf == nil {
|
|
logf = func(string, ...any) {}
|
|
}
|
|
return &ConsumerLedger{stream: stream, name: name, sink: sink, now: now, logf: logf, unacks: map[uint64]time.Time{},
|
|
facts: ConsumerFacts{Stream: stream, Name: name}}
|
|
}
|
|
|
|
// Delivered notes that m was given to the consumer, and writes the facts; a message given again keeps its first delivery's time.
|
|
func (l *ConsumerLedger) Delivered(m jetstream.Msg) {
|
|
md, err := m.Metadata()
|
|
if err != nil {
|
|
l.logf("consumers: %s's message has no metadata, its delivery is not kept: %v", l.name, err)
|
|
return
|
|
}
|
|
l.mu.Lock()
|
|
// A message delivered again keeps the time it was first delivered, so one that never settles grows old (A-187): asked again
|
|
// in this run, the time kept; after a restart, which kept none, the time the bus stored it, never later than its first
|
|
// delivery.
|
|
if _, known := l.unacks[md.Sequence.Stream]; !known {
|
|
first := l.now().UTC()
|
|
if md.NumDelivered > 1 && !md.Timestamp.IsZero() && md.Timestamp.Before(first) {
|
|
first = md.Timestamp.UTC()
|
|
}
|
|
l.unacks[md.Sequence.Stream] = first
|
|
}
|
|
l.facts.Pending = md.NumPending
|
|
l.mu.Unlock()
|
|
l.write()
|
|
}
|
|
|
|
// Applied notes that m was applied and acknowledged, at the event's own `at` (zero: when the bus stored it), and writes the facts.
|
|
func (l *ConsumerLedger) Applied(m jetstream.Msg, at time.Time) {
|
|
md, err := m.Metadata()
|
|
if err != nil {
|
|
l.logf("consumers: %s's message has no metadata, its application is not kept: %v", l.name, err)
|
|
return
|
|
}
|
|
if at.IsZero() {
|
|
at = md.Timestamp
|
|
}
|
|
l.mu.Lock()
|
|
delete(l.unacks, md.Sequence.Stream)
|
|
if md.Sequence.Stream > l.facts.Acknowledged {
|
|
l.facts.Acknowledged = md.Sequence.Stream
|
|
}
|
|
l.facts.Pending = md.NumPending
|
|
l.facts.LastEvent, l.facts.LastAt = m.Subject(), at.UTC()
|
|
l.mu.Unlock()
|
|
l.write()
|
|
}
|
|
|
|
// Facts are the facts now.
|
|
func (l *ConsumerLedger) Facts() ConsumerFacts {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
f := l.facts
|
|
f.OldestUnacknowledged = time.Time{}
|
|
for _, t := range l.unacks {
|
|
if f.OldestUnacknowledged.IsZero() || t.Before(f.OldestUnacknowledged) {
|
|
f.OldestUnacknowledged = t
|
|
}
|
|
}
|
|
return f
|
|
}
|
|
|
|
func (l *ConsumerLedger) write() {
|
|
if l.sink == nil {
|
|
return
|
|
}
|
|
data, err := json.Marshal(l.Facts())
|
|
if err == nil {
|
|
err = l.sink(context.Background(), l.name, data)
|
|
}
|
|
if err != nil {
|
|
l.logf("consumers: the facts of %s are not written to the state %s: %v", l.name, ConsumersState, err)
|
|
}
|
|
}
|