Merge pull request 'mesh-hand-acts answers bounded, newest first; mesh-healers seeds from a partial answer (issue 1966 S8, recovery)' (#642) from fix/1966-s8-hand-acts-window into main
This commit was merged in pull request #642.
This commit is contained in:
@@ -62,21 +62,38 @@ type Store interface {
|
||||
Get(state, key string) ([]byte, bool, error)
|
||||
Create(state, key string, value any) error
|
||||
Keys(state string) ([]string, error)
|
||||
// Append adds an entry to the end of a log; Last is a key's newest entry; Entries every entry in order; Fill how
|
||||
// many entries and bytes the log holds against its cap.
|
||||
// Append adds an entry to the end of a log; Last is a key's newest entry; Page at most limit entries from a
|
||||
// sequence on, oldest first; Fill how many entries and bytes the log holds against its cap, and its first and
|
||||
// last sequence.
|
||||
Append(log, key string, value any) error
|
||||
Last(log, key string) ([]byte, bool, error)
|
||||
Entries(log string) ([][]byte, error)
|
||||
Page(log string, from uint64, limit int) ([]PageEntry, error)
|
||||
Fill(log string) (Fill, error)
|
||||
}
|
||||
|
||||
// Fill is how full a log is: its entries, its bytes and its cap in bytes.
|
||||
// PageEntry is one entry of a log read: its sequence and its value.
|
||||
type PageEntry struct {
|
||||
Seq uint64
|
||||
Value []byte
|
||||
}
|
||||
|
||||
// Fill is how full a log is: its entries, its bytes and its cap in bytes, and its first and last sequence.
|
||||
type Fill struct {
|
||||
Entries uint64 `json:"entries"`
|
||||
Bytes uint64 `json:"bytes"`
|
||||
MaxBytes uint64 `json:"max-bytes"`
|
||||
First uint64 `json:"first"`
|
||||
Last uint64 `json:"last"`
|
||||
}
|
||||
|
||||
// What bounds one answer, so it always comes, whatever the log holds (issue 1966): the bus carries at most 1 MiB, and
|
||||
// the runtime's call is bound at 30 s. A read stops at either and says so.
|
||||
const (
|
||||
MaxAnswerBytes = 900 << 10
|
||||
ReadBudget = 20 * time.Second
|
||||
pageSize = 1000
|
||||
)
|
||||
|
||||
// ErrExists is a create of a key another writer made first.
|
||||
var ErrExists = errors.New("the key was made by another writer first")
|
||||
|
||||
@@ -281,7 +298,13 @@ type Listing struct {
|
||||
Days int `json:"days"`
|
||||
// Unread says which log could not be read, so what is missing is said and never read as nothing.
|
||||
Unread map[string]string `json:"unread,omitempty"`
|
||||
Kept map[string]int `json:"kept"`
|
||||
// Kept is how many entries of the log were read, newest first.
|
||||
Kept map[string]int `json:"kept"`
|
||||
// Complete is true when every act of the window asked was read. Why and Cut set mean the read was cut before the
|
||||
// fortnight's start (Why says at what, Cut how many older entries were not read), and Repeated counts only the part read.
|
||||
Complete bool `json:"complete"`
|
||||
Cut uint64 `json:"cut,omitempty"`
|
||||
Why string `json:"why,omitempty"`
|
||||
// Fill is how full this seat's log is against its cap: full, it refuses the next act.
|
||||
Fill *Fill `json:"fill,omitempty"`
|
||||
}
|
||||
@@ -291,11 +314,17 @@ func (l *Log) HandActs(days int, cause, who string) Listing {
|
||||
now := l.Now()
|
||||
since := now.Add(-time.Duration(days) * 24 * time.Hour)
|
||||
out := Listing{Acts: []Act{}, Repeated: map[string]int{}, Days: days, Kept: map[string]int{}}
|
||||
all, kept, err := l.read(now.Add(-max(time.Duration(days)*24*time.Hour, RepeatedWithin)))
|
||||
all, kept, cut, err := l.read(now.Add(-max(time.Duration(days)*24*time.Hour, RepeatedWithin)))
|
||||
if err != nil {
|
||||
out.Unread = map[string]string{ActsLog: err.Error()}
|
||||
} else {
|
||||
out.Kept[ActsLog] = kept
|
||||
out.Complete, out.Cut, out.Why = cut.why == "", cut.unread, cut.why
|
||||
// Complete is said for the window asked: a read cut after it passed the window's start lists every act of it
|
||||
// (Repeated, over the fortnight, is the part that is partial, and Why says so).
|
||||
if n := len(all); !out.Complete && n > 0 && !all[n-1].At.After(since) {
|
||||
out.Complete = true
|
||||
}
|
||||
}
|
||||
for _, a := range all {
|
||||
if now.Sub(a.At) <= RepeatedWithin && handactsclient.Repair(a) {
|
||||
@@ -318,20 +347,71 @@ func (l *Log) HandActs(days int, cause, who string) Listing {
|
||||
return out
|
||||
}
|
||||
|
||||
// read is every act of this seat's log since a moment, entry by entry, and how many it keeps.
|
||||
func (l *Log) read(since time.Time) ([]Act, int, error) {
|
||||
var out []Act
|
||||
entries, err := l.Store.Entries(ActsLog)
|
||||
// cutRead says why a read stopped before the window's start, and how many older entries it left unread.
|
||||
type cutRead struct {
|
||||
why string
|
||||
unread uint64
|
||||
}
|
||||
|
||||
// read is the acts of this seat's log since a moment, from the newest entry backwards, page by page: it stops at the
|
||||
// first entry older than the moment (the log is in time order), at MaxAnswerBytes of acts or at ReadBudget, and says
|
||||
// which. It also answers how many entries it read.
|
||||
func (l *Log) read(since time.Time) ([]Act, int, cutRead, error) {
|
||||
f, err := l.Store.Fill(ActsLog)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
return nil, 0, cutRead{}, err
|
||||
}
|
||||
for _, raw := range entries {
|
||||
var a Act
|
||||
if json.Unmarshal(raw, &a) != nil || a.At.Before(since) {
|
||||
continue
|
||||
var out []Act
|
||||
var size, read int
|
||||
deadline := l.Now().Add(ReadBudget)
|
||||
for to := f.Last; to > 0 && to >= f.First; {
|
||||
if l.Now().After(deadline) {
|
||||
return out, read, cutRead{"the read took more than " + ReadBudget.String(), to - f.First + 1}, nil
|
||||
}
|
||||
from := f.First
|
||||
if to-from+1 > pageSize {
|
||||
from = to - pageSize + 1
|
||||
}
|
||||
// A page may end early (the runtime stops one at 4 MiB): read on from its last sequence to `to`, so no entry
|
||||
// between is skipped.
|
||||
var page []PageEntry
|
||||
for cur := from; cur <= to; {
|
||||
p, err := l.Store.Page(ActsLog, cur, pageSize)
|
||||
if err != nil {
|
||||
return nil, 0, cutRead{}, err
|
||||
}
|
||||
n := len(page)
|
||||
for _, e := range p {
|
||||
if e.Seq >= cur && e.Seq <= to {
|
||||
page = append(page, e)
|
||||
}
|
||||
}
|
||||
if len(page) == n {
|
||||
break // nothing more reaches `to`: the rest of the range is gone from the log
|
||||
}
|
||||
cur = page[len(page)-1].Seq + 1
|
||||
}
|
||||
for i := len(page) - 1; i >= 0; i-- {
|
||||
read++
|
||||
var a Act
|
||||
if json.Unmarshal(page[i].Value, &a) != nil {
|
||||
continue
|
||||
}
|
||||
if a.At.Before(since) {
|
||||
return out, read, cutRead{}, nil
|
||||
}
|
||||
a.Log = handacts.Seat
|
||||
// The act as answered counts, with its log: the answer stays under the bus's payload.
|
||||
raw, _ := json.Marshal(a)
|
||||
if size += len(raw); size > MaxAnswerBytes {
|
||||
return out, read, cutRead{"the answer reached " + strconv.Itoa(MaxAnswerBytes>>10) + " KiB", page[i].Seq - f.First + 1}, nil
|
||||
}
|
||||
out = append(out, a)
|
||||
}
|
||||
if from <= f.First {
|
||||
break
|
||||
}
|
||||
a.Log = handacts.Seat
|
||||
out = append(out, a)
|
||||
to = from - 1
|
||||
}
|
||||
return out, len(entries), nil
|
||||
return out, read, cutRead{}, nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -20,6 +21,9 @@ type memStore struct {
|
||||
kv map[string]map[string][]byte
|
||||
logs map[string][]entry
|
||||
keysErr map[string]error
|
||||
pages int
|
||||
// pageCap ends a page early at this many entries, as the runtime does at its byte cap.
|
||||
pageCap int
|
||||
}
|
||||
|
||||
func newStore() *memStore {
|
||||
@@ -51,15 +55,19 @@ func (s *memStore) Last(log, key string) ([]byte, bool, error) {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
func (s *memStore) Entries(log string) ([][]byte, error) {
|
||||
func (s *memStore) Page(log string, from uint64, limit int) ([]PageEntry, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if err := s.keysErr[log]; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out [][]byte
|
||||
for _, e := range s.logs[log] {
|
||||
out = append(out, e.raw)
|
||||
s.pages++
|
||||
var out []PageEntry
|
||||
if s.pageCap > 0 {
|
||||
limit = min(limit, s.pageCap)
|
||||
}
|
||||
for i := max(int(from), 1) - 1; i < len(s.logs[log]) && len(out) < limit; i++ {
|
||||
out = append(out, PageEntry{Seq: uint64(i + 1), Value: s.logs[log][i].raw})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -67,7 +75,8 @@ func (s *memStore) Entries(log string) ([][]byte, error) {
|
||||
func (s *memStore) Fill(log string) (Fill, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return Fill{Entries: uint64(len(s.logs[log])), MaxBytes: 1 << 30}, nil
|
||||
n := uint64(len(s.logs[log]))
|
||||
return Fill{Entries: n, MaxBytes: 1 << 30, First: min(n, 1), Last: n}, nil
|
||||
}
|
||||
|
||||
// logKeys are a log's keys in order.
|
||||
@@ -242,12 +251,13 @@ func TestHandActsReadsItsOwnLogAlone(t *testing.T) {
|
||||
}
|
||||
_ = store.Put(log, actKey(at), a)
|
||||
}
|
||||
put(ActsLog, VerbRecord, "relogin", "person.jochen", time.Hour)
|
||||
// The log is in time order, oldest first, as the bus keeps it.
|
||||
put(ActsLog, VerbRecord, "config", "ace.mesh-config", 30*24*time.Hour)
|
||||
put(ActsLog, VerbRecord, "relogin", "shanks.claude-code", 3*24*time.Hour)
|
||||
put(ActsLog, VerbDrill, CauseDrill, "person.jochen", 2*time.Hour)
|
||||
put(ActsLog, VerbWarrant, CauseOperatorAnswer, "the operator", time.Hour)
|
||||
put(ActsLog, VerbWarrant, CauseOperatorAnswer, "the operator", 2*time.Hour)
|
||||
put(ActsLog, VerbRecord, "config", "ace.mesh-config", 30*24*time.Hour)
|
||||
put(ActsLog, VerbRecord, "relogin", "person.jochen", time.Hour)
|
||||
put(ActsLog, VerbWarrant, CauseOperatorAnswer, "the operator", time.Hour)
|
||||
put(controllerActs, VerbRecord, "never-read", "ace.old", time.Hour)
|
||||
store.keysErr[controllerActs] = errors.New("the controller's bucket must not be read")
|
||||
cases := []struct {
|
||||
@@ -277,10 +287,73 @@ func TestHandActsReadsItsOwnLogAlone(t *testing.T) {
|
||||
got.Fill.Entries != 6 {
|
||||
t.Errorf("the log of an act or the kept counts are not said: %+v", got.Kept)
|
||||
}
|
||||
if got := l.HandActs(14, "", ""); !got.Complete || got.Cut != 0 || got.Why != "" {
|
||||
t.Errorf("a window within the log is read whole: %+v", got)
|
||||
}
|
||||
// An 84,000-entry log with 3 acts in the window reads one page and stops at the window's start (issue 1966).
|
||||
big, bigStore, _, bnow := newLog()
|
||||
for i := 0; i < 84000; i++ {
|
||||
at := bnow.Add(-20*24*time.Hour + time.Duration(i)*time.Second)
|
||||
if i >= 83997 {
|
||||
at = bnow.Add(-time.Duration(84000-i) * time.Minute)
|
||||
}
|
||||
_ = bigStore.Append(ActsLog, actKey(at), Act{At: at, Verb: VerbRecord, Cause: "flood", By: "x", Why: "x"})
|
||||
}
|
||||
bigStore.pages = 0
|
||||
if got := big.HandActs(1, "", ""); len(got.Acts) != 3 || !got.Complete || bigStore.pages != 1 {
|
||||
t.Errorf("the window's 3 acts of 84,000: %d acts, complete %v, %d pages", len(got.Acts), got.Complete, bigStore.pages)
|
||||
}
|
||||
// A fortnight of it is cut at the answer's cap: partial, said, and under the bus's 1 MiB.
|
||||
for i := 0; i < 40; i++ {
|
||||
at := bnow.Add(-time.Duration(40-i) * time.Second)
|
||||
_ = bigStore.Append(ActsLog, actKey(at), Act{At: at, Verb: VerbRecord, Cause: "flood", By: "x", Why: strings.Repeat("w", 40<<10)})
|
||||
}
|
||||
got := big.HandActs(14, "", "")
|
||||
if raw, _ := json.Marshal(got); got.Complete || got.Cut == 0 || got.Why == "" || len(raw) > 1<<20 || len(got.Acts) < 20 ||
|
||||
got.Acts[0].At.Before(got.Acts[1].At) {
|
||||
t.Errorf("a window over the cap: complete %v, cut %d, why %q, %d acts, %d bytes", got.Complete, got.Cut, got.Why, len(got.Acts), len(raw))
|
||||
}
|
||||
// A page the runtime ends early skips no entry: 10 acts, pages of 4 (issue 1966 review).
|
||||
pc, pcStore, _, pnow := newLog()
|
||||
for i := 0; i < 10; i++ {
|
||||
at := pnow.Add(-time.Duration(10-i) * time.Minute)
|
||||
_ = pcStore.Append(ActsLog, actKey(at), Act{At: at, Verb: VerbRecord, Cause: "c" + strconv.Itoa(i), By: "x", Why: "x"})
|
||||
}
|
||||
pcStore.pageCap = 4
|
||||
if got := pc.HandActs(14, "", ""); len(got.Acts) != 10 || !got.Complete || got.Kept[ActsLog] != 10 {
|
||||
t.Errorf("early-ended pages: %d acts, complete %v, kept %v", len(got.Acts), got.Complete, got.Kept)
|
||||
}
|
||||
// The answer is counted as it is sent, with its log: 20,000 short acts stay under 1 MiB, and a day wholly listed is complete.
|
||||
sm, smStore, _, snow := newLog()
|
||||
for i := 0; i < 20000; i++ {
|
||||
at := snow.Add(-time.Duration(20000-i) * time.Second)
|
||||
_ = smStore.Append(ActsLog, actKey(at), Act{At: at, Verb: VerbRecord, By: "x", Why: "x"})
|
||||
}
|
||||
got = sm.HandActs(14, "", "")
|
||||
if raw, _ := json.Marshal(got); got.Complete || len(raw) > 1<<20 {
|
||||
t.Errorf("20,000 short acts: complete %v, %d bytes", got.Complete, len(raw))
|
||||
}
|
||||
if got := sm.HandActs(0, "", ""); !got.Complete {
|
||||
t.Errorf("a window wholly listed said partial: %q", got.Why)
|
||||
}
|
||||
// A read that takes longer than its budget stops and says so.
|
||||
slow := &Log{Store: bigStore, Logf: func(string, ...any) {}}
|
||||
slowNow := *bnow
|
||||
slow.Now = func() time.Time { slowNow = slowNow.Add(ReadBudget + time.Second); return slowNow }
|
||||
if got := slow.HandActs(14, "", ""); got.Complete || !strings.Contains(got.Why, "took more than") {
|
||||
t.Errorf("a slow read: complete %v, why %q", got.Complete, got.Why)
|
||||
}
|
||||
store.keysErr[ActsLog] = errors.New("the bus is away")
|
||||
if got := l.HandActs(14, "", ""); got.Unread[ActsLog] == "" || len(got.Acts) != 0 {
|
||||
t.Errorf("an unreadable log: %+v", got.Unread)
|
||||
}
|
||||
for i := 0; i < 30; i++ {
|
||||
_ = store.Put(WarrantsState, "asker.ask-"+strconv.Itoa(i), Warrant{Act: strings.Repeat("w", 40<<10)})
|
||||
}
|
||||
if w, err := l.Warrants(); err != nil || w.(map[string]any)["complete"] != false || w.(map[string]any)["cut"].(int) == 0 {
|
||||
t.Errorf("warrants over the cap answered %v, %v", w, err)
|
||||
}
|
||||
store.kv[WarrantsState] = nil
|
||||
w, err := l.Warrants()
|
||||
if err != nil || w.(map[string]any)["count"] != 0 {
|
||||
t.Errorf("warrants answered %v, %v", w, err)
|
||||
|
||||
@@ -49,21 +49,16 @@ func (runtimeStore) Last(log, key string) ([]byte, bool, error) {
|
||||
return e.Value, true, nil
|
||||
}
|
||||
|
||||
func (runtimeStore) Entries(log string) ([][]byte, error) {
|
||||
var out [][]byte
|
||||
for from := uint64(0); ; {
|
||||
page, err := stdio.Log(log).Read(from, "", 1000)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, e := range page.Entries {
|
||||
out = append(out, e.Value)
|
||||
}
|
||||
if page.Next == 0 {
|
||||
return out, nil
|
||||
}
|
||||
from = page.Next
|
||||
func (runtimeStore) Page(log string, from uint64, limit int) ([]PageEntry, error) {
|
||||
page, err := stdio.Log(log).Read(from, "", limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]PageEntry, 0, len(page.Entries))
|
||||
for _, e := range page.Entries {
|
||||
out = append(out, PageEntry{Seq: e.Sequence, Value: e.Value})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (runtimeStore) Fill(log string) (Fill, error) {
|
||||
@@ -71,7 +66,7 @@ func (runtimeStore) Fill(log string) (Fill, error) {
|
||||
if err != nil {
|
||||
return Fill{}, err
|
||||
}
|
||||
return Fill{Entries: i.Entries, Bytes: i.Bytes, MaxBytes: i.MaxBytes}, nil
|
||||
return Fill{Entries: i.Entries, Bytes: i.Bytes, MaxBytes: i.MaxBytes, First: i.First, Last: i.Last}, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -96,12 +96,17 @@ func (l *Log) Warrants() (any, error) {
|
||||
}
|
||||
sort.Strings(keys)
|
||||
rows := map[string]json.RawMessage{}
|
||||
for _, k := range keys {
|
||||
size := 0
|
||||
for i, k := range keys {
|
||||
if raw, found, err := l.Store.Get(WarrantsState, k); err == nil && found {
|
||||
if size += len(raw) + len(k); size > MaxAnswerBytes {
|
||||
// Bounded like hand-acts: the answer always comes, and says what it left out.
|
||||
return map[string]any{"warrants": rows, "count": len(rows), "complete": false, "cut": len(keys) - i}, nil
|
||||
}
|
||||
rows[k] = raw
|
||||
}
|
||||
}
|
||||
return map[string]any{"warrants": rows, "count": len(rows)}, nil
|
||||
return map[string]any{"warrants": rows, "count": len(rows), "complete": true}, nil
|
||||
}
|
||||
|
||||
func arg(a map[string]any, k string) string {
|
||||
|
||||
@@ -360,6 +360,14 @@ func TestACauseRepairedByHandTwiceIsAHealerWanted(t *testing.T) {
|
||||
if n, err := w.h.Seeded([]byte(`{"acts":[` + string(seatAct) + `]}`)); err != nil || n != 1 {
|
||||
t.Fatalf("the seat's listing was not taken: %d, %v", n, err)
|
||||
}
|
||||
// A partial answer (the seat bounds it) seeds all the same and says it is partial, so the start never retries forever (1966).
|
||||
var said []string
|
||||
w.h.Logf = func(f string, a ...any) { said = append(said, fmt.Sprintf(f, a...)) }
|
||||
if n, err := w.h.Seeded([]byte(`{"acts":[` + string(seatAct) + `],"complete":false,"why":"the answer reached 900 KiB","cut":83000}`)); err != nil ||
|
||||
n != 1 || len(said) != 1 || !strings.Contains(said[0], "partial: the answer reached 900 KiB") {
|
||||
t.Fatalf("a partial listing: %d, %v, said %v", n, err, said)
|
||||
}
|
||||
w.h.Logf = func(string, ...any) {}
|
||||
// A drill in each log counts none.
|
||||
act("a3", "hand-act drill", "drill", time.Hour)
|
||||
drill, _ := json.Marshal(HandAct{ID: "a4", At: w.now.Add(-2 * time.Hour), Verb: "hand-act drill", Why: "x", Cause: "drill"})
|
||||
|
||||
@@ -95,7 +95,10 @@ func (h *Healers) ActRecorded(body []byte) {
|
||||
// answers how many it took.
|
||||
func (h *Healers) Seeded(answer []byte) (int, error) {
|
||||
var l struct {
|
||||
Acts []json.RawMessage `json:"acts"`
|
||||
Acts []json.RawMessage `json:"acts"`
|
||||
Complete *bool `json:"complete"`
|
||||
Why string `json:"why"`
|
||||
Cut uint64 `json:"cut"`
|
||||
}
|
||||
if err := json.Unmarshal(answer, &l); err != nil {
|
||||
return 0, fmt.Errorf("the seat's hand-acts answer is not a list of acts: %w", err)
|
||||
@@ -103,6 +106,11 @@ func (h *Healers) Seeded(answer []byte) (int, error) {
|
||||
for _, raw := range l.Acts {
|
||||
h.ActRecorded(raw)
|
||||
}
|
||||
// A partial answer (the seat bounds it, issue 1966) seeds all the same: the newest acts are what a fortnight's
|
||||
// count needs most, and the older ones are heard no more, so asking again would only repeat it.
|
||||
if l.Complete != nil && !*l.Complete {
|
||||
h.Logf("[%s] took %d of the fortnight's hand acts, the newest (partial: %s; %d older entries not read)", Module, len(l.Acts), l.Why, l.Cut)
|
||||
}
|
||||
return len(l.Acts), nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user