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 `..`, 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() } } }