The licence manager keeps its data on the bus and switches licences by itself at 99% (issue 1409) #657

Merged
mesh-tools merged 32 commits from feat/39-licence-switch into main 2026-11-12 00:13:05 +00:00
3 changed files with 42 additions and 4 deletions
Showing only changes of commit 6a57bb7218 - Show all commits
@@ -312,14 +312,23 @@ func (co *carryOver) bindings(ctx context.Context) (CarriedTable, error) {
v := BindingState{Licence: r.Licence, Kind: kinds[r.Licence], Generation: r.Generation,
Since: co.now.UTC().Format(time.RFC3339), Why: "carried over", By: Itself}
var held BindingState
if ok, err := co.s.get(co.s.BindingsState, r.Consumer, &held); err != nil {
ok, err := co.s.get(co.s.BindingsState, r.Consumer, &held)
if err != nil {
return t, err
} else if ok && held.Licence == v.Licence && held.Generation == v.Generation && held.Why == v.Why {
}
if ok && held.Licence == v.Licence && held.Generation == v.Generation && held.Why == v.Why {
v.Since = held.Since
}
if _, err := co.s.BindingsState.Put(r.Consumer, v); err != nil {
return t, err
}
// Said after its key is written, as every binding's change is (§21), so a reader's first view taken during the
// carry-over is brought up to date by the event.
from := ""
if ok && held.Licence != v.Licence {
from = held.Licence
}
co.m.changed(Binding{Consumer: r.Consumer, Licence: v.Licence, Kind: v.Kind, Generation: v.Generation, Why: v.Why, By: v.By}, from)
t.Written++
}
return t, nil
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"regexp"
"strings"
@@ -145,6 +146,16 @@ func TestEveryRowIsCarriedOver(t *testing.T) {
if len(mm.audit.under("carried")) != 1 || !c.Ended() {
t.Fatal("the carry-over did not say it carried, or did not end")
}
// §21: each carried binding is said as binding.changed after its key is written.
said := map[string]bool{}
for _, e := range mm.changedSince(0) {
if e["why"] == "carried over" && e["key"] == KeyFor(fmt.Sprint(e["licence"])) {
said[fmt.Sprint(e["consumer"])] = true
}
}
if !said["laptop"] || !said["server"] || len(mm.changedSince(0)) != 2 {
t.Fatalf("the carry-over said %v", mm.changedSince(0))
}
}
// design 39 bus-and-switch §13/AC1: a second start, with carried.postgres present, never opens the database and
@@ -945,9 +945,24 @@ func TestTodaysRowsAreCarriedOverOnceAndWhole(t *testing.T) {
if _, err := conn.Exec(ctx, `alter table offered rename column reason to why`); err != nil {
t.Fatal(err)
}
m.mu.Lock()
beforeSecond := len(m.of("binding.changed"))
m.mu.Unlock()
second := m.start()
m.until("holdings.asked after the carry-over", func() bool { return m.count(HoldingsAsked) > 0 })
rec := second.carried()
// §21: every carried binding is said by the start that carries it whole (the stopped one said those it wrote), so a reader's first view taken during the carry-over is brought up to date.
m.mu.Lock()
carriedSaid := 0
for _, c := range m.of("binding.changed")[beforeSecond:] {
if c["why"] == "carried over" && c["licence"] == licenceA {
carriedSaid++
}
}
m.mu.Unlock()
if carriedSaid != 4 {
t.Errorf("the carry-over said %d binding.changed, not one per binding", carriedSaid)
}
want := map[string][2]int{"licence": {2, 2}, "binding": {4, 4}, "lease": {1, 0}, "offered": {2, 1}, "usage": {2, 2}, "audit": {4, 4}}
for table, n := range want {
if got := rec.Tables[table]; got.Read != n[0] || got.Written != n[1] {
@@ -1017,14 +1032,17 @@ func TestAReadingAtTheThresholdMovesEveryConsumer(t *testing.T) {
m := newWholeMesh(t, dbURL)
p := m.start()
m.until("holdings.asked after the carry-over", func() bool { return m.count(HoldingsAsked) > 0 })
m.mu.Lock()
carried := m.count("binding.changed")
m.mu.Unlock()
m.vendor.set("at-b", 10, 20)
m.vendor.set("at-a", 99, 50)
p.must("licences_read_usage", map[string]any{"licence": licenceB})
p.must("licences_read_usage", map[string]any{"licence": licenceA})
m.until("the four consumers moved", func() bool { return m.count("binding.changed") >= 4 })
m.until("the four consumers moved", func() bool { return m.count("binding.changed") >= carried+4 })
m.mu.Lock()
switched, told := m.of("licence.switched"), m.of("ask seat:operator-channel.tell")
changed := m.of("binding.changed")
changed := m.of("binding.changed")[carried:]
m.mu.Unlock()
if len(switched) != 1 || switched[0]["from"] != licenceA || switched[0]["to"] != licenceB || switched[0]["by"] != "usage" ||
len(switched[0]["consumers"].([]any)) != 4 {