Files
mesh-host/internal/store/kept.go
T
jschoubben d2d9fbbd7d Hold what mounts a missing disk, and say entries the mesh could not take out (hq ADR 0325 re-review)
A container was still created over an unmounted mount point, so its writes landed on
the filesystem below; a folder moved to another disk was kept on the old warrant; and
entries left by a drop or a move that could not run lived only in a log line. Every
container mounting an access whose disk is away now waits, the warrant holds only for
the filesystem it was carried out on (the wait names the new one, path-free), entries
left on a filesystem are recorded per mount and taken out when it is back, and every
leftover is said as unhealthy until it is. A hung stat is shared per path, and a
widened mask takes from other entries only the bits it adds.
2026-10-12 19:03:02 +02:00

198 lines
7.6 KiB
Go

package store
import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"sync"
"time"
)
// KeptName is the file, beside the node's state, of every shared folder the engine keeps (novox/hq ADR 0325 §5):
// whose entries it added, on which filesystem it was placed, whether it was taken and under which warrant, and how
// far its sweep got. Its own file for the reason the hand-overs have theirs: the take act, the watch and the sweep
// run beside an apply, whose save of the state would lose a record written into it meanwhile.
const KeptName = "kept-folders.json"
// KeptPath is that file beside the state at statePath.
func KeptPath(statePath string) string {
return filepath.Join(filepath.Dir(statePath), KeptName)
}
// KeptUser is one identity the mesh keeps an entry for on a folder: its name, its number, its use and where it
// runs (a container, or the tool runner standing for every module whose tools use the folder, ADR 0325 §4).
type KeptUser struct {
Identity string `json:"identity"`
UID uint32 `json:"uid"`
Use string `json:"use"`
From string `json:"from"`
}
// How a folder came to be kept.
const (
// KeptByWarrant is a folder that held data, taken on the operator's word (ADR 0325 §8).
KeptByWarrant = "warrant"
// KeptEmpty is a folder that held nothing yet when it was first kept (§5).
KeptEmpty = "empty"
// KeptMade is a folder the engine made under a parent on its placed filesystem (§9).
KeptMade = "made"
)
// KeptFolder is one shared folder the engine keeps, or has seen and waits to keep. Counts and mount points only
// beside its own path: never a path under it.
type KeptFolder struct {
Path string `json:"path"`
// Mount is the mount point of the filesystem the folder was placed on when the engine first saw it there:
// later, a nearest existing parent on another one is the disk not there (ADR 0325 §9).
Mount string `json:"mount,omitempty"`
// Kept is that the mesh keeps it; How why (KeptByWarrant, KeptEmpty, KeptMade); Warrant and By the warrant and
// who asked, for a folder taken on one; At since when.
Kept bool `json:"kept,omitempty"`
How string `json:"how,omitempty"`
Warrant string `json:"warrant,omitempty"`
By string `json:"by,omitempty"`
At time.Time `json:"at,omitzero"`
// Users are the identities whose entries are on everything under the folder; Want what the last apply
// declared. Where they differ a walk adds or removes exactly the difference.
Users []KeptUser `json:"users,omitempty"`
Want []KeptUser `json:"want,omitempty"`
// IDs are the accesses that last named the folder: what a leftover is said under once none does.
IDs []string `json:"ids,omitempty"`
// TakenOn is the mount point the operator's warrant was carried out on (ADR 0325 §8): the controller's word
// that the folder was taken holds for that filesystem alone, and a folder moved to another asks again.
TakenOn string `json:"taken_on,omitempty"`
// Left is the entries the mesh added on a filesystem no longer at the folder (a disk mounted over it, or
// one gone): taken out by a drop walk once that filesystem holds the folder again, said until then.
Left []KeptLeft `json:"left,omitempty"`
// Act is the last walk under the folder: a take, an add or a drop.
Act *KeptAct `json:"act,omitempty"`
// Sweep is how far the sweep got (ADR 0325 §10).
Sweep KeptSweep `json:"sweep"`
// Watches is how many folders under it are watched; OutOfWatches that the machine's watch limit was met, and
// the sweep alone finds what is made where no watch is (§6).
Watches int `json:"watches,omitempty"`
OutOfWatches bool `json:"out_of_watches,omitempty"`
}
// KeptLeft is the mesh's entries left on one filesystem: its mount point and whose entries they are.
type KeptLeft struct {
Mount string `json:"mount"`
Users []KeptUser `json:"users"`
}
// KeptAct is one walk under a kept folder, as the engine records it. Counts only: never a path.
type KeptAct struct {
// Act is take, add or drop.
Act string `json:"act"`
Warrant string `json:"warrant,omitempty"`
By string `json:"by,omitempty"`
State string `json:"state"`
Started time.Time `json:"started"`
Ended time.Time `json:"ended,omitzero"`
// Folders and Files are the entries changed; Kept the ones that already had what they need; Links the links
// passed by unfollowed; Other the entries neither a file nor a folder; Mounts the folders on another
// filesystem not entered.
Folders int `json:"folders,omitempty"`
Files int `json:"files,omitempty"`
Kept int `json:"kept,omitempty"`
Links int `json:"links,omitempty"`
Other int `json:"other,omitempty"`
Mounts int `json:"mounts,omitempty"`
// Why is what stopped or failed it, in words with no path.
Why string `json:"why,omitempty"`
}
// KeptSweep is the sweep's place in a kept folder: where the last look stopped, below the folder, by the names
// of each level (never said, only resumed from); when it last covered the folder whole; and what this pass found.
type KeptSweep struct {
Cursor []string `json:"cursor,omitempty"`
Covered time.Time `json:"covered,omitzero"`
// Read is how many entries this pass read so far; Narrowed how many gave a user less than its right and were
// put back; Unrepaired how many could not be (an owner's bits, which the mesh never changes, or a write that
// failed).
Read int `json:"read,omitempty"`
Narrowed int `json:"narrowed,omitempty"`
Unrepaired int `json:"unrepaired,omitempty"`
// LastUnrepaired is what the last whole pass could not put back: said until the next pass is through.
LastUnrepaired int `json:"last_unrepaired,omitempty"`
// Bound is that the last look ran out of its bound before the folder was covered.
Bound bool `json:"bound,omitempty"`
}
// KeptFile is the kept folders' file, one writer at a time in this process.
type KeptFile struct {
path string
mu sync.Mutex
}
// NewKeptFile is the kept folders' file beside the state at statePath.
func NewKeptFile(statePath string) *KeptFile { return &KeptFile{path: KeptPath(statePath)} }
// All is every kept or seen folder, by path. None is an empty map; a file that cannot be read is an error.
func (k *KeptFile) All() (map[string]KeptFolder, error) {
k.mu.Lock()
defer k.mu.Unlock()
return k.load()
}
// Update changes the record of one folder under the file's lock: change is given the record (empty when there is
// none) and whether there was one, and says whether to keep it; a record not kept is removed.
func (k *KeptFile) Update(path string, change func(f *KeptFolder, had bool) bool) error {
k.mu.Lock()
defer k.mu.Unlock()
all, err := k.load()
if err != nil {
return err
}
f, had := all[path]
f.Path = path
if change(&f, had) {
all[path] = f
} else {
delete(all, path)
}
return k.save(all)
}
func (k *KeptFile) load() (map[string]KeptFolder, error) {
raw, err := os.ReadFile(k.path)
if errors.Is(err, fs.ErrNotExist) {
return map[string]KeptFolder{}, nil
}
if err != nil {
return nil, fmt.Errorf("reading the kept folders: %w", err)
}
out := map[string]KeptFolder{}
if err := json.Unmarshal(raw, &out); err != nil {
return nil, fmt.Errorf("the kept folders (%s) are unreadable: %w", k.path, err)
}
return out, nil
}
func (k *KeptFile) save(all map[string]KeptFolder) error {
raw, err := json.MarshalIndent(all, "", " ")
if err != nil {
return err
}
tmp, err := os.CreateTemp(filepath.Dir(k.path), ".kept-folders-*.json")
if err != nil {
return err
}
defer os.Remove(tmp.Name())
if _, err := tmp.Write(append(raw, '\n')); 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(), k.path)
}