The gate says its records' ledger filling, full and room, and holds a merge whose record finds no room (milestone 47/G, W11.20)

Design 47: a store filling is said before it is full. The gate reads its ledger's fill through the
tool runner's mesh/ledger.fill, says filling of the seat mesh-delivery when a record takes the ledger
past 80 % of its cap, refuses the next record when none fits and says full naming it, and keeps the
merge waiting for room on its delivery (held_full), so a restarted gate judges alike. Once the
ledger is below 80 % again, after the retention's removals, it says room and hands the merge.
gate_records says the fill against its cap, or why it is unread.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ

(Joined from 061be00d.)
Builds: design 47 G §181 (GATE12)

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 14:44:17 +01:00
parent 6ac5c934d2
commit 8ee28d08c3
8 changed files with 369 additions and 19 deletions
@@ -74,7 +74,7 @@ func main() {
}
h.init()
h.SaySeat = stdio.SeatPublish
h.Records = &Records{Ledger: sdkLedger{}, Now: h.Now, Retention: h.recordRetention, Say: stdio.SeatPublish}
h.Records = &Records{Ledger: sdkLedger{}, Now: h.Now, Retention: h.recordRetention, Say: stdio.SeatPublish, Logf: logf}
h.Merges = recordedMerges{MergeStore: kvMerges{}, records: h.Records, members: h.groupMembers}
h.Old = kvOld{}
h.Consumer = &StepConsumer{Records: h.Records, Families: stepFamilies, Now: h.Now, Logf: logf}
@@ -291,6 +291,9 @@ func (h *Holder) hand(gid, name, key, target string, reads []memberRead, approve
if gid != "" && !h.groupMergeable(gid, target, reads) {
return
}
if h.waitForRoom(target) {
return
}
h.mu.Lock()
value, ok := h.mergeValue(name, key, approved, target, mdc.MergeMerging)
h.mu.Unlock()
@@ -306,6 +309,11 @@ func (h *Holder) hand(gid, name, key, target string, reads []memberRead, approve
err = h.Merges.Put(name, value)
}
if err != nil {
if errors.Is(err, ErrRecordsFull) {
h.mu.Lock()
h.holdFull(target, h.Now())
h.mu.Unlock()
}
h.giveBack(target, err)
return
}
@@ -136,7 +136,10 @@ type ReadyRun struct {
// CannotWrite is why the operator is not asked: the forge's merging account cannot write a repository the ask covers
// (writes.go, issue 1945); CannotWriteSince since when. Said at once as a condition.
CannotWrite string `json:"cannot_write,omitempty"`
CannotWrite string `json:"cannot_write,omitempty"`
// HeldFull is since when its merge waits for room in the delivery records' ledger, which refused its record as full
// (recordfill.go): handed once the ledger is below 80 % of its cap again.
HeldFull *time.Time `json:"held_full,omitempty"`
CannotWriteSince *time.Time `json:"cannot_write_since,omitempty"`
// NotHanded is why an approved merge that waits for nothing is not handed to the forge bridge (a member of its group that
@@ -345,8 +345,11 @@ type Records struct {
Now func() time.Time
// Retention is how long a record is kept; nil keeps DefaultRecordRetention.
Retention func() time.Duration
// Say says the seat mesh-delivery's events of the records: pinned, freed, record-ended (recordevents.go); nil says none.
// Say says the seat mesh-delivery's events of the records: pinned, freed, record-ended (recordevents.go), filling,
// full and room (recordfill.go); nil says none.
Say SeatSay
// Logf says what the records could not say; nothing is said when nil.
Logf func(format string, args ...any)
// held are the records kept, by instance, read once from the ledger (Current) and kept as the gate writes them: the
// gate is their one writer.
@@ -475,6 +478,10 @@ func (rs *Records) Open(milestone string, heads map[string]string, forgeMade boo
if err := rs.Sweep(now); err != nil {
return nil, false, fmt.Errorf("the records past their retention cannot be removed, so no record is opened: %w", err)
}
before, read, err := rs.refuseFull(milestone, inst)
if err != nil {
return nil, false, err
}
body := map[string]any{"milestone": milestone, "heads": heads, "opened": now, "form": RecordForm}
if forgeMade {
body["forge-made"] = true
@@ -485,6 +492,9 @@ func (rs *Records) Open(milestone string, heads map[string]string, forgeMade boo
if _, err := rs.Ledger.Apply(heldInstance(now), OpHold, heldEntry{Instance: inst, Opened: now}); err != nil {
return nil, false, fmt.Errorf("the record %s is opened but not listed: %w", inst, err)
}
if read {
rs.sayFilling(inst, before)
}
r, err = rs.Read(inst)
if err == nil && r != nil {
rs.keep(r)
@@ -30,6 +30,15 @@ type ledgerFake struct {
seq map[string]uint64
history []string
failOn string
// fill is what Fill answers; nil, the ledger says no fill.
fill *buscalls.LedgerFilled
}
func (f *ledgerFake) Fill() (buscalls.LedgerFilled, error) {
if f.fill == nil {
return buscalls.LedgerFilled{}, errors.New("the runtime answers no mesh/ledger.fill")
}
return *f.fill, nil
}
func newLedgerFake(t *testing.T) *ledgerFake {
@@ -742,3 +751,119 @@ func TestTheRecordsToolsReadThemAndEndOneByHand(t *testing.T) {
}
})
}
// W11.20 (design 47 *The delivery record*: a store filling is said before it is full): the gate reads its ledger's fill,
// says filling when a record takes it past 80 % of its cap, refuses the next record as full and holds its merge, and
// says room and hands the merge once the ledger is below 80 % again; the read tool says the fill.
func TestTheRecordsLedgerSaysFillingFullAndRoom(t *testing.T) {
const capBytes = 10 << 20
filled := func(bytes uint64) *buscalls.LedgerFilled {
return &buscalls.LedgerFilled{History: buscalls.Fill{Count: 1, Bytes: bytes, MaxBytes: capBytes},
State: buscalls.Fill{Bytes: 1, MaxBytes: 64 << 20}}
}
heads := map[string]string{"novox/app@111111111111": strings.Repeat("1", 40)}
at := time.Date(2026, 11, 9, 20, 0, 0, 0, time.UTC)
type said struct {
subject, id string
body meshdelivery.Fill
}
saying := func(out *[]said) SeatSay {
return func(subject string, body any, id string) error {
f, _ := body.(meshdelivery.Fill)
*out = append(*out, said{subject, id, f})
return nil
}
}
t.Run("filling once past 80 %, full refuses the next record, an unread fill refuses nothing", func(t *testing.T) {
l := newLedgerFake(t)
var events []said
rs := &Records{Ledger: l, Now: func() time.Time { return at }, Say: saying(&events)}
l.fill = filled(capBytes * 7 / 10)
r, opened, err := rs.Open("47/G", heads, false)
if err != nil || !opened {
t.Fatalf("open below the mark: %v %v", opened, err)
}
if len(events) != 0 {
t.Fatalf("said %+v below the mark", events)
}
// The record took the ledger past 80 %: said once as filling, with the store, its bytes and its cap.
l.fill = filled(capBytes * 85 / 100)
rs.sayFilling(r.Instance, buscalls.Fill{Bytes: capBytes * 7 / 10, MaxBytes: capBytes})
if len(events) != 1 || events[0].subject != meshdelivery.SubjectFilling || events[0].body.Store != RecordsLedger ||
events[0].body.Cap != capBytes || events[0].body.Bytes != capBytes*85/100 {
t.Fatalf("filling said as %+v", events)
}
// Already past the mark: a next record is opened and says nothing again.
events = nil
if _, opened, err := rs.Open("47/H", heads, false); err != nil || !opened || len(events) != 0 {
t.Fatalf("a record past the mark: %v %v, said %+v", opened, err, events)
}
// No room for one more record: refused, said as full naming what it refuses, nothing opened.
l.fill = filled(capBytes - RecordRoom/2)
n := len(l.history)
if _, opened, err := rs.Open("47/I", heads, false); !errors.Is(err, ErrRecordsFull) || opened || len(l.history) != n {
t.Fatalf("a full ledger opened %v (%v), %d operations", opened, err, len(l.history)-n)
}
if len(events) != 1 || events[0].subject != meshdelivery.SubjectFull || !strings.Contains(events[0].body.Refused, "47/I") {
t.Fatalf("full said as %+v", events)
}
if line, _ := rs.Lines(); !strings.HasPrefix(fmt.Sprint(line["fill"].(map[string]any)["state"]), "full") {
t.Fatalf("gate_records says the fill %v", line["fill"])
}
// A ledger that says no fill (a runner before the call): the record is opened, and the read tool says why.
l.fill = nil
if _, opened, err := rs.Open("47/J", heads, false); err != nil || !opened {
t.Fatalf("an unread fill refused the record: %v %v", opened, err)
}
if line, _ := rs.Lines(); line["fill"].(map[string]any)["unread"] == nil {
t.Fatalf("gate_records says the unread fill as %v", line["fill"])
}
})
t.Run("a merge refused for a full ledger is held, then handed with room once below 80 %", func(t *testing.T) {
w := approvedGroup(t)
l := newLedgerFake(t)
var events []said
w.h.Records = &Records{Ledger: l, Now: w.h.Now, Say: saying(&events)}
w.h.Merges = recordedMerges{MergeStore: w.merges, records: w.h.Records, members: w.h.groupMembers}
l.fill = filled(capBytes)
allGreen(w)
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
w.settleAll()
if len(w.forge.merges) != 0 {
t.Fatalf("a full ledger let %v merge", w.forge.merges)
}
w.h.mu.Lock()
since := w.h.heldFull()
w.h.mu.Unlock()
if since == nil || len(events) == 0 || events[0].subject != meshdelivery.SubjectFull {
t.Fatalf("the merge is not held for room (%v), said %+v", since, events)
}
// Room for a record but past 80 %: still held.
l.fill = filled(capBytes * 85 / 100)
w.later(askAgainAfter + time.Second)
w.settleAll()
if len(w.forge.merges) != 0 {
t.Fatalf("a ledger past 80 %% let %v merge after a full", w.forge.merges)
}
// Below 80 %: room said once, the held mark cleared, the group merged.
l.fill = filled(capBytes / 2)
w.later(askAgainAfter + time.Second)
w.settleAll()
w.settleAll()
rooms := 0
for _, e := range events {
if e.subject == meshdelivery.SubjectRoom {
rooms++
}
}
w.h.mu.Lock()
since = w.h.heldFull()
w.h.mu.Unlock()
if rooms != 1 || since != nil || len(w.forge.merges) == 0 {
t.Fatalf("room said %d times, held since %v, merges %v", rooms, since, w.forge.merges)
}
})
}
@@ -0,0 +1,201 @@
package main
// The delivery records' ledger against its cap (design 47 *The delivery record*: "A store filling is said before it is
// full"): the gate reads its ledger's fill through the tool runner (mesh-sdk buscalls.LedgerFill), says `filling` of the
// seat mesh-delivery when a record it opens takes the ledger past 80 % of its cap, refuses the next record when the
// ledger has no room for one and says `full`, and holds that record's merge until the ledger is below 80 % again, when
// it says `room` and hands the merge. Nothing is dropped to make room: only the retention removes a record. The fill
// is read from the bus at each judgement, and which merges wait for room is kept on their deliveries (Ready.HeldFull),
// so a restarted gate judges as the one before it.
import (
"encoding/json"
"errors"
"fmt"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-sdk/go/buscalls"
)
// FillingPercent is the share of its cap past which the ledger is said filling, and below which room is said again.
const FillingPercent = 80
// RecordRoom is the room one record needs at most: an operation is at most one value of the bus.
const RecordRoom = 256 << 10
// ErrRecordsFull is a record refused because the records' ledger has no room for it.
var ErrRecordsFull = errors.New("the delivery records' ledger is full")
// fillReader is a ledger that says its fill against its cap.
type fillReader interface {
Fill() (buscalls.LedgerFilled, error)
}
func (sdkLedger) Fill() (buscalls.LedgerFilled, error) {
out, err := ledgerCall(buscalls.LedgerFill, buscalls.LedgerRequest{Ledger: RecordsLedger})
if err != nil {
return buscalls.LedgerFilled{}, err
}
var got buscalls.LedgerFilled
if err := json.Unmarshal(out, &got); err != nil {
return got, fmt.Errorf("the ledger's fill is not readable: %w", err)
}
return got, nil
}
// Fill is the records' ledger's store nearest its cap.
func (rs *Records) Fill() (buscalls.Fill, error) {
fr, ok := rs.Ledger.(fillReader)
if !ok {
return buscalls.Fill{}, errors.New("this gate's ledger says no fill")
}
f, err := fr.Fill()
if err != nil {
return buscalls.Fill{}, fmt.Errorf("the records' ledger cannot say its fill: %w", err)
}
return f.Fullest(), nil
}
// pastFilling is a fill at or past FillingPercent of its cap.
func pastFilling(f buscalls.Fill) bool {
return f.MaxBytes > 0 && f.Bytes*100 >= f.MaxBytes*FillingPercent
}
// noRoom is a fill that leaves no room for one more record.
func noRoom(f buscalls.Fill) bool { return f.MaxBytes > 0 && f.Bytes+RecordRoom > f.MaxBytes }
// sayFill says filling, full or room of the seat under an id of the event and what it is about, so said again it is
// heard once.
func (rs *Records) sayFill(subject, about string, f buscalls.Fill, refused string) {
at := rs.now().UTC()
body := meshdelivery.Fill{Store: RecordsLedger, Bytes: int64(f.Bytes), Cap: int64(f.MaxBytes), Refused: refused, At: &at}
if err := rs.say(subject, body, about); err != nil {
rs.logf("[mesh-delivery] %s of the records' ledger (%d of %d bytes) is not said: %v", subject, f.Bytes, f.MaxBytes, err)
}
}
func (rs *Records) logf(format string, args ...any) {
if rs.Logf != nil {
rs.Logf(format, args...)
}
}
// refuseFull says full and answers ErrRecordsFull when the ledger has no room for the record inst; a fill it cannot
// read refuses nothing (the read tools say why it is not read).
func (rs *Records) refuseFull(milestone, inst string) (before buscalls.Fill, read bool, err error) {
before, fillErr := rs.Fill()
if fillErr != nil {
rs.logf("[mesh-delivery] the record %s is opened with its ledger's fill unread: %v", inst, fillErr)
return before, false, nil
}
if !noRoom(before) {
return before, true, nil
}
rs.sayFill(meshdelivery.SubjectFull, "full-"+inst, before,
"the next record, "+inst+" of "+milestone+"; its merge is held until room is said")
return before, true, fmt.Errorf("%w (%d of %d bytes): the record %s of %s is not opened, and its merge is held until "+
"room is said: the setting record-retention or a larger cap is the operator's", ErrRecordsFull, before.Bytes, before.MaxBytes, inst, milestone)
}
// sayFilling says filling when the record just opened took the ledger past FillingPercent of its cap.
func (rs *Records) sayFilling(inst string, before buscalls.Fill) {
if pastFilling(before) {
return
}
after, err := rs.Fill()
if err == nil && pastFilling(after) {
rs.sayFill(meshdelivery.SubjectFilling, "filling-"+inst, after, "")
}
}
// RoomAgain is whether a merge held for a full ledger since then may be handed now: the records past their retention
// removed first, the ledger below FillingPercent of its cap, said once as room. A fill it cannot read gives no room.
func (rs *Records) RoomAgain(since time.Time) (bool, error) {
if err := rs.Sweep(rs.now()); err != nil {
return false, err
}
f, err := rs.Fill()
if err != nil {
return false, err
}
if pastFilling(f) {
return false, fmt.Errorf("the records' ledger holds %d of %d bytes, past %d %% of its cap: the merge waits for room",
f.Bytes, f.MaxBytes, FillingPercent)
}
rs.sayFill(meshdelivery.SubjectRoom, "room-"+since.UTC().Format(time.RFC3339Nano), f, "")
return true, nil
}
// fillLine is the ledger's fill as the read tools say it.
func (rs *Records) fillLine() map[string]any {
f, err := rs.Fill()
if err != nil {
return map[string]any{"store": RecordsLedger, "unread": err.Error()}
}
line := map[string]any{"store": RecordsLedger, "bytes": f.Bytes, "cap": f.MaxBytes, "count": f.Count}
switch {
case noRoom(f):
line["state"] = "full: the next record is refused and its merge held"
case pastFilling(f):
line["state"] = fmt.Sprintf("filling: past %d %% of its cap", FillingPercent)
default:
line["state"] = "room"
}
return line
}
// heldFull is the earliest time a merge was held for a full ledger, of every delivery that waits for room. Under the
// lock.
func (h *Holder) heldFull() *time.Time {
var since *time.Time
for _, d := range h.deliveries {
if r := d.Ready; r != nil && r.HeldFull != nil && (since == nil || r.HeldFull.Before(*since)) {
since = r.HeldFull
}
}
return since
}
// roomFor clears every delivery's wait for room. Under the lock.
func (h *Holder) roomFor() {
for _, d := range h.deliveries {
if r := d.Ready; r != nil && r.HeldFull != nil {
r.HeldFull = nil
h.keep(d)
}
}
}
// holdFull keeps that target's merge waits for room. Under the lock.
func (h *Holder) holdFull(target string, now time.Time) {
if d := h.deliveries[target]; d != nil && d.Ready != nil && d.Ready.HeldFull == nil {
at := now
d.Ready.HeldFull = &at
h.keep(d)
}
}
// waitForRoom is whether a merge must wait because the records' ledger was full: while any merge waits for room, none
// is handed until RoomAgain gives it.
func (h *Holder) waitForRoom(target string) bool {
if h.Records == nil {
return false
}
h.mu.Lock()
since := h.heldFull()
h.mu.Unlock()
if since == nil {
return false
}
ok, err := h.Records.RoomAgain(*since)
if !ok {
h.giveBack(target, err)
return true
}
h.mu.Lock()
h.roomFor()
h.mu.Unlock()
h.Logf("[mesh-delivery] the records' ledger has room again: the merges held for it are handed")
return false
}
@@ -105,7 +105,8 @@ func (rs *Records) Lines() (map[string]any, error) {
}
lines = append(lines, l)
}
return map[string]any{"records": lines, "open": open, "kept": len(lines), "retention-days": int(rs.retention().Hours() / 24)}, nil
return map[string]any{"records": lines, "open": open, "kept": len(lines), "retention-days": int(rs.retention().Hours() / 24),
"fill": rs.fillLine()}, nil
}
// EndByHand ends one open record by hand, with the reason, recorded in the hand-act log first: nothing is ended when the
@@ -161,7 +162,8 @@ func recordTools(h *Holder, acts handActs) []stdio.Tool {
}},
{Name: "gate_records",
Description: "Every delivery record kept, oldest first: its milestone, when it opened and ended, its entries, " +
"outcomes, nodes away and pins, its size against the bus's value limit, and while open what it waits for.",
"outcomes, nodes away and pins, its size against the bus's value limit, and while open what it waits for; and " +
"the records' ledger's fill against its cap (filling past 80 %, full when no record fits, else room).",
Run: func(map[string]any) (any, error) {
if h.Records == nil {
return none()
@@ -38,22 +38,23 @@ type recordedMerges struct {
members func(group string) []string
}
// Put writes the group's merge into its delivery record first, so a ledger with no room refuses it before the merge is
// handed to the forge bridge (G §181), then into the state merges the bridge follows.
func (m recordedMerges) Put(group string, v Merge) error {
if err := m.MergeStore.Put(group, v); err != nil {
return err
if m.records != nil && len(v.Approved) > 0 {
r, _, err := m.records.Open(group, v.Approved, false)
if err != nil {
return fmt.Errorf("the merge of %s is handed but not in its delivery record: %w", group, err)
}
var members []string
if m.members != nil {
members = m.members(group)
}
if err := m.records.SetGroup(r, recordGroupOf(v, members)); err != nil {
return err
}
}
if m.records == nil || len(v.Approved) == 0 {
return nil
}
r, _, err := m.records.Open(group, v.Approved, false)
if err != nil {
return fmt.Errorf("the merge of %s is handed but not in its delivery record: %w", group, err)
}
var members []string
if m.members != nil {
members = m.members(group)
}
return m.records.SetGroup(r, recordGroupOf(v, members))
return m.MergeStore.Put(group, v)
}
// switchGroup is one group in flight as the switch finds it: its milestone, its heads and its members.