Pass a seat held once to its only claimant when its recorded holder is assigned nowhere (issue 1317, ADR 0449)

This commit is contained in:
2026-10-19 21:21:32 +00:00
parent caa3fc8995
commit 666dcff79b
7 changed files with 467 additions and 2 deletions
+21
View File
@@ -153,3 +153,24 @@ func TestAReplicatedSeatHasSeveralHoldersOnRecord(t *testing.T) {
t.Fatalf("a handover left other holders on record: %+v", held)
}
}
// A pass is a compare-and-set: it moves the record only while the holder it was read with is still on it.
func TestPassingASeatMovesTheRecordOnceAndOnlyFromTheHolderRead(t *testing.T) {
inv, ctx := twoBrokersOnTwoNodes(t)
if err := inv.HoldSeat(ctx, "mesh-broker", catalogue.ScopeMesh, "anchor", "old-broker"); err != nil {
t.Fatal(err)
}
from := catalogue.Held{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "anchor", Module: "old-broker"}
to := catalogue.Held{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "laptop", Module: "new-broker"}
passed, err := inv.PassSeat(ctx, "mesh-broker", catalogue.ScopeMesh, from, to)
if err != nil || !passed {
t.Fatalf("the seat was not passed: %v %v", passed, err)
}
held, _ := inv.Holdings(ctx)
if len(held) != 1 || held[0].Node != "laptop" || held[0].Module != "new-broker" {
t.Fatalf("the record does not name the new holder alone: %+v", held)
}
if passed, err = inv.PassSeat(ctx, "mesh-broker", catalogue.ScopeMesh, from, to); err != nil || passed {
t.Fatalf("a second pass from the same read moved the record again: %v %v", passed, err)
}
}
+42
View File
@@ -284,6 +284,48 @@ func (i *Inventory) HoldSeat(ctx context.Context, seat, scope, nodeName, module
return tx.Commit(ctx)
}
// PassSeat records `to` as the holder of a seat held once in place of `from`, only while `from` is still the
// one on record: a compare-and-set in one transaction, so two compositions reading the same record cannot both
// hand the seat over (novox/hq ADR 0449 §4). It answers false, with no error, when the record no longer names
// `from` (another composition passed it first). The new holder must be an assignment.
func (i *Inventory) PassSeat(ctx context.Context, seat, scope string, from, to catalogue.Held) (bool, error) {
fromNode, err := i.NodeByName(ctx, from.Node)
if err != nil {
return false, err
}
toNode, err := i.NodeByName(ctx, to.Node)
if err != nil {
return false, err
}
if err := i.mustBeAssigned(ctx, seat, to.Node, to.Module); err != nil {
return false, err
}
tx, err := i.store.Pool().Begin(ctx)
if err != nil {
return false, err
}
defer func() { _ = tx.Rollback(context.WithoutCancel(ctx)) }()
gone, err := tx.Exec(ctx,
`delete from seat_holding where seat = $1 and node = $2 and module = $3`,
seat, fromNode.ID, from.Module)
if err != nil {
return false, fmt.Errorf("passing %s from %s on %s: %w", seat, from.Module, from.Node, err)
}
if gone.RowsAffected() == 0 {
return false, nil
}
if _, err := tx.Exec(ctx,
`delete from seat_holding where seat = $1`, seat); err != nil {
return false, fmt.Errorf("passing %s: %w", seat, err)
}
if _, err := tx.Exec(ctx,
`insert into seat_holding (seat, scope, node, module) values ($1, $2, $3, $4)`,
seat, scope, toNode.ID, to.Module); err != nil {
return false, fmt.Errorf("recording %s on %s as the holder of %s: %w", to.Module, to.Node, seat, err)
}
return true, tx.Commit(ctx)
}
// AddSeatHolder records one more assignment holding a replicated seat, beside those already on
// record (novox/hq ADR 0223). Whether the seat may have several holders is the caller's to judge —
// the seat's definition is compiled, and the store holds no column for it. Recording a holder