J1: a failed have carries the pull it waits on (ENG-9, the lead's ruling on pull-waits; local only)

This commit is contained in:
2026-11-04 14:17:29 +01:00
parent 85b31412b9
commit f9e035fbee
2 changed files with 64 additions and 0 deletions
+35
View File
@@ -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
}
+29
View File
@@ -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