Keep a condition's looks in the store, so a look that changes nothing writes no bucket (issue 420)
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered

Every look rewrote the condition's value in the conditions bucket, because the
value carried the count of looks, the last look and the newest evidence: 131 647
writes in four days for twelve open conditions, against ADR 0289's rule that a
bucket is written only when what it describes changed. The looks now live in the
controller's store (migration 0095, condition_looks), the bucket holds what a
condition says and is written only when that changes, and the conditions verb and
the condition events put the looks back.
This commit is contained in:
jochen
2026-10-12 12:11:13 +02:00
parent 22a2f73f7b
commit dcdafd37f0
18 changed files with 803 additions and 38 deletions
+90
View File
@@ -0,0 +1,90 @@
package inventory
import (
"context"
"encoding/json"
"errors"
"time"
"github.com/jackc/pgx/v5"
)
// The looks of the open conditions (novox/hq issue 420): see migration 0095. How many times each was
// observed, when last, and the newest evidence change on every look, so they are kept here rather than in
// the conditions bucket, which is written only when what a condition says changes (ADR 0289 §1).
// ConditionLooks is one open condition's looks. Evidence is kept as the condition says it, newest first.
type ConditionLooks struct {
Key string
Observations int
LastObserved time.Time
Evidence json.RawMessage
}
// KeepConditionLooks writes a condition's looks, replacing what its row held. Written under the lease: a
// controller that may not act keeps no looks.
func (i *Inventory) KeepConditionLooks(ctx context.Context, l ConditionLooks) error {
if l.Key == "" {
return errors.New("a condition's looks name its key")
}
epoch, err := i.actingEpoch(ctx)
if err != nil {
return err
}
evidence := l.Evidence
if len(evidence) == 0 {
evidence = json.RawMessage("[]")
}
_, err = i.store.Pool().Exec(ctx,
`insert into condition_looks (key, observations, last_observed, evidence, epoch, kept_at)
values ($1, $2, $3, $4, $5, now())
on conflict (key) do update set observations = excluded.observations,
last_observed = excluded.last_observed, evidence = excluded.evidence, epoch = excluded.epoch,
kept_at = excluded.kept_at`,
l.Key, l.Observations, l.LastObserved, []byte(evidence), epoch)
return err
}
// ConditionLooksOf is one condition's looks; false when none are kept.
func (i *Inventory) ConditionLooksOf(ctx context.Context, key string) (ConditionLooks, bool, error) {
l := ConditionLooks{Key: key}
var evidence []byte
err := i.store.Pool().QueryRow(ctx,
`select observations, last_observed, evidence from condition_looks where key = $1`, key).
Scan(&l.Observations, &l.LastObserved, &evidence)
if errors.Is(err, pgx.ErrNoRows) {
return ConditionLooks{}, false, nil
}
if err != nil {
return ConditionLooks{}, false, err
}
l.Evidence = evidence
return l, true, nil
}
// AllConditionLooks is every kept condition's looks, by key.
func (i *Inventory) AllConditionLooks(ctx context.Context) (map[string]ConditionLooks, error) {
rows, err := i.store.Pool().Query(ctx, `select key, observations, last_observed, evidence from condition_looks`)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string]ConditionLooks{}
for rows.Next() {
var l ConditionLooks
var evidence []byte
if err := rows.Scan(&l.Key, &l.Observations, &l.LastObserved, &evidence); err != nil {
return nil, err
}
l.Evidence = evidence
out[l.Key] = l
}
return out, rows.Err()
}
// ForgetConditionLooks removes a condition's looks: it cleared. Whatever the lease says by now, as a
// clearing is: what is not open has nothing to count.
func (i *Inventory) ForgetConditionLooks(ctx context.Context, key string) error {
_, err := i.store.Pool().Exec(ctx, `delete from condition_looks where key = $1`, key)
return err
}
+53
View File
@@ -0,0 +1,53 @@
package inventory
import (
"context"
"encoding/json"
"errors"
"testing"
"time"
)
// **A condition's looks are kept in the store, replaced on every look and forgotten when it clears**
// (novox/hq issue 420): written under the lease, read back whole.
func TestAConditionsLooksAreKeptBesideTheBucket(t *testing.T) {
inv := ForTest(t)
ctx := t.Context()
inv.ActsUnder(func(context.Context) (uint64, error) { return 0, errors.New("the lease is not held") })
if err := inv.KeepConditionLooks(ctx, ConditionLooks{Key: "machine.ace.silent", Observations: 1,
LastObserved: time.Now()}); err == nil {
t.Fatal("looks were kept by a process that may not act")
}
inv.ActsUnder(func(context.Context) (uint64, error) { return 57, nil })
at := time.Now().UTC().Truncate(time.Microsecond)
for n := 1; n <= 3; n++ {
evidence, _ := json.Marshal([]map[string]any{{"at": at, "said": "ace has not been heard from"}})
if err := inv.KeepConditionLooks(ctx, ConditionLooks{Key: "machine.ace.silent", Observations: n,
LastObserved: at, Evidence: evidence}); err != nil {
t.Fatal(err)
}
}
if err := inv.KeepConditionLooks(ctx, ConditionLooks{Key: "machine.ace.late", Observations: 2,
LastObserved: at}); err != nil {
t.Fatal(err)
}
got, kept, err := inv.ConditionLooksOf(ctx, "machine.ace.silent")
if err != nil || !kept || got.Observations != 3 || !got.LastObserved.Equal(at) {
t.Fatalf("read %+v kept %v: %v", got, kept, err)
}
var evidence []map[string]any
if err := json.Unmarshal(got.Evidence, &evidence); err != nil || len(evidence) != 1 {
t.Fatalf("evidence %s: %v", got.Evidence, err)
}
all, err := inv.AllConditionLooks(ctx)
if err != nil || len(all) != 2 || all["machine.ace.late"].Observations != 2 ||
string(all["machine.ace.late"].Evidence) != "[]" {
t.Fatalf("all %+v: %v", all, err)
}
if err := inv.ForgetConditionLooks(ctx, "machine.ace.silent"); err != nil {
t.Fatal(err)
}
if _, kept, err := inv.ConditionLooksOf(ctx, "machine.ace.silent"); kept || err != nil {
t.Fatalf("a cleared condition's looks kept %v: %v", kept, err)
}
}
@@ -0,0 +1,20 @@
-- The looks of an open condition are kept here, not in the conditions bucket (novox/hq issue 420, ADR 0289
-- §1 and §4).
--
-- On 2026-10-10 the bus had stored 131 647 writes to the conditions bucket in four days while twelve
-- conditions were open: every look rewrote a condition's value, because the value carried how many times it
-- had been observed, when last and the newest evidence. A watcher heard a write for every look, not for
-- every change. The bucket now holds what a condition says, written when that changes; how often and when
-- it was last seen, and what each look said, change on every look and are kept in this row, written by the
-- serving controller under its lease (`epoch`) and read back by `conditions` and the events it says.
--
-- One row per open condition, removed when it clears. `evidence` is the newest observations, newest first,
-- as the condition's `evidence` says them: [{"at", "said"}].
create table condition_looks (
key text primary key,
observations integer not null check (observations >= 0),
last_observed timestamptz not null,
evidence jsonb not null default '[]',
epoch bigint,
kept_at timestamptz not null default now()
);