diff --git a/go/delivery/delivery.go b/go/delivery/delivery.go index bd84f9c..62af7fc 100644 --- a/go/delivery/delivery.go +++ b/go/delivery/delivery.go @@ -369,6 +369,31 @@ type Have struct { // slot that holds it, so a provider and a consumer of one value on one node each keep their own. Written in Sealed's // place by a node-engine that knows it; Sealed is read only from a have written before. SealedAt Seals `json:"sealed-at,omitempty"` + // PullWait is, on a failed have, the pull from a store its apply failed on and waits on that store's holder to settle + // for (design 51 *What a pull meets in the swap*, issue 1966 ENG-9): nil on every other have, and on a failure that + // waits on no holder. + PullWait *PullWait `json:"pull-wait,omitempty"` +} + +// PullWait is the pull a failed apply met from a store: the image, the seat it came through and that seat's holder, whose +// settled the apply is tried again on, when the pull began, and when the apply was last tried again with what triggered +// it, in words (empty while it has not been). +type PullWait struct { + Image string `json:"image"` + Seat string `json:"seat,omitempty"` + Holder string `json:"holder"` + Began time.Time `json:"began"` + Retried string `json:"retried,omitempty"` + RetriedAt time.Time `json:"retried-at,omitzero"` +} + +// samePullWait says whether two pull waits are the same, their times compared as instants. +func samePullWait(a, b *PullWait) bool { + if a == nil || b == nil { + return a == b + } + return a.Image == b.Image && a.Seat == b.Seat && a.Holder == b.Holder && a.Began.Equal(b.Began) && + a.Retried == b.Retried && a.RetriedAt.Equal(b.RetriedAt) } // UserListWritten is the bus's user list as the node-engine of the bus node wrote it to the bus module's file, applied @@ -488,6 +513,9 @@ func (h Have) check() error { if h.Judgement != "" || h.Waits != "" || h.BuildAlone { return fmt.Errorf("delivery: only a have waiting for a yes names a judgement, the composition waiting and whether it changes the build alone") } + if h.PullWait != nil && h.Phase != PhaseFailed { + return fmt.Errorf("delivery: only a failed have names the pull it waits on, not one %s", h.Phase) + } case PhaseWaitingYes: if h.Judgement != JudgedRecreates && h.Judgement != JudgedMovesData && h.Judgement != JudgedCannot { return fmt.Errorf("delivery: a have waiting for a yes names the judgement %q, not recreates, moves-data or cannot-judge", h.Judgement) @@ -498,6 +526,9 @@ func (h Have) check() error { default: return fmt.Errorf("delivery: phase %q is not backing-up, applying, checking, running, failed, removed, held or waiting-yes", h.Phase) } + if w := h.PullWait; w != nil && (w.Image == "" || w.Holder == "") { + return fmt.Errorf("delivery: a have's pull wait names no image or no holder") + } if h.Composed != "" { if err := checkDigest(h.Composed, "the have's composition digest"); err != nil { return err @@ -601,6 +632,10 @@ func DecodePulse(data []byte) (Pulse, error) { func SameHave(a, b Have) bool { ua, ub := a.UserList, b.UserList a.UserList, b.UserList = nil, nil + if !samePullWait(a.PullWait, b.PullWait) { + return false + } + a.PullWait, b.PullWait = nil, nil if !slices.Equal(a.Sealed, b.Sealed) || !maps.Equal(a.SealedAt, b.SealedAt) { return false } diff --git a/go/delivery/delivery_test.go b/go/delivery/delivery_test.go index c54fdbd..f292a01 100644 --- a/go/delivery/delivery_test.go +++ b/go/delivery/delivery_test.go @@ -452,6 +452,35 @@ func TestSame(t *testing.T) { } else if back, err := DecodeHave(raw); err != nil || !SameHave(back, sa) { t.Errorf("sealed values do not travel: %+v, %v", back, err) } + // A failed have carries the pull it waits on, compared by what it says (issue 1966 ENG-9); only a failed one does. + began := time.Date(2026, 11, 4, 14, 0, 0, 0, time.UTC) + pa, pb := a, a + pa.Phase, pb.Phase = PhaseFailed, PhaseFailed + pa.PullWait = &PullWait{Image: "reg/x@sha256:1", Seat: "container-image-registry", Holder: "distribution", Began: began, + Retried: "distribution settled", RetriedAt: began.Add(time.Minute)} + w := *pa.PullWait + w.Began, w.RetriedAt = began.In(time.Local), w.RetriedAt.In(time.Local) + pb.PullWait = &w + if !SameHave(pa, pb) || SameHave(pa, Have{V: 1, Build: "b", Phase: PhaseFailed, Health: HealthOK}) { + t.Error("equal pull waits differ, or one said and one not are the same") + } + if raw, err := EncodeHave(pa); err != nil { + t.Error(err) + } else if back, err := DecodeHave(raw); err != nil || !SameHave(back, pa) { + t.Errorf("a pull wait does not travel: %+v, %v", back, err) + } + pb.PullWait = &PullWait{Image: "reg/x@sha256:1", Holder: "distribution", Began: began} + if SameHave(pa, pb) { + t.Error("pull waits retried and not are the same") + } + for name, h := range map[string]Have{ + "on a running have": {Phase: PhaseRunning, Health: HealthOK, PullWait: pa.PullWait}, + "naming no holder": {Phase: PhaseFailed, Health: HealthOK, PullWait: &PullWait{Image: "reg/x@sha256:1"}}, + } { + if _, err := EncodeHave(h); err == nil { + t.Errorf("a pull wait %s was encoded", name) + } + } // The user list is compared by what it says, never by where it is held (ADR 0547 ยง8). at := time.Date(2026, 10, 25, 21, 0, 0, 0, time.UTC) ua, ub := a, a