diff --git a/modules/restic/cmd/restic-backups/hand.go b/modules/restic/cmd/restic-backups/hand.go index 16f97f6f..4fc0dba5 100644 --- a/modules/restic/cmd/restic-backups/hand.go +++ b/modules/restic/cmd/restic-backups/hand.go @@ -26,8 +26,16 @@ import ( const HandTag = "hand" // BackUpNow is a backup asked for now, of every module or one: as a night, each restore point tagged HandTag. +// +// One that cannot start (the lock cannot be taken, the repository cannot be opened, the module declares nothing) +// is recorded as that module's last night, failed and with the reason: the mesh asking before a delivery reads +// it from backed-up, and would otherwise read silence (novox/hq issue 1067). func (b *Backups) BackUpNow(ctx context.Context, only string) (map[string]Night, error) { - return b.backUp(ctx, only, true) + out, err := b.backUp(ctx, only, true) + if err != nil && only != "" { + b.record(only, Night{OK: false, At: b.Now().UTC(), Error: err.Error()}) + } + return out, err } // isHand is whether a restore point was taken by hand. diff --git a/modules/restic/cmd/restic-backups/hand_test.go b/modules/restic/cmd/restic-backups/hand_test.go index f59a8ef5..22af7160 100644 --- a/modules/restic/cmd/restic-backups/hand_test.go +++ b/modules/restic/cmd/restic-backups/hand_test.go @@ -324,3 +324,30 @@ func TestAThinningThatFailsAfterLettingGoSaysWhatItLetGo(t *testing.T) { t.Errorf("said %q, want it to say aaaaaaaa was let go", why) } } + +// A backup asked for now that cannot start leaves its failure as the module's last night, so the mesh asking +// for it before a delivery reads the backup's own words from backed-up, not silence (novox/hq issue 1067). +func TestABackupAskedForNowThatCannotStartIsRecordedAsThatModulesLastNight(t *testing.T) { + h := &handRestic{} + asked := time.Date(2026, 10, 17, 22, 50, 0, 0, time.UTC) + b := &Backups{Where: placed(t, "# pg\npath /\n"), Run: h.run, Now: func() time.Time { return asked }, Say: quiet} + must(t, os.Mkdir(filepath.Join(b.Where.State, nightLock), 0o700)) // a directory cannot be opened read-write: the lock cannot be taken + if _, err := b.BackUpNow(context.Background(), "pg"); err == nil { + t.Fatal("a backup started without its lock") + } + night, ok := b.Nights()["pg"] + if !ok || night.OK || night.Error == "" || !night.At.Equal(asked) { + t.Fatalf("pg's last night is %+v, want a failure at %s with the reason", night, asked) + } + if !strings.Contains(night.Error, nightLock) { + t.Errorf("the reason is %q, want it to name the lock", night.Error) + } + // Asked of a module that declares nothing here, once the lock can be taken: recorded too, with the reason. + must(t, os.Remove(filepath.Join(b.Where.State, nightLock))) + if _, err := b.BackUpNow(context.Background(), "mail"); err == nil { + t.Fatal("a backup of a module that declares nothing started") + } + if n := b.Nights()["mail"]; n.OK || !strings.Contains(n.Error, "declares nothing") { + t.Errorf("mail's last night is %+v, want a failure saying it declares nothing", n) + } +}