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 superseded: a newer head of the same pull request
109 lines
4.3 KiB
Go
109 lines
4.3 KiB
Go
package main
|
|
|
|
// A backup taken by hand (novox/hq issue 453): `node-backup.now`, asked by a person before a risky change or by
|
|
// the mesh before a module moves (ADR 0233, the bus's snapshot before its upgrade). The thinning after every
|
|
// backup keeps only the newest restore point of each day, so one taken at 02:35 was forgotten by the night at
|
|
// 03:01 — while the night's may already hold the damage it was taken against.
|
|
//
|
|
// So a backup taken by hand is tagged HandTag beside its module's tag, and is kept by restic's own --keep-tag:
|
|
// with the thinning grouping by tags, it is its own group too, never counted against the nights. It is kept
|
|
// until a later good night of its module exists, and for the machine's keep-hand-taken-days in any case; past
|
|
// both, the thinning forgets it by its id (handLetGo). The nights are thinned by their counts alone: no rule
|
|
// that keeps one keeps every night.
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// HandTag marks a restore point taken by hand.
|
|
const HandTag = "hand"
|
|
|
|
// BackUpNow is a backup asked for now, of every module or one: as a night, each restore point tagged HandTag.
|
|
func (b *Backups) BackUpNow(ctx context.Context, only string) (map[string]Night, error) {
|
|
return b.backUp(ctx, only, true)
|
|
}
|
|
|
|
// isHand is whether a restore point was taken by hand.
|
|
func isHand(s Snapshot) bool { return slices.Contains(s.Tags, HandTag) }
|
|
|
|
// moduleOf is the module a restore point is of; empty when it carries no module's tag.
|
|
func moduleOf(s Snapshot) string {
|
|
for _, t := range s.Tags {
|
|
if m, ok := strings.CutPrefix(t, "module="); ok && m != "" {
|
|
return m
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// isNight is whether a restore point is a night's: its module's tag and nothing else — neither taken by hand
|
|
// nor a retired item's last one. Restic writes a snapshot only for a backup that succeeded, so each is good.
|
|
func isNight(s Snapshot) bool { return len(s.Tags) == 1 && moduleOf(s) != "" }
|
|
|
|
// latestNights is the time of each module's newest night.
|
|
func latestNights(snaps []Snapshot) map[string]time.Time {
|
|
latest := map[string]time.Time{}
|
|
for _, s := range snaps {
|
|
if m := moduleOf(s); isNight(s) && s.Time.After(latest[m]) {
|
|
latest[m] = s.Time
|
|
}
|
|
}
|
|
return latest
|
|
}
|
|
|
|
// handLetGo is the restore points taken by hand that the thinning forgets: older than days, and with a night of
|
|
// the same module after them. Every other one taken by hand is kept.
|
|
func handLetGo(snaps []Snapshot, now time.Time, days int) []Snapshot {
|
|
latest := latestNights(snaps)
|
|
cutoff := now.AddDate(0, 0, -days)
|
|
var out []Snapshot
|
|
for _, s := range snaps {
|
|
if isHand(s) && s.Time.Before(cutoff) && latest[moduleOf(s)].After(s.Time) {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// HandKept is one restore point taken by hand, as `backed-up` says it.
|
|
type HandKept struct {
|
|
Snapshot string `json:"snapshot"`
|
|
At time.Time `json:"at"`
|
|
// KeptAtLeastUntil is when its days are up; WaitsForANight that no later good night of its module exists
|
|
// yet, and it is kept until one does as well. The first thinning after both forgets it.
|
|
KeptAtLeastUntil time.Time `json:"keptAtLeastUntil"`
|
|
WaitsForANight bool `json:"waitsForANight"`
|
|
Kept string `json:"kept"`
|
|
}
|
|
|
|
// handKept is a module's restore points taken by hand, oldest first, said with the retention in force.
|
|
func handKept(module string, snaps []Snapshot, keep InForce) []HandKept {
|
|
latest := latestNights(snaps)
|
|
out := []HandKept{}
|
|
for _, s := range snaps {
|
|
if !isHand(s) || moduleOf(s) != module {
|
|
continue
|
|
}
|
|
k := HandKept{Snapshot: s.ShortID, At: s.Time, WaitsForANight: !latest[module].After(s.Time)}
|
|
switch {
|
|
case keep.Retention == nil:
|
|
k.Kept = "kept: nothing is thinned while the retention is refused"
|
|
case k.WaitsForANight:
|
|
k.KeptAtLeastUntil = s.Time.AddDate(0, 0, keep.Hand)
|
|
k.Kept = fmt.Sprintf("kept as taken by hand until a later good night of %s exists, and at least until %s",
|
|
module, k.KeptAtLeastUntil.UTC().Format(time.RFC3339))
|
|
default:
|
|
k.KeptAtLeastUntil = s.Time.AddDate(0, 0, keep.Hand)
|
|
k.Kept = fmt.Sprintf("kept as taken by hand until %s; the first thinning after forgets it",
|
|
k.KeptAtLeastUntil.UTC().Format(time.RFC3339))
|
|
}
|
|
out = append(out, k)
|
|
}
|
|
slices.SortFunc(out, func(a, b HandKept) int { return a.At.Compare(b.At) })
|
|
return out
|
|
}
|