M-6 fix: each carried binding is said as binding.changed (issue 1409)
Review round 1, finding 2: a reader's first view taken while the carry-over runs is brought up to date by the event said after each key is written. Builds: design 39 bus-and-switch §12, §21 Claude-Session: https://claude.ai/code/session_01NcbgfHb9d4T9nq5Q6WqdiJ
This commit is contained in:
@@ -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
|
||||
|
||||
+20
-2
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user