Readings and last good nights are the owners' published packages: disk-load's diskreadings and restic's backupgood, taken by series (B5b Left 3; ADR 0634 §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
module git.novox.be/novox/mesh-catalog/modules/restic/backupgood
|
||||
|
||||
go 1.26.0
|
||||
@@ -0,0 +1,45 @@
|
||||
package backupgood
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// **Each item's last good night** (novox/hq ADR 0571 clause 3, ADR 0564 §6; the lead, 2026-10-29): the node-backup holder
|
||||
// restic keeps, from its tools bundle through the runtime on the night's file event (its night's process runs as the
|
||||
// operator's account and holds no bus account, ADR 0436 §1-2), its own state `good`, one key `<node>.<module>.<item>` for every item a module
|
||||
// declares that a backup path covers, written when it changes: the item, the path whose snapshot keeps it and the newest
|
||||
// good night of its module (zero before the first). The self-check's probe of a stale backup reads it there. The package is restic's own, published by its series (ADR 0634 §4); the readers import it.
|
||||
const (
|
||||
ResticModule = "restic"
|
||||
GoodState = "good"
|
||||
GoodBucket = ResticModule + "_" + GoodState
|
||||
GoodRead = ResticModule + "." + GoodState
|
||||
GoodVersion = 1
|
||||
)
|
||||
|
||||
// GoodRow is one key of restic's `good`.
|
||||
type GoodRow struct {
|
||||
V int `json:"v"`
|
||||
Node string `json:"node"`
|
||||
Module string `json:"module"`
|
||||
Item string `json:"item"`
|
||||
CoveredBy string `json:"covered_by"`
|
||||
LastGood time.Time `json:"last_good,omitzero"`
|
||||
}
|
||||
|
||||
// GoodKey is a row's key: `<node>.<module>.<item>`, as a disk-load reading's.
|
||||
func GoodKey(node, module, item string) string { return node + "." + module + "." + item }
|
||||
|
||||
// DecodeGoodRow reads a row; it refuses another version.
|
||||
func DecodeGoodRow(raw []byte) (GoodRow, error) {
|
||||
var g GoodRow
|
||||
if err := json.Unmarshal(raw, &g); err != nil {
|
||||
return GoodRow{}, fmt.Errorf("backupgood: a last good night is not readable: %w", err)
|
||||
}
|
||||
if g.V != GoodVersion {
|
||||
return GoodRow{}, fmt.Errorf("backupgood: a last good night of version %d, and this reads %d", g.V, GoodVersion)
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package backupgood
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Restic's last good night per item, keyed as a disk-load reading is.
|
||||
func TestALastGoodNightIsReadBackAndAnotherVersionRefused(t *testing.T) {
|
||||
at := time.Date(2026, 10, 29, 12, 0, 0, 0, time.UTC)
|
||||
goodRow, _ := json.Marshal(GoodRow{V: GoodVersion, Node: "ace", Module: "pg", Item: "data", CoveredBy: "/srv/pg", LastGood: at})
|
||||
if g, err := DecodeGoodRow(goodRow); err != nil || !g.LastGood.Equal(at) || GoodKey("ace", "pg", "data") != "ace.pg.data" ||
|
||||
GoodBucket != "restic_good" || GoodRead != "restic.good" {
|
||||
t.Fatalf("a last good night read back as %+v: %v", g, err)
|
||||
}
|
||||
if _, err := DecodeGoodRow([]byte(`{"v":2}`)); err == nil {
|
||||
t.Fatal("a last good night of another version was read")
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,8 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
"git.novox.be/novox/mesh-catalog/modules/disk-load/diskreadings"
|
||||
"git.novox.be/novox/mesh-catalog/modules/restic/backupgood"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -209,10 +210,10 @@ type Backups struct {
|
||||
Node string
|
||||
// Readings is every reading disk-load keeps for this machine (its state `readings`, ADR 0571 clause 3); nil where the
|
||||
// process has no runtime to read it.
|
||||
Readings func() ([]health.Reading, error)
|
||||
// PutGood writes one row of the state `good` (health.GoodRow): the night's process through its own bus account, the
|
||||
Readings func() ([]diskreadings.Reading, error)
|
||||
// PutGood writes one row of the state `good` (backupgood.GoodRow): the night's process through its own bus account, the
|
||||
// tools through the runtime; nil where there is no bus to write to.
|
||||
PutGood func(key string, row health.GoodRow) error
|
||||
PutGood func(key string, row backupgood.GoodRow) error
|
||||
|
||||
goodMu sync.Mutex
|
||||
goodWrote map[string][]byte
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-catalog/modules/disk-load/diskreadings"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -32,7 +32,7 @@ type Item = catalogue.PlacedDataItem
|
||||
// its newest good backup.
|
||||
type ItemReport struct {
|
||||
Item
|
||||
health.Measured
|
||||
diskreadings.Measured
|
||||
// Peak is the largest size in the last seven days, on PeakDay.
|
||||
Peak *int64 `json:"peak_bytes,omitempty"`
|
||||
PeakDay string `json:"peak_day,omitempty"`
|
||||
@@ -59,8 +59,8 @@ func (b *Backups) Items() (map[string][]Item, error) {
|
||||
|
||||
// Measurements is the newest reading disk-load keeps of each item on this machine, by module and item (its state
|
||||
// `readings`, ADR 0571 clause 3); none where it cannot be read, which `backed-up` then says as no reading.
|
||||
func (b *Backups) Measurements() map[string]map[string]health.Reading {
|
||||
out := map[string]map[string]health.Reading{}
|
||||
func (b *Backups) Measurements() map[string]map[string]diskreadings.Reading {
|
||||
out := map[string]map[string]diskreadings.Reading{}
|
||||
if b.Readings == nil {
|
||||
return out
|
||||
}
|
||||
@@ -71,7 +71,7 @@ func (b *Backups) Measurements() map[string]map[string]health.Reading {
|
||||
}
|
||||
for _, r := range rs {
|
||||
if out[r.Module] == nil {
|
||||
out[r.Module] = map[string]health.Reading{}
|
||||
out[r.Module] = map[string]diskreadings.Reading{}
|
||||
}
|
||||
out[r.Module][r.ID] = r
|
||||
}
|
||||
@@ -107,7 +107,7 @@ func (b *Backups) recordGood(module string, at time.Time) {
|
||||
|
||||
// itemReports is every item of one module, with its newest reading and its newest good backup: the
|
||||
// module's newest good night, where that night keeps the path that covers the item.
|
||||
func itemReports(its []Item, measured map[string]health.Reading, paths []string, good time.Time) []ItemReport {
|
||||
func itemReports(its []Item, measured map[string]diskreadings.Reading, paths []string, good time.Time) []ItemReport {
|
||||
out := []ItemReport{}
|
||||
for _, it := range its {
|
||||
r := ItemReport{Item: it}
|
||||
|
||||
@@ -6,8 +6,9 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.novox.be/novox/mesh-catalog/modules/disk-load/diskreadings"
|
||||
"git.novox.be/novox/mesh-catalog/modules/restic/backupgood"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -53,9 +54,9 @@ func TestEveryItemIsSaidWithItsReadingAndItsLastGoodBackup(t *testing.T) {
|
||||
night := time.Date(2026, 10, 6, 3, 0, 0, 0, time.UTC)
|
||||
b := &Backups{Where: where, Now: func() time.Time { return night.Add(time.Hour) }, Say: quiet,
|
||||
Run: func(context.Context, string, ...string) (string, error) { return "[]", nil },
|
||||
Readings: func() ([]health.Reading, error) {
|
||||
return []health.Reading{{V: 1, Node: "ace", PlacedDataItem: catalogue.PlacedDataItem{Module: "postgres", ID: "store"},
|
||||
Measured: health.Measured{SizeBytes: &size, LastWrite: &write, MeasuredAt: &at, Precision: "exact"}, Peak: &peak, PeakAt: "2026-10-05"}}, nil
|
||||
Readings: func() ([]diskreadings.Reading, error) {
|
||||
return []diskreadings.Reading{{V: 1, Node: "ace", PlacedDataItem: catalogue.PlacedDataItem{Module: "postgres", ID: "store"},
|
||||
Measured: diskreadings.Measured{SizeBytes: &size, LastWrite: &write, MeasuredAt: &at, Precision: "exact"}, Peak: &peak, PeakAt: "2026-10-05"}}, nil
|
||||
}}
|
||||
b.recordGood("postgres", night)
|
||||
got, err := b.BackedUp(context.Background(), "")
|
||||
@@ -80,13 +81,13 @@ func TestEveryItemIsSaidWithItsReadingAndItsLastGoodBackup(t *testing.T) {
|
||||
}
|
||||
// The same last good night is kept on the bus beside good.json, one row per item a backup path covers.
|
||||
b.Node = "ace"
|
||||
wrote := map[string]health.GoodRow{}
|
||||
b.PutGood = func(k string, r health.GoodRow) error { wrote[k] = r; return nil }
|
||||
wrote := map[string]backupgood.GoodRow{}
|
||||
b.PutGood = func(k string, r backupgood.GoodRow) error { wrote[k] = r; return nil }
|
||||
b.recordGood("postgres", night) // a good night writes the rows, once; the same night again writes nothing
|
||||
n := len(wrote)
|
||||
b.recordGood("postgres", night)
|
||||
row, ok := wrote[health.GoodKey("ace", "postgres", "store")]
|
||||
if len(wrote) != 1 || n != 1 || !ok || !row.LastGood.Equal(night) || row.CoveredBy != "/var/lib/mesh-store/dumps" || row.V != health.GoodVersion {
|
||||
row, ok := wrote[backupgood.GoodKey("ace", "postgres", "store")]
|
||||
if len(wrote) != 1 || n != 1 || !ok || !row.LastGood.Equal(night) || row.CoveredBy != "/var/lib/mesh-store/dumps" || row.V != backupgood.GoodVersion {
|
||||
t.Fatalf("the good rows are %+v", wrote)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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
|
||||
// own state `good` (backupgood.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 (
|
||||
@@ -12,12 +12,12 @@ import (
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
"git.novox.be/novox/mesh-catalog/modules/restic/backupgood"
|
||||
)
|
||||
|
||||
// GoodRows is the rows of this machine (health.GoodRow): every item a module declares that a path of the module's backup
|
||||
// GoodRows is the rows of this machine (backupgood.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) {
|
||||
func (b *Backups) GoodRows() (map[string]backupgood.GoodRow, error) {
|
||||
items, err := b.Items()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -31,7 +31,7 @@ func (b *Backups) GoodRows() (map[string]health.GoodRow, error) {
|
||||
paths[d.Module] = d.Paths
|
||||
}
|
||||
good := b.Good()
|
||||
out := map[string]health.GoodRow{}
|
||||
out := map[string]backupgood.GoodRow{}
|
||||
for module, its := range items {
|
||||
for _, it := range its {
|
||||
if it.Class == "cache" || it.CoveredBy == "" {
|
||||
@@ -44,7 +44,7 @@ func (b *Backups) GoodRows() (map[string]health.GoodRow, error) {
|
||||
if !covered {
|
||||
continue
|
||||
}
|
||||
out[health.GoodKey(b.Node, module, it.ID)] = health.GoodRow{V: health.GoodVersion, Node: b.Node, Module: module,
|
||||
out[backupgood.GoodKey(b.Node, module, it.ID)] = backupgood.GoodRow{V: backupgood.GoodVersion, Node: b.Node, Module: module,
|
||||
Item: it.ID, CoveredBy: it.CoveredBy, LastGood: good[module].UTC()}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-catalog/modules/disk-load/diskreadings"
|
||||
"git.novox.be/novox/mesh-catalog/modules/restic/backupgood"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/announce"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
)
|
||||
|
||||
// Seat is the role this module holds.
|
||||
@@ -57,12 +58,12 @@ func main() {
|
||||
counting(b)
|
||||
case "":
|
||||
// Readable: the self-check reads it (its manifest's `reads`); checks/readable holds it.
|
||||
if err := stdio.Keeps(announce.State{Name: health.GoodState, Readable: true}); err != nil {
|
||||
if err := stdio.Keeps(announce.State{Name: backupgood.GoodState, Readable: true}); err != nil {
|
||||
say("%v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
b.PutGood = func(key string, row health.GoodRow) error {
|
||||
_, err := stdio.State(health.GoodState).Put(key, row)
|
||||
b.PutGood = func(key string, row backupgood.GoodRow) error {
|
||||
_, err := stdio.State(backupgood.GoodState).Put(key, row)
|
||||
return err
|
||||
}
|
||||
go watchGood(b)
|
||||
@@ -197,14 +198,14 @@ func tools(b *Backups) []stdio.Tool {
|
||||
}
|
||||
|
||||
// busReadings reads disk-load's readings of this machine from its state, every key `<node>.<module>.<item>`.
|
||||
func busReadings(node string) func() ([]health.Reading, error) {
|
||||
return func() ([]health.Reading, error) {
|
||||
st := stdio.State(health.ReadingsRead)
|
||||
func busReadings(node string) func() ([]diskreadings.Reading, error) {
|
||||
return func() ([]diskreadings.Reading, error) {
|
||||
st := stdio.State(diskreadings.ReadingsRead)
|
||||
keys, err := st.Keys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []health.Reading
|
||||
var out []diskreadings.Reading
|
||||
for _, k := range keys {
|
||||
if !strings.HasPrefix(k, node+".") {
|
||||
continue
|
||||
@@ -213,7 +214,7 @@ func busReadings(node string) func() ([]health.Reading, error) {
|
||||
if err != nil || e == nil {
|
||||
continue
|
||||
}
|
||||
if r, err := health.DecodeReading(e.Value); err == nil {
|
||||
if r, err := diskreadings.DecodeReading(e.Value); err == nil {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The readings' layout is restic's own package, published by its series for the readers to take (ADR 0634 §4).
|
||||
func TestTheReadingsLayoutIsAPublishedPackageOfThisModule(t *testing.T) {
|
||||
raw, err := os.ReadFile("../../module.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var m struct {
|
||||
Build struct {
|
||||
Artifacts []struct {
|
||||
Name, Kind, Series string
|
||||
Paths []string
|
||||
} `json:"artifacts"`
|
||||
} `json:"build"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, a := range m.Build.Artifacts {
|
||||
if a.Name == "backupgood" && a.Kind == "package" && a.Series == "0.1" && len(a.Paths) == 1 && a.Paths[0] == "backupgood" {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Errorf("no package artifact backupgood of series 0.1 in %s", raw)
|
||||
}
|
||||
@@ -13,3 +13,11 @@ require (
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
)
|
||||
|
||||
require git.novox.be/novox/mesh-catalog/modules/disk-load/diskreadings v0.0.0-00010101000000-000000000000
|
||||
|
||||
replace git.novox.be/novox/mesh-catalog/modules/disk-load/diskreadings => ../disk-load/diskreadings
|
||||
|
||||
require git.novox.be/novox/mesh-catalog/modules/restic/backupgood v0.0.0-00010101000000-000000000000
|
||||
|
||||
replace git.novox.be/novox/mesh-catalog/modules/restic/backupgood => ./backupgood
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"module": "restic",
|
||||
"version": "1",
|
||||
"packages": {
|
||||
"diskreadings": "0.1"
|
||||
},
|
||||
"moved": true,
|
||||
"claims": [
|
||||
{
|
||||
@@ -193,6 +196,15 @@
|
||||
"name": "files",
|
||||
"kind": "archive",
|
||||
"from": "files"
|
||||
},
|
||||
{
|
||||
"name": "backupgood",
|
||||
"kind": "package",
|
||||
"language": "go",
|
||||
"paths": [
|
||||
"backupgood"
|
||||
],
|
||||
"series": "0.1"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user