End an unassigned key with ended and remove no key of the chain (issue 1093)
ADR 0408 section 4 says no key of the chain is removed, a delete marker included. The Backend loses Remove and the Chain RemoveAssignment. An unassignment, and the end of a value only pulled in by another, write the value again with ended set, by compare-and-set; assigning it again writes a fresh standing value over it. A value with ended or a state is no assignment (Assignment.Stands) to the assignment lists, the joins, forgetting a module, seat holdings, the given values, the move and the assignment-changed event.
This commit is contained in:
@@ -335,51 +335,57 @@ func TestAnEmptyBucketIsReadAsEmpty(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// **The one removal the chain has ends an assignment's key with a marker and keeps its earlier values** (issue 1093,
|
||||
// ADR 0408 §5): the key reads as absent, can be made again, builds and rollouts have no removal, and a controller without
|
||||
// the lease removes nothing.
|
||||
func TestAnAssignmentsKeyEndsWithAMarkerAndKeepsItsHistory(t *testing.T) {
|
||||
// **An assignment that has ended still stands in the bucket as a value that says so, and nothing in the chain can remove a
|
||||
// key** (ADR 0408 §4, issue 1093): the value with `ended` is no assignment to a reader, and the earlier value stays in
|
||||
// history.
|
||||
func TestAnEndedAssignmentIsNoAssignmentAndStaysInTheBucket(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
var away error
|
||||
c := New(NewMemory(), NewMemory(), NewMemory(), func(context.Context) error { return away })
|
||||
rev, err := c.Assignments.Create(ctx, "anchor.notes", Assignment{Node: "anchor", Module: "notes", Commit: "c1"})
|
||||
c := InMemory(nil)
|
||||
rev, err := c.Assignments.Create(ctx, "anchor.notes", Assignment{Node: "anchor", Module: "notes", Assigned: at(1), Commit: "c1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
away = errors.New("another controller holds the lease")
|
||||
if err := c.RemoveAssignment(ctx, "anchor.notes", rev); !errors.Is(err, ErrNotHeld) {
|
||||
t.Fatalf("a removal without the lease answered %v", err)
|
||||
standing, _, _ := c.Assignments.Get(ctx, "anchor.notes")
|
||||
if !standing.Value.Stands() {
|
||||
t.Fatal("a value that has not ended does not stand")
|
||||
}
|
||||
away = nil
|
||||
if err := c.RemoveAssignment(ctx, "anchor.notes", rev+1); !errors.Is(err, ErrMoved) {
|
||||
t.Fatalf("a removal at a revision that moved answered %v", err)
|
||||
}
|
||||
if err := c.RemoveAssignment(ctx, "anchor.notes", rev); err != nil {
|
||||
ended := standing.Value
|
||||
ended.Ended = at(2)
|
||||
if _, err := c.Assignments.Update(ctx, "anchor.notes", ended, rev); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok, _ := c.Assignments.Get(ctx, "anchor.notes"); ok {
|
||||
t.Fatal("a removed key reads as present")
|
||||
got, found, _ := c.Assignments.Get(ctx, "anchor.notes")
|
||||
if !found || got.Value.Stands() || got.Value.Commit != "c1" || got.Value.Ended.IsZero() {
|
||||
t.Fatalf("the ended value is %+v (found %v)", got.Value, found)
|
||||
}
|
||||
if keys, _ := c.Assignments.Keys(ctx); len(keys) != 0 {
|
||||
t.Fatalf("keys %v", keys)
|
||||
}
|
||||
if all, _ := c.Assignments.All(ctx); len(all) != 0 {
|
||||
t.Fatalf("all %v", all)
|
||||
}
|
||||
if history, _ := c.Assignments.History(ctx, "anchor.notes"); len(history) != 1 || history[0].Value.Commit != "c1" {
|
||||
if history, _ := c.Assignments.History(ctx, "anchor.notes"); len(history) != 2 || !history[0].Value.Stands() {
|
||||
t.Fatalf("the earlier values are %+v", history)
|
||||
}
|
||||
if _, err := c.Assignments.Create(ctx, "anchor.notes", Assignment{Node: "anchor", Module: "notes"}); err != nil {
|
||||
t.Fatalf("a removed key could not be made again: %v", err)
|
||||
if (Assignment{Node: "a", Module: "m", Assigned: at(1), State: "pending"}).Stands() {
|
||||
t.Fatal("a pending value stands")
|
||||
}
|
||||
if _, ok := any(c.Builds).(interface {
|
||||
Remove(context.Context, string, uint64) error
|
||||
}); ok {
|
||||
t.Fatal("builds can be removed")
|
||||
raw, _ := json.Marshal(ended)
|
||||
if !strings.Contains(string(raw), `"ended":"2026-10-18T02:00:00Z"`) {
|
||||
t.Fatalf("the ended value is %s", raw)
|
||||
}
|
||||
if _, ok := any(c.Rollouts).(interface {
|
||||
// And a value that stands says nothing of either, so a reader of today reads the bytes it did.
|
||||
raw, _ = json.Marshal(Assignment{Node: "a", Module: "m", Assigned: at(1)})
|
||||
if strings.Contains(string(raw), "ended") || strings.Contains(string(raw), "state") {
|
||||
t.Fatalf("a standing value says %s", raw)
|
||||
}
|
||||
// No bucket, and no chain, has a removal.
|
||||
type remover interface {
|
||||
Remove(context.Context, string, uint64) error
|
||||
}); ok {
|
||||
t.Fatal("rollouts can be removed")
|
||||
}
|
||||
for name, x := range map[string]any{"builds": c.Builds, "rollouts": c.Rollouts, "assignments": c.Assignments, "chain": c,
|
||||
"a backend": NewMemory()} {
|
||||
if _, has := x.(remover); has {
|
||||
t.Errorf("%s can remove a key", name)
|
||||
}
|
||||
if _, has := x.(interface {
|
||||
RemoveAssignment(context.Context, string, uint64) error
|
||||
}); has {
|
||||
t.Errorf("%s can remove an assignment", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user