129 lines
3.9 KiB
Go
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()
|
|
}
|
|
}
|
|
}
|