Let a module read its own streams back by a batch direct get, as it reads its log, so its tools can answer what the stream holds (a build step's whole failure output)

This commit is contained in:
2026-10-30 10:54:05 +01:00
parent 95a12105cf
commit b90376ba59
7 changed files with 255 additions and 39 deletions
+57 -37
View File
@@ -221,49 +221,17 @@ func (c *Conn) LogRead(module, name string, from uint64, keys string, limit int)
if from == 0 {
from = 1
}
req, _ := json.Marshal(map[string]any{"seq": from, "next_by_subj": filter, "batch": limit, "max_bytes": LogPageBytes})
inbox := c.nc.NewInbox()
sub, err := c.nc.SubscribeSync(inbox)
msgs, next, err := c.directBatch(l.Stream, filter, from, limit, name, func() error { return notOnTheBus(l) })
if err != nil {
return nil, err
}
defer func() { _ = sub.Unsubscribe() }()
if err := c.nc.PublishRequest("$JS.API.DIRECT.GET."+l.Stream, inbox, req); err != nil {
return nil, err
}
page := &LogPage{Entries: []LogEntry{}}
deadline := time.Now().Add(StateTimeout)
for done := false; !done; {
msg, err := sub.NextMsg(time.Until(deadline))
page := &LogPage{Entries: []LogEntry{}, Next: next}
for _, msg := range msgs {
e, err := logEntryOf(l, msg)
if err != nil {
if errors.Is(err, nats.ErrNoResponders) {
return nil, notOnTheBus(l)
}
if errors.Is(err, nats.ErrTimeout) {
return nil, fmt.Errorf("the read of %s did not end in %s", name, StateTimeout)
}
return nil, err
}
switch status := msg.Header.Get("Status"); status {
case "":
e, err := logEntryOf(l, msg)
if err != nil {
return nil, err
}
page.Entries = append(page.Entries, *e)
case "204": // the end of the batch: what is still pending after the last entry sent
pending, _ := strconv.ParseUint(msg.Header.Get("Nats-Num-Pending"), 10, 64)
if pending > 0 && len(page.Entries) > 0 {
page.Next = page.Entries[len(page.Entries)-1].Sequence + 1
}
done = true
case "404": // nothing from that sequence on
done = true
case "503":
return nil, notOnTheBus(l)
default:
return nil, fmt.Errorf("the read of %s was refused by the bus: %s %s", name, status, msg.Header.Get("Description"))
}
page.Entries = append(page.Entries, *e)
}
info, err := c.logInfo(l)
if err != nil {
@@ -273,6 +241,58 @@ func (c *Conn) LogRead(module, name string, from uint64, keys string, limit int)
return page, nil
}
// directBatch is the messages of a stream from a sequence on, on the subjects matching filter, at most limit and about
// LogPageBytes, by one batch direct get (NATS 2.11), and the sequence to read on from, 0 when nothing follows. name is
// what is read, for its errors; missing is the error of a stream that is not on the bus.
func (c *Conn) directBatch(stream, filter string, from uint64, limit int, name string, missing func() error) ([]*nats.Msg, uint64, error) {
if from == 0 {
from = 1
}
req, _ := json.Marshal(map[string]any{"seq": from, "next_by_subj": filter, "batch": limit, "max_bytes": LogPageBytes})
inbox := c.nc.NewInbox()
sub, err := c.nc.SubscribeSync(inbox)
if err != nil {
return nil, 0, err
}
defer func() { _ = sub.Unsubscribe() }()
if err := c.nc.PublishRequest("$JS.API.DIRECT.GET."+stream, inbox, req); err != nil {
return nil, 0, err
}
var msgs []*nats.Msg
var next uint64
deadline := time.Now().Add(StateTimeout)
for done := false; !done; {
msg, err := sub.NextMsg(time.Until(deadline))
if err != nil {
if errors.Is(err, nats.ErrNoResponders) {
return nil, 0, missing()
}
if errors.Is(err, nats.ErrTimeout) {
return nil, 0, fmt.Errorf("the read of %s did not end in %s", name, StateTimeout)
}
return nil, 0, err
}
switch status := msg.Header.Get("Status"); status {
case "":
msgs = append(msgs, msg)
case "204": // the end of the batch: what is still pending after the last message sent
pending, _ := strconv.ParseUint(msg.Header.Get("Nats-Num-Pending"), 10, 64)
if pending > 0 && len(msgs) > 0 {
last, _ := strconv.ParseUint(msgs[len(msgs)-1].Header.Get("Nats-Sequence"), 10, 64)
next = last + 1
}
done = true
case "404": // nothing from that sequence on
done = true
case "503":
return nil, 0, missing()
default:
return nil, 0, fmt.Errorf("the read of %s was refused by the bus: %s %s", name, status, msg.Header.Get("Description"))
}
}
return msgs, next, nil
}
// LogLast is a key's last entry, or nil when it has none.
func (c *Conn) LogLast(module, name, key string) (*LogEntry, error) {
l, err := c.issuedLog(module, name)