On novox at 01:16 on 2026-10-29, after the node began composing its own modules, mesh-vault's apply failed while the vault ran: its own composition carries the export as it runs (mesh-sdk frombus KeptRuns) by a digest whose object the controller's sweep (mesh-controller cmd/mesh-controller/declaration_sweep.go, sweepNode and sweepDeclarationFiles) had removed, since the sweep keeps only what the controller's compositions in composed-<node> and the wants name. The node may only read its bucket (ADR 0486 §2), so the node keeps what it had: a file here that verifies, or an object fetched, goes to <state dir>/objects under the object's name, and a kept one is used before the bus is asked. An object unused for 14 days is let go. A fetch that still fails says the keep never held it, before its cause. Quick fix weighed: re-put the object from the controller's store. Thorough (chosen): the node never depends on the bucket for content it once had, for every module, since the sweep and a node composing its own can always disagree. The whole-apply test gains three rows that fail on the old code; the test count stays.
112 lines
3.3 KiB
Go
112 lines
3.3 KiB
Go
package apply
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/declaration"
|
|
)
|
|
|
|
// ObjectKeep is the node-engine's own keep of the content of files given by digest (novox/hq ADR 0486 §1, issue 924): one
|
|
// file per object, named as the object is, beside the node's state. **What a node once had it never asks the bus for
|
|
// again**, so neither the controller's sweep of the bucket (an object is removed a day after no composition of the
|
|
// controller names it) nor a bus that does not answer can refuse a composition the node already ran. The node's account
|
|
// may only read its bucket (ADR 0486 §2), so the keep is the node's, not the bucket's: nothing is written back.
|
|
//
|
|
// An object unused for KeptObjectsFor is let go when another is kept, so the keep holds what the node applied lately and
|
|
// not every export it ever ran. Every failure to keep is silent and costs only a fetch: the keep is a copy.
|
|
type ObjectKeep struct {
|
|
Dir string
|
|
// Now is the clock; nil is time.Now.
|
|
Now func() time.Time
|
|
}
|
|
|
|
// KeptObjectsFor is how long an object of the keep is held after it was last used.
|
|
const KeptObjectsFor = 14 * 24 * time.Hour
|
|
|
|
// KeepObjectsIn is the keep at dir; an empty dir keeps nothing.
|
|
func KeepObjectsIn(dir string) *ObjectKeep {
|
|
if dir == "" {
|
|
return nil
|
|
}
|
|
return &ObjectKeep{Dir: dir}
|
|
}
|
|
|
|
func (k *ObjectKeep) now() time.Time {
|
|
if k.Now != nil {
|
|
return k.Now()
|
|
}
|
|
return time.Now()
|
|
}
|
|
|
|
// get is the kept content of the object name when it is what f names, and marks it used.
|
|
func (k *ObjectKeep) get(name string, f *declaration.File) ([]byte, bool) {
|
|
if k == nil {
|
|
return nil, false
|
|
}
|
|
path := filepath.Join(k.Dir, name)
|
|
body, err := os.ReadFile(path)
|
|
if err != nil || int64(len(body)) != f.Size || f.Verify(body) != nil {
|
|
return nil, false
|
|
}
|
|
now := k.now()
|
|
_ = os.Chtimes(path, now, now)
|
|
return body, true
|
|
}
|
|
|
|
// put keeps body under name, unless it is kept already (then it is marked used), and lets go of what was unused too long.
|
|
func (k *ObjectKeep) put(name string, body []byte) {
|
|
if k == nil {
|
|
return
|
|
}
|
|
path := filepath.Join(k.Dir, name)
|
|
now := k.now()
|
|
if info, err := os.Stat(path); err == nil && info.Size() == int64(len(body)) {
|
|
_ = os.Chtimes(path, now, now)
|
|
return
|
|
}
|
|
if err := os.MkdirAll(k.Dir, 0o700); err != nil {
|
|
return
|
|
}
|
|
tmp, err := os.CreateTemp(k.Dir, ".put-*")
|
|
if err != nil {
|
|
return
|
|
}
|
|
_, werr := tmp.Write(body)
|
|
cerr := tmp.Close()
|
|
if werr != nil || cerr != nil || os.Chmod(tmp.Name(), 0o600) != nil || os.Rename(tmp.Name(), path) != nil {
|
|
_ = os.Remove(tmp.Name())
|
|
return
|
|
}
|
|
_ = os.Chtimes(path, now, now)
|
|
k.prune(now)
|
|
}
|
|
|
|
// prune lets go of every object unused for KeptObjectsFor.
|
|
func (k *ObjectKeep) prune(now time.Time) {
|
|
entries, err := os.ReadDir(k.Dir)
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, e := range entries {
|
|
info, err := e.Info()
|
|
if err != nil || !info.Mode().IsRegular() {
|
|
continue
|
|
}
|
|
if now.Sub(info.ModTime()) >= KeptObjectsFor {
|
|
_ = os.Remove(filepath.Join(k.Dir, e.Name()))
|
|
}
|
|
}
|
|
}
|
|
|
|
// never says, in a failed fetch's error before its cause (a transient one's detail keeps the tail), that the node's own
|
|
// keep did not hold the object either.
|
|
func (k *ObjectKeep) never() string {
|
|
if k == nil {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf(" (this node's own keep of objects, %s, never held it)", k.Dir)
|
|
}
|