Drop the plan state from the store and the assignments: migration 0108 and placement-only assignments (release 6, issue 1945 B9, B4)
This commit is contained in:
@@ -49,23 +49,23 @@ func TestABuildIsKeptWholeAndReadBackWithItsReuseKeyAndWhatItPublished(t *testin
|
||||
func TestAWriteAgainstAMovedRevisionIsRefusedAndNothingIsLaidOver(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
c := InMemory(nil)
|
||||
first, err := c.Assignments.Create(ctx, "anchor.notes", Assignment{Node: "anchor", Module: "notes", Commit: "c1"})
|
||||
first, err := c.Assignments.Create(ctx, "anchor.notes", Assignment{Node: "anchor", Module: "notes", RequiredBy: "c1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := c.Assignments.Create(ctx, "anchor.notes", Assignment{Node: "anchor", Module: "notes"}); !errors.Is(err, ErrExists) {
|
||||
t.Fatalf("a create of a key that holds a value answered %v", err)
|
||||
}
|
||||
second, err := c.Assignments.Update(ctx, "anchor.notes", Assignment{Node: "anchor", Module: "notes", Commit: "c2"}, first)
|
||||
second, err := c.Assignments.Update(ctx, "anchor.notes", Assignment{Node: "anchor", Module: "notes", RequiredBy: "c2"}, first)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// A writer that read the first revision writes now.
|
||||
if _, err := c.Assignments.Update(ctx, "anchor.notes", Assignment{Node: "anchor", Module: "notes", Commit: "stale"}, first); !errors.Is(err, ErrMoved) {
|
||||
if _, err := c.Assignments.Update(ctx, "anchor.notes", Assignment{Node: "anchor", Module: "notes", RequiredBy: "stale"}, first); !errors.Is(err, ErrMoved) {
|
||||
t.Fatalf("a write at a moved revision answered %v", err)
|
||||
}
|
||||
got, _, _ := c.Assignments.Get(ctx, "anchor.notes")
|
||||
if got.Value.Commit != "c2" || got.Revision != second {
|
||||
if got.Value.RequiredBy != "c2" || got.Revision != second {
|
||||
t.Fatalf("the key that moved was laid over: %+v at %d", got.Value, got.Revision)
|
||||
}
|
||||
}
|
||||
@@ -91,8 +91,8 @@ func TestNoKeyOfTheChainIsEverRemoved(t *testing.T) {
|
||||
}
|
||||
ctx := context.Background()
|
||||
c := InMemory(nil)
|
||||
rev, _ := c.Assignments.Create(ctx, "anchor.notes", Assignment{Node: "anchor", Module: "notes", Commit: "c1"})
|
||||
if _, err := c.Assignments.Update(ctx, "anchor.notes", Assignment{Node: "anchor", Module: "notes", Commit: "c1"}, rev); err != nil {
|
||||
rev, _ := c.Assignments.Create(ctx, "anchor.notes", Assignment{Node: "anchor", Module: "notes", RequiredBy: "c1"})
|
||||
if _, err := c.Assignments.Update(ctx, "anchor.notes", Assignment{Node: "anchor", Module: "notes", RequiredBy: "c1"}, rev); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
keys, err := c.Assignments.Keys(ctx)
|
||||
@@ -190,31 +190,36 @@ func TestARolloutNamesEveryAssignmentItChangedWithBuildBeforeAndAfter(t *testing
|
||||
}
|
||||
}
|
||||
|
||||
// **An assignment names its build, artifact or package, commit and rollout**, and keeps the fields its readers read
|
||||
// under the names they read them by (ADR 0408 §3).
|
||||
func TestAnAssignmentNamesBuildArtifactCommitAndRollout(t *testing.T) {
|
||||
// **An assignment is placement only** (ADR 0539 §12): the node, the module, since when, what requires it and its
|
||||
// provisioning plan; no build, artifact, package, commit, rollout or time since is written in it, and a value of before
|
||||
// release 6 that names them is read with them ignored.
|
||||
func TestAnAssignmentIsPlacementOnly(t *testing.T) {
|
||||
a := Assignment{Node: "anchor", Module: "notes", Assigned: at(3), RequiredBy: "wiki",
|
||||
Provisioning: json.RawMessage(`{"plan":1}`), Build: "b2", Artifact: "sha256:bb", Commit: "c2", Rollout: "r1", Since: at(4)}
|
||||
Provisioning: json.RawMessage(`{"plan":1}`)}
|
||||
raw, _ := json.Marshal(a)
|
||||
for _, want := range []string{`"node":"anchor"`, `"module":"notes"`, `"assigned":"2026-10-18T03:00:00Z"`,
|
||||
`"required-by":"wiki"`, `"provisioning":{"plan":1}`, `"build":"b2"`, `"artifact":"sha256:bb"`, `"commit":"c2"`,
|
||||
`"rollout":"r1"`, `"since":"2026-10-18T04:00:00Z"`} {
|
||||
`"required-by":"wiki"`, `"provisioning":{"plan":1}`} {
|
||||
if !strings.Contains(string(raw), want) {
|
||||
t.Errorf("the assignment's value lacks %s: %s", want, raw)
|
||||
}
|
||||
}
|
||||
for _, not := range []string{`"build"`, `"artifact"`, `"package"`, `"commit"`, `"rollout"`, `"since"`, `"pulled-in"`, `"sent-unknown"`} {
|
||||
if strings.Contains(string(raw), not) {
|
||||
t.Errorf("the assignment's value names %s: %s", not, raw)
|
||||
}
|
||||
}
|
||||
if a.Key() != "anchor.notes" {
|
||||
t.Errorf("key %s", a.Key())
|
||||
}
|
||||
// An assignment of today carries none of the new fields, and says none: a reader of today reads the bytes it did.
|
||||
raw, _ = json.Marshal(Assignment{Node: "anchor", Module: "notes", Assigned: at(3)})
|
||||
if string(raw) != `{"node":"anchor","module":"notes","assigned":"2026-10-18T03:00:00Z"}` {
|
||||
t.Errorf("an assignment without a chain is %s", raw)
|
||||
// A value written before release 6 is read, its chain fields ignored, and written back without them.
|
||||
var old Assignment
|
||||
if err := json.Unmarshal([]byte(`{"node":"anchor","module":"notes","assigned":"2026-10-18T03:00:00Z","build":"b2","commit":"c2",`+
|
||||
`"rollout":"r1","since":"2026-10-18T04:00:00Z","pulled-in":true}`), &old); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// A package in place of an artifact.
|
||||
raw, _ = json.Marshal(Assignment{Node: "a", Module: "m", Package: &Package{Name: "n", Version: "1"}})
|
||||
if !strings.Contains(string(raw), `"package":{"name":"n","version":"1"}`) {
|
||||
t.Errorf("the package is %s", raw)
|
||||
raw, _ = json.Marshal(old)
|
||||
if string(raw) != `{"node":"anchor","module":"notes","assigned":"2026-10-18T03:00:00Z"}` {
|
||||
t.Errorf("an old value written again is %s", raw)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,7 +346,7 @@ func TestAnEmptyBucketIsReadAsEmpty(t *testing.T) {
|
||||
func TestAnEndedAssignmentIsNoAssignmentAndStaysInTheBucket(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
c := InMemory(nil)
|
||||
rev, err := c.Assignments.Create(ctx, "anchor.notes", Assignment{Node: "anchor", Module: "notes", Assigned: at(1), Commit: "c1"})
|
||||
rev, err := c.Assignments.Create(ctx, "anchor.notes", Assignment{Node: "anchor", Module: "notes", Assigned: at(1), RequiredBy: "c1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -355,7 +360,7 @@ func TestAnEndedAssignmentIsNoAssignmentAndStaysInTheBucket(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, found, _ := c.Assignments.Get(ctx, "anchor.notes")
|
||||
if !found || got.Value.Stands() || got.Value.Commit != "c1" || got.Value.Ended.IsZero() {
|
||||
if !found || got.Value.Stands() || got.Value.RequiredBy != "c1" || got.Value.Ended.IsZero() {
|
||||
t.Fatalf("the ended value is %+v (found %v)", got.Value, found)
|
||||
}
|
||||
if history, _ := c.Assignments.History(ctx, "anchor.notes"); len(history) != 2 || !history[0].Value.Stands() {
|
||||
|
||||
@@ -61,27 +61,3 @@ func TestTheTransitionsOfAPendingAssignment(t *testing.T) {
|
||||
t.Error("a claim moves a wait to applying and back")
|
||||
}
|
||||
}
|
||||
|
||||
// **A module only pulled in is a standing value marked pulled-in, never a state, and a hand send makes a key say its commit
|
||||
// unknown with a time** (issue 1109): every reader treats a non-empty state as no assignment, so a pulled-in key must have
|
||||
// none; and unknown is never written in `commit`.
|
||||
func TestAPulledInValueStandsAndAHandSendIsATimeNotACommit(t *testing.T) {
|
||||
at := time.Date(2026, 10, 18, 3, 0, 0, 0, time.UTC)
|
||||
pulled := Assignment{Node: "anchor", Module: "mesh-wireguard", PulledIn: true, Commit: "c1", Build: "b1", Since: at}
|
||||
if !pulled.Stands() || pulled.State != "" {
|
||||
t.Fatalf("a pulled-in value does not stand: %+v", pulled)
|
||||
}
|
||||
raw, _ := json.Marshal(pulled)
|
||||
if !strings.Contains(string(raw), `"pulled-in":true`) || strings.Contains(string(raw), `"state"`) || strings.Contains(string(raw), `"assigned"`) {
|
||||
t.Fatalf("the pulled-in value is %s", raw)
|
||||
}
|
||||
unknown := Assignment{Node: "anchor", Module: "notes", Assigned: at, SentUnknown: at}
|
||||
raw, _ = json.Marshal(unknown)
|
||||
if !strings.Contains(string(raw), `"sent-unknown":"2026-10-18T03:00:00Z"`) || strings.Contains(string(raw), `"commit"`) || !unknown.Stands() {
|
||||
t.Fatalf("the hand-sent value is %s", raw)
|
||||
}
|
||||
plain, _ := json.Marshal(Assignment{Node: "anchor", Module: "notes", Assigned: at})
|
||||
if strings.Contains(string(plain), "pulled-in") || strings.Contains(string(plain), "sent-unknown") {
|
||||
t.Fatalf("a value that is neither says so: %s", plain)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,9 +175,10 @@ type Rollout struct {
|
||||
Links
|
||||
}
|
||||
|
||||
// Assignment is one module on one node, kept under `<node>.<module>`. The first five fields are the ones its readers
|
||||
// have always read, with the names they have always had (novox/hq ADR 0408 §3); the rest name the chain: the build, the
|
||||
// artifact (by digest) or the package it runs, its commit, the rollout that put it there and since when.
|
||||
// Assignment is one module on one node, kept under `<node>.<module>`: placement only (novox/hq ADR 0539 §12). The first
|
||||
// five fields are the ones its readers have always read, with the names they have always had (novox/hq ADR 0408 §3);
|
||||
// no build, commit or rollout is written in it any more, and a value written before release 6 that names them is read
|
||||
// with them ignored.
|
||||
type Assignment struct {
|
||||
Node string `json:"node"`
|
||||
Module string `json:"module"`
|
||||
@@ -185,13 +186,6 @@ type Assignment struct {
|
||||
RequiredBy string `json:"required-by,omitempty"`
|
||||
Provisioning json.RawMessage `json:"provisioning,omitempty"`
|
||||
|
||||
Build string `json:"build,omitempty"`
|
||||
Artifact string `json:"artifact,omitempty"`
|
||||
Package *Package `json:"package,omitempty"`
|
||||
Commit string `json:"commit,omitempty"`
|
||||
Rollout string `json:"rollout,omitempty"`
|
||||
Since time.Time `json:"since,omitzero"`
|
||||
|
||||
// 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 StatePending while the key holds an assignment waiting for its build, or one that
|
||||
@@ -201,14 +195,6 @@ type Assignment struct {
|
||||
// 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"`
|
||||
|
||||
// PulledIn marks a standing value of a module that is on the machine only because another requires it, or because a
|
||||
// send carried it: no time assigned, but a key all the same, so what the machine was sent of it is kept and read
|
||||
// (issue 1109). Never a state: every reader treats a non-empty state as no assignment, and would take it off the machine.
|
||||
PulledIn bool `json:"pulled-in,omitempty"`
|
||||
// SentUnknown is when a send by hand made what the machine runs unknown (a declaration sent with no builds known): the
|
||||
// commit is cleared with it. Absent normally; a send that names its builds clears it.
|
||||
SentUnknown time.Time `json:"sent-unknown,omitzero"`
|
||||
}
|
||||
|
||||
// StatePending is the State of a value that holds no assignment but a pending one.
|
||||
|
||||
Reference in New Issue
Block a user