A release the controller refuses to take leaves the delivery held for its failed backup (issue 1068)
mesh/merge-gate waits for its group's composed check
mesh/repo-check head 82693a441cf0 merged into trunk f076cadecedc beside mesh-controller#375@a39d6ce7 — fail: its merge-check.sh failed: --- FAIL: TestAPlan…
mesh/delivery-group group feat/backup-before-send rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request

This commit is contained in:
2026-10-17 22:53:43 +02:00
parent 4799f79242
commit 82693a441c
3 changed files with 42 additions and 4 deletions
@@ -126,3 +126,29 @@ func TestADeliveryHeldForAFailedBackupIsNotLetGoByItsWalkAlone(t *testing.T) {
t.Fatal("a walk started on the controller's own path lifted the backup hold")
}
}
func TestAReleaseWhoseBackupsTheControllerRefusedToAskAgainLeavesTheDeliveryHeld(t *testing.T) {
w, id, plan := aDeliveringWalk(t)
held := w.ctl.walk(plan)
held.Backups = failedBackup()
w.h.WalkMoved(w.ctl.put(held))
w.settleAll()
w.ctl.refuseBackupsAgain = true
_, err := w.h.Release(id, "freed some space", "jochen")
if err == nil || !strings.Contains(err.Error(), "held again") {
t.Fatalf("the release of a refused word answered %v, want an error saying it is held again", err)
}
w.settleAll()
d := w.delivery(id)
if d.State != Held || d.BackupsAgainAfter != 0 || !strings.Contains(d.HeldWhy, "no space left on device") {
t.Fatalf("after the refusal it is %s (again-after %d, why %q), want held for the backup", d.State, d.BackupsAgainAfter, d.HeldWhy)
}
// The controller takes the word the next time: the delivery goes on.
w.ctl.refuseBackupsAgain = false
if _, err := w.h.Release(id, "freed some space", "jochen"); err != nil {
t.Fatal(err)
}
if s := w.state(id); s != Delivering {
t.Fatalf("released again, it is %s", s)
}
}
@@ -94,7 +94,9 @@ type fakeController struct {
delivers []string
// backupsAgain are the walks whose failed backups were asked again.
backupsAgain []string
stops []string
// refuseBackupsAgain refuses the word to take a walk's backups again.
refuseBackupsAgain bool
stops []string
// withouts are the stops that left merges out (novox/hq ADR 0299): "<walk> without <merge>,…".
withouts []string
// refuseWithout refuses every stop with merges to leave out, as a controller before ADR 0299 does; mergedAt are
@@ -130,6 +132,9 @@ func (c *fakeController) BackupsAgain(walk, why string) error {
if c.down {
return errors.New("down")
}
if c.refuseBackupsAgain {
return errors.New("the controller refuses the backups")
}
w, known := c.walks[walk]
if !known || w.Backups == nil || w.Backups.State != "failed" {
return fmt.Errorf("%s holds no failed backup", walk)
@@ -370,7 +370,7 @@ func (h *Holder) Release(id, why, by string) (string, error) {
}
// Held for a failed backup before the send: the controller is asked to take the backups again (novox/hq ADR
// 0404 clause 4); its record at this revision or before still holds the failure, and does not hold it again.
backupsOf := ""
backupsOf, againBefore := "", d.BackupsAgainAfter
if d.Walk != nil && len(d.Walk.BackupsFailed) > 0 {
backupsOf, d.BackupsAgainAfter = d.Walk.ID, d.Walk.Revision
}
@@ -379,9 +379,16 @@ func (h *Holder) Release(id, why, by string) (string, error) {
if backupsOf != "" {
h.mu.Unlock()
if err := h.Controller.BackupsAgain(backupsOf, "released by "+by+": "+why); err != nil {
// The controller did not clear the failure: its record still holds it, so the delivery is held for it again now.
h.mu.Lock()
if d := h.deliveries[id]; d != nil {
d.BackupsAgainAfter = againBefore
h.settleAndKeep(d)
}
h.mu.Unlock()
h.again("a walk's backups the controller refused or did not answer")
return "", fmt.Errorf("%s is delivering on %s's word, and the controller could not be asked to take the backups of "+
"%s again (%v): the failed backup holds it again when its record is read", id, by, backupsOf, err)
return "", fmt.Errorf("%s was not released: the controller could not be asked to take the backups of %s again "+
"(%v), so it is held again for the failed backup", id, backupsOf, err)
}
return fmt.Sprintf("%s is delivering on %s's word; the backups of its walk %s are asked again", id, by, backupsOf), nil
}