mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds mesh-tools, node-tools → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of …
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
The runtime answers mesh/log.append, read, last and info for a log issued in the module's membership, by publish, batch direct get and stream info alone. A stale expect is refused as a lost state update, so a module tells it by the same code.
319 lines
13 KiB
Go
319 lines
13 KiB
Go
package runtime
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
|
"github.com/novox/mesh-tools/node-tools/internal/launch"
|
|
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
|
|
)
|
|
|
|
// logBuses is a module, issues, that keeps a log `changes` on a real bus, and a module, other, on the
|
|
// same runtime that was issued no log — each reached as a bundle reaches it: mesh/log.<verb> with its
|
|
// JSON, through its module's bus (novox/hq ADR 0297).
|
|
func logBuses(t *testing.T, maxBytes int64) (issues, other launch.Bus, mesh *mt.Mesh) {
|
|
t.Helper()
|
|
mesh = mt.New(t)
|
|
m := mt.MembershipOf("issues", "anchor", false, nil)
|
|
m.Logs = []bus.LogIssued{mesh.Log(t, "issues", "changes", maxBytes)}
|
|
mesh.Issue(t, m)
|
|
mesh.Issue(t, mt.MembershipOf("other", "anchor", false, nil))
|
|
conn := connect(t, "node-tools", "anchor")
|
|
conn.Follow("issues")
|
|
conn.Follow("other")
|
|
mt.Until(t, func() error {
|
|
if conn.Membership("issues") == nil || conn.Membership("other") == nil {
|
|
return errors.New("not issued yet")
|
|
}
|
|
return nil
|
|
})
|
|
all := &consumers{conn: conn, logf: t.Logf, of: map[string]*moduleEvents{}}
|
|
return all.forModule("issues"), all.forModule("other"), mesh
|
|
}
|
|
|
|
// logAsk is a bundle's mesh/log.<verb>, with its params exactly as they go over the wire.
|
|
func logAsk(b launch.Bus, verb, params string) (json.RawMessage, error) {
|
|
return b.Log(verb, json.RawMessage(params))
|
|
}
|
|
|
|
func appendOf(t *testing.T, b launch.Bus, params string) uint64 {
|
|
t.Helper()
|
|
raw, err := logAsk(b, "append", params)
|
|
if err != nil {
|
|
t.Fatalf("append %s: %v", params, err)
|
|
}
|
|
var r struct {
|
|
Sequence *uint64 `json:"sequence"`
|
|
}
|
|
if err := json.Unmarshal(raw, &r); err != nil || r.Sequence == nil {
|
|
t.Fatalf("append answered %s, not {sequence}", raw)
|
|
}
|
|
return *r.Sequence
|
|
}
|
|
|
|
// wirePage is a read's answer with exactly the contract's field names.
|
|
type wirePage struct {
|
|
Entries []struct {
|
|
Sequence uint64 `json:"sequence"`
|
|
Key string `json:"key"`
|
|
Value json.RawMessage `json:"value"`
|
|
At string `json:"at"`
|
|
} `json:"entries"`
|
|
Next uint64 `json:"next"`
|
|
Last uint64 `json:"last"`
|
|
}
|
|
|
|
func readOf(t *testing.T, b launch.Bus, params string) wirePage {
|
|
t.Helper()
|
|
raw, err := logAsk(b, "read", params)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", params, err)
|
|
}
|
|
var fields map[string]json.RawMessage
|
|
_ = json.Unmarshal(raw, &fields)
|
|
for _, f := range []string{"entries", "next", "last"} {
|
|
if _, ok := fields[f]; !ok {
|
|
t.Fatalf("read answered %s, without %q", raw, f)
|
|
}
|
|
}
|
|
var p wirePage
|
|
if err := json.Unmarshal(raw, &p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func infoOf(t *testing.T, b launch.Bus) map[string]uint64 {
|
|
t.Helper()
|
|
raw, err := logAsk(b, "info", `{"log":"changes"}`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var info map[string]uint64
|
|
if err := json.Unmarshal(raw, &info); err != nil {
|
|
t.Fatalf("info answered %s: %v", raw, err)
|
|
}
|
|
for _, f := range []string{"entries", "bytes", "max_bytes", "first", "last"} {
|
|
if _, ok := info[f]; !ok {
|
|
t.Fatalf("info answered %s, without %q", raw, f)
|
|
}
|
|
}
|
|
return info
|
|
}
|
|
|
|
// Each verb against a real bus: append answers the sequence, read pages from a sequence on, of a key
|
|
// pattern, with the sequence to continue from (0 at the end) and the log's last, each entry's time in
|
|
// 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)
|
|
before := time.Now().Add(-time.Second)
|
|
|
|
var seqs []uint64
|
|
for i, key := range []string{"12", "13", "12", "14.notes", "12"} {
|
|
seqs = append(seqs, appendOf(t, b, fmt.Sprintf(`{"log":"changes","key":%q,"value":{"op":"move","n":%d}}`, key, i)))
|
|
}
|
|
for i := 1; i < len(seqs); i++ {
|
|
if seqs[i] != seqs[i-1]+1 {
|
|
t.Fatalf("sequences %v are not the log's order", seqs)
|
|
}
|
|
}
|
|
|
|
// Everything, no keys and no limit: the default page holds all five.
|
|
all := readOf(t, b, `{"log":"changes","from":0}`)
|
|
if len(all.Entries) != 5 || all.Next != 0 || all.Last != seqs[4] {
|
|
t.Fatalf("a read of everything: %d entries, next %d, last %d", len(all.Entries), all.Next, all.Last)
|
|
}
|
|
for i, e := range all.Entries {
|
|
at, err := time.Parse(time.RFC3339, e.At)
|
|
if err != nil || at.Before(before) || at.After(time.Now().Add(time.Second)) {
|
|
t.Errorf("entry %d's time %q is not RFC 3339 and now: %v", e.Sequence, e.At, err)
|
|
}
|
|
if e.Sequence != seqs[i] || string(e.Value) != fmt.Sprintf(`{"op":"move","n":%d}`, i) {
|
|
t.Errorf("entry %d: %d %s %s", i, e.Sequence, e.Key, e.Value)
|
|
}
|
|
}
|
|
if all.Entries[3].Key != "14.notes" {
|
|
t.Errorf("a key of two names read back as %q", all.Entries[3].Key)
|
|
}
|
|
|
|
// One key's entries, a page of two at a time.
|
|
first := readOf(t, b, `{"log":"changes","from":1,"keys":"12","limit":2}`)
|
|
if len(first.Entries) != 2 || first.Entries[0].Sequence != seqs[0] || first.Entries[1].Sequence != seqs[2] ||
|
|
first.Next != seqs[2]+1 {
|
|
t.Fatalf("the first page of key 12: %+v", first)
|
|
}
|
|
second := readOf(t, b, fmt.Sprintf(`{"log":"changes","from":%d,"keys":"12","limit":2}`, first.Next))
|
|
if len(second.Entries) != 1 || second.Entries[0].Sequence != seqs[4] || second.Next != 0 {
|
|
t.Fatalf("the second page of key 12: %+v", second)
|
|
}
|
|
// A pattern: `*` one name, `>` the rest.
|
|
if p := readOf(t, b, `{"log":"changes","from":1,"keys":"14.*"}`); len(p.Entries) != 1 || p.Entries[0].Key != "14.notes" {
|
|
t.Fatalf("14.*: %+v", p)
|
|
}
|
|
if p := readOf(t, b, `{"log":"changes","from":1,"keys":"14.>"}`); len(p.Entries) != 1 {
|
|
t.Fatalf("14.>: %+v", p)
|
|
}
|
|
// Past the end: nothing, nothing follows.
|
|
if p := readOf(t, b, fmt.Sprintf(`{"log":"changes","from":%d}`, seqs[4]+1)); len(p.Entries) != 0 || p.Next != 0 || p.Last != seqs[4] {
|
|
t.Fatalf("a read past the end: %+v", p)
|
|
}
|
|
if p := readOf(t, b, `{"log":"changes","from":1,"keys":"99"}`); len(p.Entries) != 0 || p.Next != 0 {
|
|
t.Fatalf("a key with no entries: %+v", p)
|
|
}
|
|
|
|
raw, err := logAsk(b, "last", `{"log":"changes","key":"12"}`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var last map[string]json.RawMessage
|
|
_ = json.Unmarshal(raw, &last)
|
|
if string(last["sequence"]) != fmt.Sprint(seqs[4]) || string(last["key"]) != `"12"` ||
|
|
string(last["value"]) != `{"op":"move","n":4}` || len(last["at"]) == 0 {
|
|
t.Fatalf("the last entry of 12: %s", raw)
|
|
}
|
|
if raw, err := logAsk(b, "last", `{"log":"changes","key":"99"}`); err != nil || string(raw) != "null" {
|
|
t.Fatalf("the last entry of a key with none: %s %v", raw, err)
|
|
}
|
|
|
|
info := infoOf(t, b)
|
|
if info["entries"] != 5 || info["first"] != seqs[0] || info["last"] != seqs[4] || info["max_bytes"] != 1<<20 ||
|
|
info["bytes"] == 0 {
|
|
t.Fatalf("info: %v", info)
|
|
}
|
|
if n := mesh.Consumers(t, "LOG_issues_changes"); n != 0 {
|
|
t.Fatalf("the runtime made %d consumers of the log; it uses none", n)
|
|
}
|
|
}
|
|
|
|
// A page holds about 4 MiB at most, whatever its limit, and says where to continue.
|
|
func TestALogPageIsBoundedByItsBytes(t *testing.T) {
|
|
b, _, _ := logBuses(t, 64<<20)
|
|
big := `"` + strings.Repeat("x", 250*1024) + `"`
|
|
for i := 0; i < 20; i++ {
|
|
appendOf(t, b, fmt.Sprintf(`{"log":"changes","key":"k%d","value":%s}`, i, big))
|
|
}
|
|
p := readOf(t, b, `{"log":"changes","from":1,"limit":1000}`)
|
|
if len(p.Entries) == 0 || len(p.Entries) >= 20 || p.Next != p.Entries[len(p.Entries)-1].Sequence+1 {
|
|
t.Fatalf("a page of 20 entries of 250 KiB: %d entries, next %d", len(p.Entries), p.Next)
|
|
}
|
|
got := len(p.Entries)
|
|
for p.Next != 0 {
|
|
p = readOf(t, b, fmt.Sprintf(`{"log":"changes","from":%d,"limit":1000}`, p.Next))
|
|
got += len(p.Entries)
|
|
}
|
|
if got != 20 {
|
|
t.Fatalf("paging read %d of 20 entries", got)
|
|
}
|
|
}
|
|
|
|
// An append with `expect` is written only when the key's last entry is the one expected — 0 when it
|
|
// has none — and is otherwise refused with the error a lost mesh/state.update answers: the same code,
|
|
// the same error, and nothing written. An append without `expect` is not checked.
|
|
func TestAnAppendAtAStaleExpectIsRefusedAsALostUpdate(t *testing.T) {
|
|
b, _, _ := logBuses(t, 1<<20)
|
|
isLost := func(err error) bool {
|
|
var coded interface{ ErrorCode() int }
|
|
return errors.As(err, &coded) && coded.ErrorCode() == launch.CodeChanged && errors.Is(err, bus.ErrStateChanged)
|
|
}
|
|
|
|
first := appendOf(t, b, `{"log":"changes","key":"7","value":{"op":"open"},"expect":0}`)
|
|
if _, err := logAsk(b, "append", `{"log":"changes","key":"7","value":{"op":"open"},"expect":0}`); !isLost(err) {
|
|
t.Fatalf("a second open of one key at expect 0: %v", err)
|
|
}
|
|
second := appendOf(t, b, fmt.Sprintf(`{"log":"changes","key":"7","value":{"op":"move"},"expect":%d}`, first))
|
|
if _, err := logAsk(b, "append", fmt.Sprintf(`{"log":"changes","key":"7","value":{"op":"note"},"expect":%d}`, first)); !isLost(err) {
|
|
t.Fatalf("an append at a stale expect: %v", err)
|
|
}
|
|
if _, err := logAsk(b, "append", fmt.Sprintf(`{"log":"changes","key":"7","value":{"op":"note"},"expect":%d}`, second+100)); !isLost(err) {
|
|
t.Fatalf("an append at an expect not yet written: %v", err)
|
|
}
|
|
// Another key's entries do not make this key's expect stale.
|
|
appendOf(t, b, `{"log":"changes","key":"8","value":{"op":"open"}}`)
|
|
third := appendOf(t, b, fmt.Sprintf(`{"log":"changes","key":"7","value":{"op":"note"},"expect":%d}`, second))
|
|
// No expect: no check.
|
|
appendOf(t, b, `{"log":"changes","key":"7","value":{"op":"set"}}`)
|
|
|
|
if info := infoOf(t, b); info["entries"] != 5 {
|
|
t.Fatalf("refused appends wrote: %d entries, not 5", info["entries"])
|
|
}
|
|
p := readOf(t, b, `{"log":"changes","from":1,"keys":"7"}`)
|
|
if len(p.Entries) != 4 || p.Entries[2].Sequence != third {
|
|
t.Fatalf("key 7 holds %+v", p.Entries)
|
|
}
|
|
|
|
// The refusal is the very error a lost state update is: its code reaches a bundle as CodeChanged.
|
|
_, err := logAsk(b, "append", `{"log":"changes","key":"7","value":{},"expect":0}`)
|
|
var coded interface{ ErrorCode() int }
|
|
if !errors.As(err, &coded) || coded.ErrorCode() != -32010 {
|
|
t.Fatalf("the refusal carries no lost-update code: %v", err)
|
|
}
|
|
}
|
|
|
|
// A log is its owner's alone: another module on the same runtime is refused it with the reason, and so
|
|
// is a log of a name the owner was not issued — before anything is sent.
|
|
func TestALogNotIssuedIsRefusedWithTheReason(t *testing.T) {
|
|
issues, other, _ := logBuses(t, 1<<20)
|
|
for verb, params := range map[string]string{
|
|
"append": `{"log":"changes","key":"1","value":{}}`,
|
|
"read": `{"log":"changes","from":1}`,
|
|
"last": `{"log":"changes","key":"1"}`,
|
|
"info": `{"log":"changes"}`,
|
|
} {
|
|
_, err := logAsk(other, verb, params)
|
|
if err == nil || !strings.Contains(err.Error(), `other keeps no log called "changes"`) ||
|
|
!strings.Contains(err.Error(), "issued: none") {
|
|
t.Errorf("%s of a log not issued: %v", verb, err)
|
|
}
|
|
}
|
|
_, err := logAsk(issues, "append", `{"log":"moves","key":"1","value":{}}`)
|
|
if err == nil || !strings.Contains(err.Error(), `issues keeps no log called "moves"`) ||
|
|
!strings.Contains(err.Error(), "issued: changes") {
|
|
t.Errorf("an append to a log of a name not issued: %v", err)
|
|
}
|
|
if info := infoOf(t, issues); info["entries"] != 0 {
|
|
t.Fatalf("a refused append wrote: %v", info)
|
|
}
|
|
}
|
|
|
|
// What the bus cannot hold, or the contract does not allow, is refused with the reason and not sent:
|
|
// a value that is not JSON or over 256 KiB, a key that is not names joined by dots or over 255 bytes, a
|
|
// key pattern with `>` not last, a limit over 1000.
|
|
func TestWhatALogCannotHoldIsRefusedWithTheReason(t *testing.T) {
|
|
b, _, _ := logBuses(t, 1<<20)
|
|
over := `"` + strings.Repeat("x", 256*1024) + `"`
|
|
for params, reason := range map[string]string{
|
|
`{"log":"changes","key":"1","value":nope}`: "names no log",
|
|
`{"log":"changes","key":"1"}`: "is JSON",
|
|
`{"log":"changes","key":"1","value":` + over + `}`: "at most 262144 bytes",
|
|
`{"log":"changes","key":"a b","value":1}`: "is not a log key",
|
|
`{"log":"changes","key":"a..b","value":1}`: "is not a log key",
|
|
`{"log":"changes","key":"a.*","value":1}`: "is not a log key",
|
|
`{"log":"changes","key":"` + strings.Repeat("k", 256) + `","value":1}`: "is not a log key",
|
|
`{"key":"1","value":1}`: "names no log",
|
|
} {
|
|
if _, err := logAsk(b, "append", params); err == nil || !strings.Contains(err.Error(), reason) {
|
|
t.Errorf("append %.80s: %v, want %q", params, err, reason)
|
|
}
|
|
}
|
|
for params, reason := range map[string]string{
|
|
`{"log":"changes","from":1,"keys":"a.>.b"}`: "is not a key pattern",
|
|
`{"log":"changes","from":1,"limit":1001}`: "1 to 1000",
|
|
`{"log":"changes","from":1,"limit":-1}`: "1 to 1000",
|
|
} {
|
|
if _, err := logAsk(b, "read", params); err == nil || !strings.Contains(err.Error(), reason) {
|
|
t.Errorf("read %s: %v, want %q", params, err, reason)
|
|
}
|
|
}
|
|
if info := infoOf(t, b); info["entries"] != 0 {
|
|
t.Fatalf("a refused append wrote: %v", info)
|
|
}
|
|
// The largest value allowed is taken.
|
|
appendOf(t, b, `{"log":"changes","key":"1","value":"`+strings.Repeat("x", 256*1024-2)+`"}`)
|
|
}
|