Say an apply's report even when the apply ends the link

A host that delivered its own successor stood aside before the report of
that apply was published, so it failed with 'context canceled' and the
release plan waited for a report that never came (novox/hq issue 264).
Reports are now published on a context the stand-aside does not cancel,
and the last apply's report is kept until the broker takes it and said
again on the next link, so a crash between apply and report is covered too.
This commit is contained in:
jochen
2026-10-06 00:30:15 +02:00
parent 07430240bb
commit f62ee0bc75
5 changed files with 447 additions and 11 deletions
+76
View File
@@ -0,0 +1,76 @@
package store
import (
"errors"
"os"
"path/filepath"
)
// The report of the last apply of a declaration from the mesh, kept until the mesh has taken it.
//
// **An apply the mesh never heard about is one it waits on for ever.** The controller's release
// plan waits for a machine to report the exact declaration it was sent; a report lost between the
// apply and the publish — a host standing aside for its successor, a crash, a power cut — leaves the
// machine applied and the plan waiting until somebody pushes by hand (novox/hq issue 264). The
// declaration itself is kept (declared.go), and re-applying it would say something new about the
// machine; what was lost is what *that* apply did, so that is what is kept, exactly as it was made,
// and said again once the host is linked.
//
// Opaque bytes here: the report is the link's word, and the store keeps it without reading it.
// UnsaidName is where it lives, beside the state.
const UnsaidName = "unsaid.json"
// UnsaidPath is where the unsaid report lives, given where the state lives.
func UnsaidPath(statePath string) string {
return filepath.Join(filepath.Dir(statePath), UnsaidName)
}
// SaveUnsaid keeps a report the mesh has not taken yet, replacing whatever was kept before: only
// the last apply's report is worth saying again, because the mesh compares against what it sent last.
func SaveUnsaid(path string, report []byte) error {
if len(report) == 0 {
return errors.New("refusing to keep an empty report")
}
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return err
}
tmp, err := os.CreateTemp(filepath.Dir(path), ".unsaid-*")
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(report); 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)
}
// ReadUnsaid is the report kept unsaid, or nil when there is none — the ordinary case.
func ReadUnsaid(path string) ([]byte, error) {
raw, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
return raw, err
}
// ClearUnsaid forgets the kept report, once the mesh has taken it. Absent is already clear.
func ClearUnsaid(path string) error {
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
return nil
}