Fold a pending assignment into the value of its assignment key (issue 1094)
A wait for a build is the value of <node>.<module> with state pending and a pending object (id p-<unix nanoseconds>, a string), so every reader that skips a value with a state skips it, and the controller's own lists skip it through Stands. The transitions are compare-and-set on the key's revision (claim, release, settle, repoint, mark); assigning over a wait carries it into the value that stands, applied when it was waiting; a plan pass never overwrites it; one open wait per machine and module, and a key that holds the module refuses a new one. The table is no longer read or written, ForgetEndedPending is gone (no key is removed), and a wait of a machine the store dropped is none. Callers change type only; their conditions are the next issue.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// **A pending value is not a standing assignment** (ADR 0261, ADR 0408, issue 1094): it names its node and module, holds no
|
||||
// time assigned, says `state: pending`, and every reader that skips a value with a state skips it.
|
||||
func TestAPendingValueIsNotAStandingAssignment(t *testing.T) {
|
||||
since := time.Date(2026, 10, 18, 3, 0, 0, 0, time.UTC)
|
||||
a := Assignment{Node: "anchor", Module: "notes", State: StatePending,
|
||||
Pending: &Pending{ID: "p-1", State: PendingWaiting, Build: "build-1", Since: since}}
|
||||
if a.Stands() {
|
||||
t.Fatal("a pending value stands")
|
||||
}
|
||||
raw, _ := json.Marshal(a)
|
||||
for _, want := range []string{`"node":"anchor"`, `"module":"notes"`, `"state":"pending"`, `"pending":{"id":"p-1","state":"waiting"`} {
|
||||
if !strings.Contains(string(raw), want) {
|
||||
t.Errorf("the value lacks %s: %s", want, raw)
|
||||
}
|
||||
}
|
||||
if strings.Contains(string(raw), `"assigned"`) {
|
||||
t.Errorf("a pending value names a time assigned: %s", raw)
|
||||
}
|
||||
// The wait carried into the value that stands: no state, so it stands, and the record stays.
|
||||
made := Assignment{Node: "anchor", Module: "notes", Assigned: since, Pending: &Pending{ID: "p-1", State: PendingApplied}}
|
||||
if !made.Stands() || !a.Pending.Open() || made.Pending.Open() {
|
||||
t.Fatalf("stands %v, open before %v, open after %v", made.Stands(), a.Pending.Open(), made.Pending.Open())
|
||||
}
|
||||
var back Assignment
|
||||
if err := json.Unmarshal(raw, &back); err != nil || back.Pending == nil || back.Pending.Build != "build-1" || !back.Pending.Since.Equal(since) {
|
||||
t.Fatalf("read back %+v: %v", back, err)
|
||||
}
|
||||
now := since
|
||||
raised := Pending{ID: "p-2", State: PendingExpired, Raised: &now, Acknowledged: &now, Cleared: &now}
|
||||
body, _ := json.Marshal(raised)
|
||||
if !strings.Contains(string(body), `"raised"`) || !strings.Contains(string(body), `"acknowledged"`) || !strings.Contains(string(body), `"cleared"`) {
|
||||
t.Errorf("the marks of a wait: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheTransitionsOfAPendingAssignment(t *testing.T) {
|
||||
ok := [][2]string{{PendingWaiting, PendingExpired}, {PendingWaiting, PendingWithdrawn}, {PendingApplying, PendingApplied},
|
||||
{PendingApplying, PendingRefused}}
|
||||
for _, p := range ok {
|
||||
if !CanSettle(p[0], p[1]) {
|
||||
t.Errorf("%s may end as %s", p[0], p[1])
|
||||
}
|
||||
}
|
||||
for _, p := range [][2]string{{PendingWaiting, PendingApplied}, {PendingWaiting, PendingRefused}, {PendingApplying, PendingExpired},
|
||||
{PendingApplying, PendingWithdrawn}, {PendingApplied, PendingExpired}, {PendingExpired, PendingWithdrawn},
|
||||
{PendingWaiting, PendingWaiting}} {
|
||||
if CanSettle(p[0], p[1]) {
|
||||
t.Errorf("%s may not end as %s", p[0], p[1])
|
||||
}
|
||||
}
|
||||
if !CanMove(PendingWaiting, PendingApplying) || !CanMove(PendingApplying, PendingWaiting) || CanMove(PendingApplied, PendingWaiting) {
|
||||
t.Error("a claim moves a wait to applying and back")
|
||||
}
|
||||
}
|
||||
@@ -189,9 +189,62 @@ type Assignment struct {
|
||||
|
||||
// Ended is when the assignment ended (an unassignment): absent while it stands. **No key of the chain is removed**
|
||||
// (ADR 0408 §4), so an unassigned module keeps its key, saying so; every reader treats a value with Ended, or with a
|
||||
// State, as no assignment. State is, in a later delivery, the pending assignment waiting for its build (issue 1094).
|
||||
// State, as no assignment. State is StatePending while the key holds an assignment waiting for its build, or one that
|
||||
// was never made (issue 1094).
|
||||
Ended time.Time `json:"ended,omitzero"`
|
||||
State string `json:"state,omitempty"`
|
||||
// Pending is the assignment's wait for its build (novox/hq ADR 0261), kept in the value of the key it waits for and, when
|
||||
// it is made, carried into the value that stands: the record of what became of the wait.
|
||||
Pending *Pending `json:"pending,omitempty"`
|
||||
}
|
||||
|
||||
// StatePending is the State of a value that holds no assignment but a pending one.
|
||||
const StatePending = "pending"
|
||||
|
||||
// The states of a pending assignment (novox/hq ADR 0261).
|
||||
const (
|
||||
PendingWaiting = "waiting"
|
||||
PendingApplying = "applying"
|
||||
PendingApplied = "applied"
|
||||
PendingRefused = "refused"
|
||||
PendingExpired = "expired"
|
||||
PendingWithdrawn = "withdrawn"
|
||||
)
|
||||
|
||||
// Pending is an assignment kept until its module is registered: the build it waits for, where that build is from, and the
|
||||
// times of its claim and its end. Its id is p-<unix nanoseconds of Since>, unique for a key, which holds one at a time.
|
||||
type Pending struct {
|
||||
ID string `json:"id"`
|
||||
State string `json:"state"`
|
||||
Build string `json:"build,omitempty"`
|
||||
Repository string `json:"repository,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Since time.Time `json:"since"`
|
||||
Note string `json:"note,omitempty"`
|
||||
Settled *time.Time `json:"settled,omitempty"`
|
||||
Raised *time.Time `json:"raised,omitempty"`
|
||||
Acknowledged *time.Time `json:"acknowledged,omitempty"`
|
||||
Cleared *time.Time `json:"cleared,omitempty"`
|
||||
}
|
||||
|
||||
// Open says the assignment is still to be made.
|
||||
func (p Pending) Open() bool { return p.State == PendingWaiting || p.State == PendingApplying }
|
||||
|
||||
// CanSettle says a pending assignment in state from may end in state to: a wait expires or is withdrawn, a claim becomes
|
||||
// applied or refused, and nothing else.
|
||||
func CanSettle(from, to string) bool {
|
||||
switch from {
|
||||
case PendingWaiting:
|
||||
return to == PendingExpired || to == PendingWithdrawn
|
||||
case PendingApplying:
|
||||
return to == PendingApplied || to == PendingRefused
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// CanMove says a pending assignment in state from may go to state to without ending: claimed, or released again.
|
||||
func CanMove(from, to string) bool {
|
||||
return (from == PendingWaiting && to == PendingApplying) || (from == PendingApplying && to == PendingWaiting)
|
||||
}
|
||||
|
||||
// Stands says the value is an assignment that holds: it has not ended and is not waiting in a state of its own.
|
||||
|
||||
Reference in New Issue
Block a user