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.
77 lines
2.4 KiB
Go
77 lines
2.4 KiB
Go
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
|
|
}
|