153 lines
5.1 KiB
Go
153 lines
5.1 KiB
Go
package process
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
// **Another module's log, read on this process's own credential** (novox/hq ADR 0571 clause 1; the lead, 2026-10-29): a
|
|
// log the module's manifest names in `reads`, `<module>.<log>`, read through an ordered consumer this connection creates
|
|
// and lets go of, never a durable one. Newest is the newest entry of each key, Since every entry from a time, and Watch
|
|
// those or every entry, then each new one as it is appended: event-driven, nothing reads the log again on a timer. The
|
|
// grants are buscompose's readLogGrants, given for a declared read of a log alone.
|
|
|
|
// LogReader is one module's log this connection reads.
|
|
type LogReader struct {
|
|
Module, Name string
|
|
js func() (jetstream.JetStream, error)
|
|
closed func() bool
|
|
}
|
|
|
|
// ReadLog is module's log name, read on this connection.
|
|
func (c *Conn) ReadLog(module, name string) LogReader {
|
|
return LogReader{Module: module, Name: name, closed: func() bool { return c.nc.IsClosed() },
|
|
js: func() (jetstream.JetStream, error) { return jetstream.New(c.nc) }}
|
|
}
|
|
|
|
func (l LogReader) stream() string { return "LOG_" + l.Module + "_" + l.Name }
|
|
func (l LogReader) subject() string { return "mesh.log." + l.Module + "." + l.Name }
|
|
|
|
// filter is keys as a subject filter: a key, `*` for one name, `**` for the rest; empty is every key.
|
|
func (l LogReader) filter(keys string) string {
|
|
if keys == "" {
|
|
return l.subject() + ".>"
|
|
}
|
|
return l.subject() + "." + watchPattern(keys)
|
|
}
|
|
|
|
func (l LogReader) config(keys string, from stdio.LogFrom) (jetstream.OrderedConsumerConfig, error) {
|
|
cfg := jetstream.OrderedConsumerConfig{FilterSubjects: []string{l.filter(keys)}}
|
|
switch from.Kind {
|
|
case stdio.LogFromNewestKind:
|
|
cfg.DeliverPolicy = jetstream.DeliverLastPerSubjectPolicy
|
|
case stdio.LogFromAllKind:
|
|
cfg.DeliverPolicy = jetstream.DeliverAllPolicy
|
|
case stdio.LogFromSinceKind:
|
|
at := from.Since
|
|
cfg.DeliverPolicy, cfg.OptStartTime = jetstream.DeliverByStartTimePolicy, &at
|
|
case stdio.LogFromNowKind:
|
|
cfg.DeliverPolicy = jetstream.DeliverNewPolicy
|
|
default:
|
|
return cfg, fmt.Errorf("%s's log %s: a read starts from newest, all, since or now, not %q", l.Module, l.Name, from.Kind)
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func (l LogReader) entry(m jetstream.Msg) (stdio.LogEntry, error) {
|
|
meta, err := m.Metadata()
|
|
if err != nil {
|
|
return stdio.LogEntry{}, err
|
|
}
|
|
return stdio.LogEntry{Sequence: meta.Sequence.Stream, Key: strings.TrimPrefix(m.Subject(), l.subject()+"."),
|
|
Value: append([]byte(nil), m.Data()...), At: meta.Timestamp}, nil
|
|
}
|
|
|
|
// Newest is the newest entry of each key keys names, in the log's order.
|
|
func (l LogReader) Newest(keys string) ([]stdio.LogEntry, error) {
|
|
return l.collect(keys, stdio.LogFromNewest)
|
|
}
|
|
|
|
// Since is every entry of keys from at on, in the log's order.
|
|
func (l LogReader) Since(at time.Time, keys string) ([]stdio.LogEntry, error) {
|
|
return l.collect(keys, stdio.LogFromSince(at))
|
|
}
|
|
|
|
func (l LogReader) collect(keys string, from stdio.LogFrom) ([]stdio.LogEntry, error) {
|
|
var out []stdio.LogEntry
|
|
stop, err := l.watch(keys, from, func(e stdio.LogEntry) error { out = append(out, e); return nil }, false)
|
|
if stop != nil {
|
|
stop()
|
|
}
|
|
return out, err
|
|
}
|
|
|
|
// Watch hands handle each entry from where from says and then every new one, in the log's order, and returns once the
|
|
// entries there at its start have been handled. The watch lasts as long as the connection; an error from handle ends
|
|
// it, as the connection closing does. Before the entries at its start are handled, either is returned as the error.
|
|
func (l LogReader) Watch(keys string, from stdio.LogFrom, handle func(stdio.LogEntry) error) error {
|
|
_, err := l.watch(keys, from, handle, true)
|
|
return err
|
|
}
|
|
|
|
func (l LogReader) watch(keys string, from stdio.LogFrom, handle func(stdio.LogEntry) error, follow bool) (func(), error) {
|
|
cfg, err := l.config(keys, from)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
js, err := l.js()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bind, stopBind := context.WithTimeout(context.Background(), stateTimeout)
|
|
defer stopBind()
|
|
cons, err := js.OrderedConsumer(bind, l.stream(), cfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s's log %s (stream %s): %w", l.Module, l.Name, l.stream(), err)
|
|
}
|
|
info, err := cons.Info(bind)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s's log %s (stream %s): %w", l.Module, l.Name, l.stream(), err)
|
|
}
|
|
pending := info.NumPending
|
|
it, err := cons.Messages()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s's log %s (stream %s): %w", l.Module, l.Name, l.stream(), err)
|
|
}
|
|
for ; pending > 0; pending-- {
|
|
m, err := it.Next()
|
|
if err == nil {
|
|
var e stdio.LogEntry
|
|
if e, err = l.entry(m); err == nil {
|
|
err = handle(e)
|
|
}
|
|
}
|
|
if err != nil {
|
|
it.Stop()
|
|
return nil, fmt.Errorf("%s's log %s: %w", l.Module, l.Name, err)
|
|
}
|
|
}
|
|
if !follow {
|
|
return it.Stop, nil
|
|
}
|
|
go func() {
|
|
defer it.Stop()
|
|
for {
|
|
m, err := it.Next()
|
|
if err != nil || (l.closed != nil && l.closed()) {
|
|
return
|
|
}
|
|
e, err := l.entry(m)
|
|
if err != nil || handle(e) != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
return it.Stop, nil
|
|
}
|