restic: raise a night that never happens, and have the mesh write the retention readable (novox/hq issue 454)
mesh/merge-gate pass: builds restic → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
mesh/merge-gate pass: builds restic → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
A hung night, a lock held for hours or a lock that cannot be taken left no failed night behind, so it stayed healthy. The night's process now counts how long the machine was up since its newest good night, by the monotonic clock so a sleeping laptop is not blamed, and past 26 hours it is raised. The retention file is written 0644 and root's instead of read through sudo: the account reads it and still cannot change it. An unreadable file names its cause, and the healthy answer says only what was seen.
This commit is contained in:
@@ -16,8 +16,6 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Retention is how many restore points each period keeps.
|
||||
@@ -87,55 +85,31 @@ func (e *SettingRefused) Error() string {
|
||||
// rather than thinned by 14/8/6, which would thin by a number nobody set (novox/hq issue 454).
|
||||
const Missing = "is missing"
|
||||
|
||||
// retentionRead is the last read of the file, by what its stat said, so a health look every minute does not
|
||||
// run sudo every minute while nothing changed.
|
||||
type retentionRead struct {
|
||||
mu sync.Mutex
|
||||
key string
|
||||
keep Retention
|
||||
err error
|
||||
}
|
||||
// Unreadable is said of a retention file this program's account may not read.
|
||||
const Unreadable = "cannot be read by the account this module runs as"
|
||||
|
||||
// retention is this machine's, as the file the mesh wrote says; an error when it is missing, cannot be
|
||||
// read, or does not read, and a *SettingRefused within it when one setting is what is wrong.
|
||||
//
|
||||
// The mesh writes the file as root's alone (a trusted file, novox/hq issue 339) and this program runs as the
|
||||
// operator account, so what the account may not read is read as root, as the declared directories are
|
||||
// (novox/hq issue 454: until then every night refused it as unreadable and thinned nothing).
|
||||
func (b *Backups) retention(ctx context.Context) (Retention, error) {
|
||||
// The mesh writes the file root's and readable by everyone (0644): the account the night runs as reads it
|
||||
// and cannot change it, and the counts are no secret (novox/hq issue 454 — written 0600 before, which the
|
||||
// account could not read, so every night would have refused it and thinned nothing).
|
||||
func (b *Backups) retention(context.Context) (Retention, error) {
|
||||
path := filepath.Join(b.Where.State, RetentionFile)
|
||||
info, err := os.Stat(path)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return Retention{}, fmt.Errorf("%s %s", path, Missing)
|
||||
}
|
||||
if err != nil {
|
||||
return Retention{}, err
|
||||
}
|
||||
key := fmt.Sprint(info.ModTime().UnixNano(), info.Size(), info.Mode())
|
||||
if st, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||
key += fmt.Sprint(" ", st.Ino, st.Ctim.Nano())
|
||||
}
|
||||
b.read.mu.Lock()
|
||||
defer b.read.mu.Unlock()
|
||||
if b.read.key == key {
|
||||
return b.read.keep, b.read.err
|
||||
}
|
||||
raw, err := os.ReadFile(path)
|
||||
if errors.Is(err, fs.ErrPermission) {
|
||||
var out string
|
||||
out, err = b.Run(ctx, "cat", "--", path)
|
||||
raw = []byte(out)
|
||||
}
|
||||
if err != nil {
|
||||
// Not kept: a read that failed may succeed at the next look.
|
||||
return Retention{}, fmt.Errorf("%s cannot be read: %w", path, err)
|
||||
switch {
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
return Retention{}, fmt.Errorf("%s %s", path, Missing)
|
||||
case errors.Is(err, fs.ErrPermission):
|
||||
return Retention{}, fmt.Errorf("%s %s (%v)", path, Unreadable, errors.Unwrap(err))
|
||||
case err != nil:
|
||||
return Retention{}, err
|
||||
}
|
||||
keep, err := ParseRetention(string(raw))
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%s: %w", path, err)
|
||||
return Retention{}, fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
b.read.key, b.read.keep, b.read.err = key, keep, err
|
||||
return keep, err
|
||||
return keep, nil
|
||||
}
|
||||
|
||||
// InForce is the retention a night thins by, or why it thins nothing: what `backed-up` says beside each
|
||||
|
||||
Reference in New Issue
Block a user