Fail a backup whose holder cannot be asked at the bound, not retry forever (issue 1066)
mesh/merge-gate head a39d6ce78d19 merged into trunk 7c9c187ca6e2 — pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus…
mesh/repo-check head a39d6ce78d19 merged into trunk 7c9c187ca6e2 — fail: its merge-check.sh failed: --- FAIL: TestADeliveryThatMovesABackedUpModuleWaitsFor…
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
mesh/merge-gate head a39d6ce78d19 merged into trunk 7c9c187ca6e2 — pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus…
mesh/repo-check head a39d6ce78d19 merged into trunk 7c9c187ca6e2 — fail: its merge-check.sh failed: --- FAIL: TestADeliveryThatMovesABackedUpModuleWaitsFor…
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:
@@ -131,8 +131,8 @@ func loadBackupFacts(ctx context.Context, inv *inventory.Inventory) (backupFacts
|
|||||||
// stepBackups takes one step of a plan's backups and says whether the plan may go on to ask its first tier. It
|
// stepBackups takes one step of a plan's backups and says whether the plan may go on to ask its first tier. It
|
||||||
// asks what is not asked, reads how the asked ones went, and records it all in p.Backups; it waits (false, nil)
|
// asks what is not asked, reads how the asked ones went, and records it all in p.Backups; it waits (false, nil)
|
||||||
// while any is pending, and stays stopped (false, nil) once one failed, which part B holds and says. A plan whose
|
// while any is pending, and stays stopped (false, nil) once one failed, which part B holds and says. A plan whose
|
||||||
// moved modules have no backed-up item keeps no record and goes on at once. An error is an ask that could not be
|
// moved modules have no backed-up item keeps no record and goes on at once. A holder that cannot be asked is tried again
|
||||||
// made or a read the store refused: the step is tried again, the asks already made kept.
|
// within the bound and then failed; an error is left for what the step itself cannot do.
|
||||||
func stepBackups(ctx context.Context, p *inventory.Plan, facts backupFacts, now time.Time) (bool, error) {
|
func stepBackups(ctx context.Context, p *inventory.Plan, facts backupFacts, now time.Time) (bool, error) {
|
||||||
proceed, err := stepBackupsOnce(ctx, p, facts, now)
|
proceed, err := stepBackupsOnce(ctx, p, facts, now)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -164,16 +164,25 @@ func stepBackupsOnce(ctx context.Context, p *inventory.Plan, facts backupFacts,
|
|||||||
}
|
}
|
||||||
if a.AskedAt == nil {
|
if a.AskedAt == nil {
|
||||||
asked := now
|
asked := now
|
||||||
|
if a.TriedAt == nil {
|
||||||
|
a.TriedAt = &asked
|
||||||
|
}
|
||||||
if err := askBackupNow(ctx, a.Machine, a.Module); err != nil {
|
if err := askBackupNow(ctx, a.Machine, a.Module); err != nil {
|
||||||
var unasked *backupNotAsked
|
var unasked *backupNotAsked
|
||||||
if errors.As(err, &unasked) {
|
if errors.As(err, &unasked) {
|
||||||
return false, err
|
// A holder off the bus is tried again within the bound, by the look and never by the
|
||||||
|
// "tried again" error path; past it the backup is failed and the plan held.
|
||||||
|
a.Words = "could not be asked: " + err.Error()
|
||||||
|
if now.Sub(*a.TriedAt) >= backupsWithin {
|
||||||
|
a.State = inventory.BackupFailed
|
||||||
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
a.State, a.Words = inventory.BackupFailed, err.Error()
|
a.State, a.Words = inventory.BackupFailed, err.Error()
|
||||||
a.AskedAt = &asked
|
a.AskedAt, a.TriedAt = &asked, nil
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
a.AskedAt = &asked
|
a.AskedAt, a.TriedAt, a.Words = &asked, nil, ""
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, read := nights[a.Machine]; !read {
|
if _, read := nights[a.Machine]; !read {
|
||||||
@@ -355,7 +364,7 @@ func askBackupsAgain(p *inventory.Plan, by, why string) ([]inventory.PlanBackup,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
again = append(again, *a)
|
again = append(again, *a)
|
||||||
a.State, a.AskedAt, a.Words = inventory.BackupPending, nil, ""
|
a.State, a.AskedAt, a.TriedAt, a.Words = inventory.BackupPending, nil, nil, ""
|
||||||
}
|
}
|
||||||
p.Backups.State = backupsStateOf(p.Backups.Asked)
|
p.Backups.State = backupsStateOf(p.Backups.Asked)
|
||||||
p.Note = backupsNote + "asked again by " + by + ": " + why
|
p.Note = backupsNote + "asked again by " + by + ": " + why
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -115,3 +117,38 @@ func TestAHeldPlanRaisesOneConditionWithTheBackupsWords(t *testing.T) {
|
|||||||
t.Fatalf("%+v", o)
|
t.Fatalf("%+v", o)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A holder off the bus cannot be asked: the plan waits within the bound, retried by the look and not by the error
|
||||||
|
// path, and at the bound the backup is failed with the reason, so the hold and the condition take over.
|
||||||
|
func TestABackupHolderThatCannotBeAskedIsFailedAtTheBoundWithTheReason(t *testing.T) {
|
||||||
|
seat := &aFakeBackupSeat{}
|
||||||
|
seat.install(t)
|
||||||
|
was := askBackupNow
|
||||||
|
t.Cleanup(func() { askBackupNow = was })
|
||||||
|
calls := 0
|
||||||
|
askBackupNow = func(_ context.Context, machine, module string) error {
|
||||||
|
calls++
|
||||||
|
return &backupNotAsked{fmt.Errorf("%s's backup holder was not asked: no responders", machine)}
|
||||||
|
}
|
||||||
|
entries := []inventory.Entry{aModuleWith(t, "mail", []string{"ace"}, anItem("a", ""))}
|
||||||
|
facts := backupFactsFrom(entries, holders("ace"))
|
||||||
|
p := aPlanOf("mail")
|
||||||
|
if proceed, err := stepBackups(t.Context(), p, facts, backupT0); proceed || err != nil || p.Backups.State != inventory.BackupPending {
|
||||||
|
t.Fatalf("proceed=%v err=%v %+v", proceed, err, p.Backups)
|
||||||
|
}
|
||||||
|
if proceed, err := stepBackups(t.Context(), p, facts, backupT0.Add(backupsWithin/2)); proceed || err != nil ||
|
||||||
|
p.Backups.State != inventory.BackupPending || calls != 2 || p.Backups.Asked[0].AskedAt != nil {
|
||||||
|
t.Fatalf("within the bound: proceed=%v err=%v calls=%d %+v", proceed, err, calls, p.Backups)
|
||||||
|
}
|
||||||
|
if proceed, err := stepBackups(t.Context(), p, facts, backupT0.Add(backupsWithin)); proceed || err != nil {
|
||||||
|
t.Fatalf("proceed=%v err=%v", proceed, err)
|
||||||
|
}
|
||||||
|
a := p.Backups.Asked[0]
|
||||||
|
if p.Backups.State != inventory.BackupFailed || a.State != inventory.BackupFailed ||
|
||||||
|
!strings.Contains(a.Words, "could not be asked: ") || !strings.Contains(a.Words, "no responders") {
|
||||||
|
t.Fatalf("%+v", p.Backups)
|
||||||
|
}
|
||||||
|
if strings.Contains(p.Note, "tried again") || !strings.Contains(p.Note, "held") {
|
||||||
|
t.Fatalf("note %q", p.Note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -100,6 +100,9 @@ type PlanBackup struct {
|
|||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
// AskedAt is when the holder was asked; nil while it is not asked yet.
|
// AskedAt is when the holder was asked; nil while it is not asked yet.
|
||||||
AskedAt *time.Time `json:"asked_at,omitempty"`
|
AskedAt *time.Time `json:"asked_at,omitempty"`
|
||||||
|
// TriedAt is when the controller first tried to ask, kept while the holder cannot be reached: the bound of a
|
||||||
|
// backup that is never asked counts from it. Nil once asked.
|
||||||
|
TriedAt *time.Time `json:"tried_at,omitempty"`
|
||||||
// Words are the backup's own: the holder's error for a failed one, "did not answer in ten minutes" for a silent one.
|
// Words are the backup's own: the holder's error for a failed one, "did not answer in ten minutes" for a silent one.
|
||||||
Words string `json:"words,omitempty"`
|
Words string `json:"words,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user