lifetimes witnesses: the synthetic (1, 9) is explained, and the interleave observes the slot retired when the tombstone's delete is issued
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 files changed,
+28
−6
internal/console/lifetimes_e2e_test.go+21 −3internal/host/movesoffline_lifetime_test.go+7 −3
diff --git a/internal/console/lifetimes_e2e_test.go b/internal/console/lifetimes_e2e_test.go| index c04e20ffd..37599927a 100644 |
| --- a/internal/console/lifetimes_e2e_test.go |
| +++ b/internal/console/lifetimes_e2e_test.go |
| @@ -385,11 +385,29 @@ func TestDelayedCreateReplayMoveAndRetirementInterleave(t *testing.T) { |
| t.Fatalf("the uk box minted a row after the late create: %+v", rt) |
| } |
| - // Recovery: the slot retired, then the tombstone removed. |
| + // Recovery: the slot retired, then the tombstone removed. The slot |
| + // is read at the moment each conditional delete of the key is |
| + // issued, so the order is observed, not inferred from the end state. |
| + var atDelete []string |
| + l.bucket.setHook(func(op, k string) error { |
| + if op == "delete-if-match" && k == key { |
| + slot, _, err := provision.ReadHostIncarnation(context.Background(), l.fx.rel, ltOne) |
| + switch { |
| + case err != nil: |
| + atDelete = append(atDelete, "unreadable") |
| + case slot == nil: |
| + atDelete = append(atDelete, "absent") |
| + default: |
| + atDelete = append(atDelete, slot.State) |
| + } |
| + } |
| + return nil |
| + }) |
| l.recoveredTo(t, r.OpID, lifetimeCommitted) |
| + l.bucket.setHook(nil) |
| requireRetired(t, l.fx.rel, ltApp, ltOne, 1) |
| - if ops := l.bucket.opsOf(key); !contains(ops, "delete-if-match") { |
| - t.Fatalf("record ops = %v, want the tombstone deleted on its ETag", ops) |
| + if len(atDelete) != 1 || atDelete[0] != provision.SlotRetired { |
| + t.Fatalf("the slot when the tombstone's delete was issued: %v, want one delete, under the retired slot", atDelete) |
| } |
| // The hostname's next lifetime is 2. |
| if rec := l.mustCreate(t, ltOne); rec.Incarnation != 2 { |
diff --git a/internal/host/movesoffline_lifetime_test.go b/internal/host/movesoffline_lifetime_test.go| index 331436394..9aa37e7a5 100644 |
| --- a/internal/host/movesoffline_lifetime_test.go |
| +++ b/internal/host/movesoffline_lifetime_test.go |
| @@ -92,9 +92,13 @@ func requireFoldWinner(t *testing.T, why string, a, b []byte, want string) { |
| // syntheticReadyPublication is a SYNTHETIC fold input: box's edges file as |
| // the real writer publishes a ready row for the host at pair, built over a |
| -// scratch registry. This plan's writer never publishes the row it stands |
| -// for (a held row is not published; plan 3's holding entry is), so no box |
| -// of the world produces it. |
| +// scratch registry. It has two uses. In the abort witnesses it stands for |
| +// the target's landing row at (1, 1): this plan's writer never publishes |
| +// that row (a held row is not published; plan 3's holding entry is), so no |
| +// box of the world produces it. In TestRecreatedHostOutranksItsPreviousLifetime |
| +// it is the brief's synthetic retained (1, 9), from a box the world does not |
| +// run, kept beside the real (1, 9) that uk-b publishes there: the real one |
| +// is the primary witness, the synthetic one the form the brief asked for. |
| func syntheticReadyPublication(t *testing.T, box string, pair registry.Pair) []byte { |
| t.Helper() |
| reg := edgesTestRegistry(t) |