Apply through one queue, and order what is applied and reported (hq to-be 45 Phase 2)
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.
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user