The seat-list finding is read from the node's own state, not kept in memory (issue 1966)
A new node-engine process cleared nothing at its first full read of the seat list, because the Waiter remembered in memory whether it had written the finding; ace and g14 kept the condition for ever. The Waiter now asks the node's state findings whether the finding stands, clears it at the first full read and writes it at the bound only when it does not stand. Builds: design 47 H-7, ADR 0636 §1 (the finding is cleared at the first full read), issue 1966 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -126,12 +126,18 @@ func (w findingsOfTheNode) Put(f views.Finding) error {
|
|||||||
return w.session.PutFinding(key, raw)
|
return w.session.PutFinding(key, raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// keyOf is the finding's key in the state; a read or a clear needs no more of the finding than Kind and ID.
|
||||||
|
func (w findingsOfTheNode) keyOf(f views.Finding) string {
|
||||||
|
return conditions.Key(conditions.ScopeMachine, f.ID, f.Kind)
|
||||||
|
}
|
||||||
|
|
||||||
func (w findingsOfTheNode) Delete(f views.Finding) error {
|
func (w findingsOfTheNode) Delete(f views.Finding) error {
|
||||||
key, _, err := w.encode(f)
|
return w.session.DeleteFinding(w.keyOf(f))
|
||||||
if err != nil {
|
}
|
||||||
return err
|
|
||||||
}
|
// Has says whether the finding stands in the node's state `findings` now.
|
||||||
return w.session.DeleteFinding(key)
|
func (w findingsOfTheNode) Has(f views.Finding) (bool, error) {
|
||||||
|
return w.session.HasFinding(w.keyOf(f))
|
||||||
}
|
}
|
||||||
|
|
||||||
// startSeatList starts this run's reader: its bootstrapped seats are given to catalogue at once, and every change of what
|
// startSeatList starts this run's reader: its bootstrapped seats are given to catalogue at once, and every change of what
|
||||||
|
|||||||
@@ -42,6 +42,21 @@ func (q *Session) PutFinding(key string, value []byte) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasFinding says whether one finding of this engine stands in its state now.
|
||||||
|
func (q *Session) HasFinding(key string) (bool, error) {
|
||||||
|
kv, err := q.findings()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if _, err = kv.Get(key); err != nil {
|
||||||
|
if errors.Is(err, nats.ErrKeyNotFound) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteFinding clears one finding of this engine over the link open now; a finding already gone is cleared.
|
// DeleteFinding clears one finding of this engine over the link open now; a finding already gone is cleared.
|
||||||
func (q *Session) DeleteFinding(key string) error {
|
func (q *Session) DeleteFinding(key string) error {
|
||||||
kv, err := q.findings()
|
kv, err := q.findings()
|
||||||
|
|||||||
@@ -36,20 +36,32 @@ type Clock interface {
|
|||||||
AfterFunc(d time.Duration, f func()) Timer
|
AfterFunc(d time.Duration, f func()) Timer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Findings is the node's own state `findings`: the writer, and the read of whether a finding stands there. The bus state
|
||||||
|
// is the one truth of whether the finding exists (ADR 0636 §1, issue 1966): the Waiter keeps no copy, so a new process
|
||||||
|
// of the node-engine clears what an earlier one wrote.
|
||||||
|
type Findings interface {
|
||||||
|
views.FindingWriter
|
||||||
|
Has(f views.Finding) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
// Waiter writes and clears the node's finding.
|
// Waiter writes and clears the node's finding.
|
||||||
type Waiter struct {
|
type Waiter struct {
|
||||||
Node string
|
Node string
|
||||||
Observer string
|
Observer string
|
||||||
Reader Reader
|
Reader Reader
|
||||||
Findings views.FindingWriter
|
Findings Findings
|
||||||
Clock Clock
|
Clock Clock
|
||||||
// Say tells the node's journal; nil says nothing.
|
// Say tells the node's journal; nil says nothing.
|
||||||
Say func(string)
|
Say func(string)
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
timer Timer
|
timer Timer
|
||||||
since time.Time
|
since time.Time
|
||||||
written *views.Finding
|
}
|
||||||
|
|
||||||
|
// key is the finding this node's Waiter writes: Kind and ID name it, the rest is its text.
|
||||||
|
func (w *Waiter) key() views.Finding {
|
||||||
|
return views.Finding{Observer: w.Observer, Kind: Kind, ID: w.Node}
|
||||||
}
|
}
|
||||||
|
|
||||||
func waiting(st seatlist.Status) bool { return !st.Heard || len(st.Waiting) > 0 }
|
func waiting(st seatlist.Status) bool { return !st.Heard || len(st.Waiting) > 0 }
|
||||||
@@ -66,18 +78,24 @@ func (w *Waiter) Changed() {
|
|||||||
w.timer = nil
|
w.timer = nil
|
||||||
}
|
}
|
||||||
w.since = time.Time{}
|
w.since = time.Time{}
|
||||||
if w.written != nil {
|
stands, err := w.Findings.Has(w.key())
|
||||||
if err := w.Findings.Delete(*w.written); err != nil {
|
if err != nil {
|
||||||
|
w.say(fmt.Sprintf("the node's finding that its seat list is unread cannot be read: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if stands {
|
||||||
|
if err := w.Findings.Delete(w.key()); err != nil {
|
||||||
w.say(fmt.Sprintf("the node's finding that its seat list is unread cannot be cleared: %v", err))
|
w.say(fmt.Sprintf("the node's finding that its seat list is unread cannot be cleared: %v", err))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
w.written = nil
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if w.timer != nil || w.written != nil {
|
if w.timer != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if stands, err := w.Findings.Has(w.key()); err == nil && stands {
|
||||||
|
return // an earlier process wrote it and the list is still unread: it stands until the first full read
|
||||||
|
}
|
||||||
w.since = w.Clock.Now()
|
w.since = w.Clock.Now()
|
||||||
w.timer = w.Clock.AfterFunc(Bound, w.expired)
|
w.timer = w.Clock.AfterFunc(Bound, w.expired)
|
||||||
}
|
}
|
||||||
@@ -86,7 +104,11 @@ func (w *Waiter) Changed() {
|
|||||||
func (w *Waiter) Waiting() (bool, time.Time) {
|
func (w *Waiter) Waiting() (bool, time.Time) {
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
defer w.mu.Unlock()
|
defer w.mu.Unlock()
|
||||||
return w.timer != nil || w.written != nil, w.since
|
if w.timer != nil {
|
||||||
|
return true, w.since
|
||||||
|
}
|
||||||
|
stands, err := w.Findings.Has(w.key())
|
||||||
|
return err == nil && stands, w.since
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Waiter) expired() {
|
func (w *Waiter) expired() {
|
||||||
@@ -98,6 +120,9 @@ func (w *Waiter) expired() {
|
|||||||
w.since = time.Time{}
|
w.since = time.Time{}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if stands, err := w.Findings.Has(w.key()); err == nil && stands {
|
||||||
|
return
|
||||||
|
}
|
||||||
f := views.Finding{Observer: w.Observer, Kind: Kind, ID: w.Node,
|
f := views.Finding{Observer: w.Observer, Kind: Kind, ID: w.Node,
|
||||||
Summary: fmt.Sprintf("%s has read no complete seat list since %s: %s", w.Node, w.since.UTC().Format(time.RFC3339), st.Health()),
|
Summary: fmt.Sprintf("%s has read no complete seat list since %s: %s", w.Node, w.since.UTC().Format(time.RFC3339), st.Health()),
|
||||||
Headline: fmt.Sprintf("%s waits for the seat list", w.Node),
|
Headline: fmt.Sprintf("%s waits for the seat list", w.Node),
|
||||||
@@ -107,7 +132,6 @@ func (w *Waiter) expired() {
|
|||||||
w.say(fmt.Sprintf("the node's finding that its seat list is unread cannot be written: %v", err))
|
w.say(fmt.Sprintf("the node's finding that its seat list is unread cannot be written: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.written = &f
|
|
||||||
w.say(f.Summary)
|
w.say(f.Summary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,16 +48,24 @@ func (c *fakeClock) advance(d time.Duration) {
|
|||||||
type fakeFindings struct {
|
type fakeFindings struct {
|
||||||
put, deleted []views.Finding
|
put, deleted []views.Finding
|
||||||
err error
|
err error
|
||||||
|
standing bool // the bus state holds the finding, written by this process or an earlier one
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fakeFindings) Has(views.Finding) (bool, error) { return f.standing, nil }
|
||||||
|
|
||||||
func (f *fakeFindings) Put(x views.Finding) error {
|
func (f *fakeFindings) Put(x views.Finding) error {
|
||||||
if f.err != nil {
|
if f.err != nil {
|
||||||
return f.err
|
return f.err
|
||||||
}
|
}
|
||||||
f.put = append(f.put, x)
|
f.put = append(f.put, x)
|
||||||
|
f.standing = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (f *fakeFindings) Delete(x views.Finding) error {
|
||||||
|
f.deleted = append(f.deleted, x)
|
||||||
|
f.standing = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (f *fakeFindings) Delete(x views.Finding) error { f.deleted = append(f.deleted, x); return nil }
|
|
||||||
|
|
||||||
// ADR 0636 §1: a reader that waited five minutes makes the node write one finding, which the first full read clears.
|
// ADR 0636 §1: a reader that waited five minutes makes the node write one finding, which the first full read clears.
|
||||||
func caseTheNodeWritesOneFindingAfterFiveMinutesAndClearsItAtTheFirstFullRead(t *testing.T) {
|
func caseTheNodeWritesOneFindingAfterFiveMinutesAndClearsItAtTheFirstFullRead(t *testing.T) {
|
||||||
@@ -137,12 +145,35 @@ func caseAFindingThatCannotBeWrittenIsSaidAndTriedAgain(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A finding an earlier process wrote stands in the bus state: the new process clears it at its first full read with no
|
||||||
|
// write first, and while the list stays unread it neither starts a bound nor writes it again (ADR 0636 §1, issue 1966).
|
||||||
|
func caseAFindingOfAnEarlierProcessIsClearedAtTheFirstFullRead(t *testing.T) {
|
||||||
|
reader := &fakeReader{st: seatlist.Status{Heard: false}}
|
||||||
|
clock := &fakeClock{now: time.Date(2026, 11, 11, 12, 0, 0, 0, time.UTC)}
|
||||||
|
findings := &fakeFindings{standing: true}
|
||||||
|
w := &Waiter{Node: "ace", Observer: "node-engine", Reader: reader, Findings: findings, Clock: clock}
|
||||||
|
w.Changed()
|
||||||
|
clock.advance(Bound)
|
||||||
|
if len(findings.put) != 0 || len(clock.timers) != 0 {
|
||||||
|
t.Fatalf("a standing finding was written again (%+v) or a bound started (%d)", findings.put, len(clock.timers))
|
||||||
|
}
|
||||||
|
if on, _ := w.Waiting(); !on {
|
||||||
|
t.Error("the waiter does not say the finding stands")
|
||||||
|
}
|
||||||
|
reader.st = seatlist.Status{Heard: true}
|
||||||
|
w.Changed()
|
||||||
|
if len(findings.deleted) != 1 || findings.deleted[0].ID != "ace" || len(findings.put) != 0 {
|
||||||
|
t.Fatalf("the first full read left the earlier process's finding: deleted %+v, put %+v", findings.deleted, findings.put)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ADR 0636 §1, the node's finding of a seat list unread.
|
// ADR 0636 §1, the node's finding of a seat list unread.
|
||||||
func TestTheSeatListWait(t *testing.T) {
|
func TestTheSeatListWait(t *testing.T) {
|
||||||
for name, run := range map[string]func(*testing.T){
|
for name, run := range map[string]func(*testing.T){
|
||||||
"five minutes write one finding and the first full read clears it": caseTheNodeWritesOneFindingAfterFiveMinutesAndClearsItAtTheFirstFullRead,
|
"five minutes write one finding and the first full read clears it": caseTheNodeWritesOneFindingAfterFiveMinutesAndClearsItAtTheFirstFullRead,
|
||||||
"a read within the bound writes nothing": caseAReadWithinTheBoundWritesNothing,
|
"a read within the bound writes nothing": caseAReadWithinTheBoundWritesNothing,
|
||||||
"a finding that cannot be written is said and tried again": caseAFindingThatCannotBeWrittenIsSaidAndTriedAgain,
|
"a finding that cannot be written is said and tried again": caseAFindingThatCannotBeWrittenIsSaidAndTriedAgain,
|
||||||
|
"a finding of an earlier process is cleared at the first full read": caseAFindingOfAnEarlierProcessIsClearedAtTheFirstFullRead,
|
||||||
} {
|
} {
|
||||||
t.Run(name, run)
|
t.Run(name, run)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user