A delivery and the five-minute reconcile were two paths that applied, ordered only by a lock, and each order it allowed was met live (issues 257, 261, 267). Now both only enqueue: one worker takes the newest declaration held when it starts, applies it once and makes one report, and reports leave in the order they are made. A declaration may carry the controller's lease epoch beside its sequence; one older than what this node applied is refused before anything is touched, counted, logged and reported. A report carries the declaration's epoch and sequence and the host's own report sequence, kept on disk so it goes on increasing across restarts and self-updates. Without an epoch, today's behaviour stands.
95 lines
2.9 KiB
Go
95 lines
2.9 KiB
Go
package store
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// What the node-engine counts about what it says, kept so the counting outlives the process.
|
|
//
|
|
// **The report sequence must increase across restarts and self-updates** (novox/hq to-be 45 §6). The
|
|
// mesh keeps, per machine, the highest report it accepted and refuses an older one; a host that began
|
|
// again from one after every restart — and a self-update is a restart — would have every report it
|
|
// made refused until it had counted past where its predecessor stopped. So the number is kept beside
|
|
// the state, where the successor reads it.
|
|
//
|
|
// And the count of declarations refused as older than what this node applied (rule 2), which the
|
|
// mesh's stale-writer watchdog reads from every report.
|
|
|
|
// NumbersName is where they live, beside the state.
|
|
const NumbersName = "numbers.json"
|
|
|
|
// NumbersPath is where the numbers live, given where the state lives.
|
|
func NumbersPath(statePath string) string {
|
|
return filepath.Join(filepath.Dir(statePath), NumbersName)
|
|
}
|
|
|
|
// Numbers is what is kept.
|
|
type Numbers struct {
|
|
// ReportSequence is the number of the last report this node made.
|
|
ReportSequence int64 `json:"report_sequence"`
|
|
// RefusedOlder is how many declarations it has refused as older, ever.
|
|
RefusedOlder int64 `json:"refused_older"`
|
|
}
|
|
|
|
// ReadNumbers is what was kept, or zero when nothing was — a node that has never reported.
|
|
//
|
|
// **Unreadable is an error, never zero** (novox/hq ADR 0227, rule 4): read as zero, every report the
|
|
// node makes next would be older than the ones it already made, and refused.
|
|
func ReadNumbers(path string) (Numbers, error) {
|
|
raw, err := os.ReadFile(path)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return Numbers{}, nil
|
|
}
|
|
if err != nil {
|
|
return Numbers{}, err
|
|
}
|
|
var n Numbers
|
|
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(&n); err != nil {
|
|
return Numbers{}, fmt.Errorf("the numbers kept at %s are unreadable: %w", path, err)
|
|
}
|
|
if n.ReportSequence < 0 || n.RefusedOlder < 0 {
|
|
return Numbers{}, fmt.Errorf("the numbers kept at %s are below zero, which nothing counting writes", path)
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
// SaveNumbers keeps them, replacing what was kept, and is on disk when it returns: a number used and
|
|
// not kept is one the next host would use again.
|
|
func SaveNumbers(path string, n Numbers) error {
|
|
raw, err := json.Marshal(n)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
return err
|
|
}
|
|
tmp, err := os.CreateTemp(filepath.Dir(path), ".numbers-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer os.Remove(tmp.Name())
|
|
if err := tmp.Chmod(0o600); err != nil {
|
|
tmp.Close()
|
|
return err
|
|
}
|
|
if _, err := tmp.Write(raw); err != nil {
|
|
tmp.Close()
|
|
return err
|
|
}
|
|
if err := tmp.Sync(); err != nil {
|
|
tmp.Close()
|
|
return err
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tmp.Name(), path)
|
|
}
|