Read memberships from the grants part's state, what the mesh holds from the indexer and placement, and give bundles newest, since and watch on logs, so the tool runner asks the controller for nothing it keeps (issue 1955, ADR 0571 clauses 1, 4, 5, 7)

Pins mesh-sdk bb993dd. The assignments stream stays the membership's
fallback until the lease write; a log another module keeps is read as
<module>.<log>.
This commit is contained in:
2026-10-29 23:42:43 +01:00
parent 200249a84f
commit 54680238a0
16 changed files with 716 additions and 204 deletions
+66 -4
View File
@@ -20,9 +20,13 @@ func logBuses(t *testing.T, maxBytes int64) (issues, other launch.Bus, mesh *mt.
t.Helper()
mesh = mt.New(t)
m := mt.MembershipOf("issues", "anchor", false, nil)
m.Logs = []bus.LogIssued{mesh.Log(t, "issues", "changes", maxBytes)}
changes := mesh.Log(t, "issues", "changes", maxBytes)
m.Logs = []bus.LogIssued{changes}
mesh.Issue(t, m)
mesh.Issue(t, mt.MembershipOf("other", "anchor", false, nil))
// other reads issues' log, as its manifest's reads declare it (ADR 0571): `<module>.<log>`, without writing it.
o := mt.MembershipOf("other", "anchor", false, nil)
o.Logs = []bus.LogIssued{{Name: "issues.changes", Stream: changes.Stream, Subject: changes.Subject}}
mesh.Issue(t, o)
conn := connect(t, "node-tools", "anchor")
conn.Follow("issues")
conn.Follow("other")
@@ -111,7 +115,7 @@ func infoOf(t *testing.T, b launch.Bus) map[string]uint64 {
// RFC 3339; last is a key's last entry or null; info says entries, bytes, cap, first and last — and no
// consumer is made for any of it.
func TestALogAnswersEachVerbOnTheBus(t *testing.T) {
b, _, mesh := logBuses(t, 1<<20)
b, other, mesh := logBuses(t, 1<<20)
before := time.Now().Add(-time.Second)
var seqs []uint64
@@ -189,6 +193,64 @@ func TestALogAnswersEachVerbOnTheBus(t *testing.T) {
if n := mesh.Consumers(t, "LOG_issues_changes"); n != 0 {
t.Fatalf("the runtime made %d consumers of the log; it uses none", n)
}
// From a time (ADR 0571): `since` in place of `from`, RFC 3339; both at once are refused.
cut := all.Entries[2].At
if p := readOf(t, b, fmt.Sprintf(`{"log":"changes","since":%q}`, cut)); len(p.Entries) == 0 || p.Entries[0].Sequence > seqs[2] ||
p.Entries[len(p.Entries)-1].Sequence != seqs[4] {
t.Fatalf("a read since %s: %+v", cut, p)
}
if p := readOf(t, b, fmt.Sprintf(`{"log":"changes","since":%q}`, time.Now().Add(time.Hour).Format(time.RFC3339))); len(p.Entries) != 0 {
t.Fatalf("a read since an hour ahead: %+v", p)
}
if _, err := logAsk(b, "read", `{"log":"changes","from":2,"since":"2026-10-29T00:00:00Z"}`); err == nil {
t.Fatal("a read from a sequence and a time was not refused")
}
// The newest of each key, in the log's order; `**` for the rest, as mesh-sdk says it.
raw, err = logAsk(b, "newest", `{"log":"changes"}`)
var newest []struct {
Sequence uint64 `json:"sequence"`
Key string `json:"key"`
}
if err != nil || json.Unmarshal(raw, &newest) != nil || len(newest) != 3 || newest[0].Key != "13" || newest[1].Key != "14.notes" ||
newest[2].Sequence != seqs[4] {
t.Fatalf("newest: %s %v", raw, err)
}
if raw, err := logAsk(b, "newest", `{"log":"changes","keys":"14.**"}`); err != nil || !strings.Contains(string(raw), `"14.notes"`) ||
strings.Contains(string(raw), `"12"`) {
t.Fatalf("newest of 14.**: %s %v", raw, err)
}
// Another module's log, read by the name its reads declare; never written.
if p := readOf(t, other, `{"log":"issues.changes","from":1}`); len(p.Entries) != 5 {
t.Fatalf("other's read of issues.changes: %+v", p)
}
if _, err := logAsk(other, "append", `{"log":"issues.changes","key":"1","value":{}}`); err == nil ||
!strings.Contains(err.Error(), "without writing it") {
t.Fatalf("other's append to the log it reads: %v", err)
}
// A watch hands each entry from where it is asked, then each new one, one at a time.
heard := make(chan bus.LogEntry, 16)
stop, err := b.LogWatch(json.RawMessage(fmt.Sprintf(`{"log":"changes","keys":"12","from":%d}`, seqs[2])), func(raw json.RawMessage) error {
var e bus.LogEntry
_ = json.Unmarshal(raw, &e)
heard <- e
return nil
})
if err != nil {
t.Fatal(err)
}
defer stop()
next := appendOf(t, b, `{"log":"changes","key":"12","value":{"op":"late"}}`)
for _, want := range []uint64{seqs[2], seqs[4], next} {
select {
case e := <-heard:
if e.Sequence != want || e.Key != "12" {
t.Fatalf("the watch handed %d %s, want %d", e.Sequence, e.Key, want)
}
case <-time.After(5 * time.Second):
t.Fatalf("the watch did not hand entry %d", want)
}
}
}
// A page holds about 4 MiB at most, whatever its limit, and says where to continue.
@@ -267,7 +329,7 @@ func TestALogNotIssuedIsRefusedWithTheReason(t *testing.T) {
} {
_, err := logAsk(other, verb, params)
if err == nil || !strings.Contains(err.Error(), `other keeps no log called "changes"`) ||
!strings.Contains(err.Error(), "issued: none") {
!strings.Contains(err.Error(), "issued: issues.changes") {
t.Errorf("%s of a log not issued: %v", verb, err)
}
}
+48 -2
View File
@@ -969,9 +969,24 @@ type logAsked struct {
From uint64 `json:"from"`
Keys string `json:"keys"`
Limit int `json:"limit"`
// Since is a read's or a watch's start as an RFC 3339 time, in place of From (novox/hq ADR 0571).
Since string `json:"since"`
}
// Log answers a bundle's `append`, `read`, `last` and `info` on its module's log (ADR 0297). The runtime
// since is the asked start time, zero when none.
func (a logAsked) since() (time.Time, error) {
if a.Since == "" {
return time.Time{}, nil
}
at, err := time.Parse(time.RFC3339Nano, a.Since)
if err != nil {
return time.Time{}, fmt.Errorf("since is an RFC 3339 time (2026-10-29T09:30:00Z), not %q", a.Since)
}
return at, nil
}
// Log answers a bundle's `append`, `read`, `newest`, `last` and `info` on its module's log and on a log its module reads
// as `<module>.<log>` (ADR 0297, ADR 0571). The runtime
// refuses, with the reason, a log the module was not issued; an append whose `expect` is stale is
// refused with the code of a lost state update.
func (b *moduleBus) Log(verb string, params json.RawMessage) (json.RawMessage, error) {
@@ -989,11 +1004,22 @@ func (b *moduleBus) Log(verb string, params json.RawMessage) (json.RawMessage, e
}
answer = map[string]any{"sequence": seq}
case "read":
page, err := conn.LogRead(b.module, asked.Log, asked.From, asked.Keys, asked.Limit)
since, err := asked.since()
if err != nil {
return nil, err
}
page, err := conn.LogRead(b.module, asked.Log, asked.From, since, asked.Keys, asked.Limit)
if err != nil {
return nil, err
}
answer = page
case "newest":
// The newest entry of each key the pattern names (mesh-sdk process.LogReader.Newest).
entries, err := conn.LogNewest(b.module, asked.Log, asked.Keys)
if err != nil {
return nil, err
}
answer = entries
case "last":
entry, err := conn.LogLast(b.module, asked.Log, asked.Key)
if err != nil {
@@ -1015,6 +1041,26 @@ func (b *moduleBus) Log(verb string, params json.RawMessage) (json.RawMessage, e
return json.Marshal(answer)
}
// LogWatch hands a bundle each entry of a log, `{log, keys, from, since}`: from a sequence, from a time, or with neither
// each new one, one at a time (novox/hq ADR 0571; mesh-sdk process.LogReader.Watch).
func (b *moduleBus) LogWatch(params json.RawMessage, deliver func(json.RawMessage) error) (func(), error) {
var asked logAsked
if err := json.Unmarshal(params, &asked); err != nil || asked.Log == "" {
return nil, fmt.Errorf("mesh/log.watch names no log: {log, keys, from, since}")
}
since, err := asked.since()
if err != nil {
return nil, err
}
return b.all.conn.LogWatch(b.module, asked.Log, asked.Keys, asked.From, since, func(e bus.LogEntry) error {
raw, err := json.Marshal(e)
if err != nil {
return err
}
return deliver(raw)
})
}
// Watch hands a bundle its module's state as it is and as it changes (ADR 0201): `{state, key}`, the
// key a pattern with `*` and `**`, empty for every key.
func (b *moduleBus) Watch(params json.RawMessage, deliver func(json.RawMessage) error) (func(), error) {