Files
mesh-catalog/modules/restic/cmd/restic-backups/good.go
T

129 lines
3.9 KiB
Go

package main
// **Each item's last good night, on the bus** (novox/hq ADR 0571 clause 3, ADR 0564 §6): beside good.json, the module's
// own state `good` (health.GoodRow), one key `<node>.<module>.<item>`, written when a night's files change, so the
// self-check's probe of a stale backup reads it there.
import (
"encoding/json"
"path/filepath"
"strings"
"syscall"
"time"
"unsafe"
"git.novox.be/novox/mesh-sdk/go/health"
)
// GoodRows is the rows of this machine (health.GoodRow): every item a module declares that a path of the module's backup
// covers, with the module's newest good night.
func (b *Backups) GoodRows() (map[string]health.GoodRow, error) {
items, err := b.Items()
if err != nil {
return nil, err
}
declared, err := b.Declared()
if err != nil {
return nil, err
}
paths := map[string][]string{}
for _, d := range declared {
paths[d.Module] = d.Paths
}
good := b.Good()
out := map[string]health.GoodRow{}
for module, its := range items {
for _, it := range its {
if it.Class == "cache" || it.CoveredBy == "" {
continue
}
covered := false
for _, p := range paths[module] {
covered = covered || p == it.CoveredBy
}
if !covered {
continue
}
out[health.GoodKey(b.Node, module, it.ID)] = health.GoodRow{V: health.GoodVersion, Node: b.Node, Module: module,
Item: it.ID, CoveredBy: it.CoveredBy, LastGood: good[module].UTC()}
}
}
return out, nil
}
// keepGood writes the rows that differ from what was last written, when a night succeeded (recordGood). A bus that
// cannot be reached loses nothing: good.json is the local copy, and the next good night writes the rows again.
func (b *Backups) keepGood() {
if b.PutGood == nil {
return
}
rows, err := b.GoodRows()
if err != nil {
b.Say("the last good nights could not be read: %v", err)
return
}
b.goodMu.Lock()
defer b.goodMu.Unlock()
if b.goodWrote == nil {
b.goodWrote = map[string][]byte{}
}
for k, r := range rows {
raw, _ := json.Marshal(r)
if string(b.goodWrote[k]) == string(raw) {
continue
}
if err := b.PutGood(k, r); err != nil {
b.Say("the last good night of %s was not put on the bus: %v", k, err)
continue
}
b.goodWrote[k] = raw
}
}
// watchGood writes the rows once at start and then each time the night's process changes what it keeps in the state
// directory (good.json, the nights' file), as the file system says it: the night runs as the operator account, which a
// process with a bus account of its own may not (ADR 0436 §2), so the tools' runtime, which has the module's account,
// writes at the night's event instead of the night itself. No timer.
func watchGood(b *Backups) {
time.Sleep(20 * time.Second) // the runtime serves first
b.keepGood()
for {
err := whenWritten(b.Where.State, func(name string) bool { return name == "good.json" || name == filepath.Base(b.nightsFile()) }, b.keepGood)
b.Say("watching the state directory for a night failed (%v); trying again in a minute", err)
time.Sleep(time.Minute)
b.keepGood()
}
}
// whenWritten calls then each time a file of dir that match names is written or put in place, until the watch fails.
func whenWritten(dir string, match func(name string) bool, then func()) error {
fd, err := syscall.InotifyInit1(syscall.IN_CLOEXEC)
if err != nil {
return err
}
defer syscall.Close(fd)
if _, err := syscall.InotifyAddWatch(fd, dir, syscall.IN_CLOSE_WRITE|syscall.IN_MOVED_TO); err != nil {
return err
}
buf := make([]byte, 64*syscall.SizeofInotifyEvent+4096)
for {
n, err := syscall.Read(fd, buf)
if err != nil {
if err == syscall.EINTR {
continue
}
return err
}
heard := false
for off := 0; off+syscall.SizeofInotifyEvent <= n; {
ev := (*syscall.InotifyEvent)(unsafe.Pointer(&buf[off]))
name := strings.TrimRight(string(buf[off+syscall.SizeofInotifyEvent:off+syscall.SizeofInotifyEvent+int(ev.Len)]), "\x00")
heard = heard || match(name)
off += syscall.SizeofInotifyEvent + int(ev.Len)
}
if heard {
then()
}
}
}