Ask again for a folder that moved, and carry entries left while a disk was away (hq ADR 0325)
The engine names the filesystem a waiting folder is on; a warrant is recorded for that filesystem, and a folder on another is asked about again and not taken there meanwhile. Entries the engine could not take out while their disk was away, stated before a send, do not hold that delivery's gate. mesh-host pinned to d2d9fbbd.
This commit is contained in:
@@ -48,24 +48,27 @@ type folderTake struct {
|
||||
// Access is the resource id of one access to it in the machine's declaration: what the take act names.
|
||||
Access string `json:"access"`
|
||||
// Accesses are the access ids (each module's own) that place it, Modules the modules waiting on it, both sorted.
|
||||
Accesses []string `json:"accesses"`
|
||||
Modules []string `json:"modules"`
|
||||
At time.Time `json:"at"`
|
||||
Accesses []string `json:"accesses"`
|
||||
Modules []string `json:"modules"`
|
||||
// Placed is the filesystem the folder is on, as the engine names it without a path: the warrant is for that one.
|
||||
Placed string `json:"placed,omitempty"`
|
||||
At time.Time `json:"at"`
|
||||
}
|
||||
|
||||
// takeVerb is the verb a take's answer binds: the controller's own, performed by itself on the warrant.
|
||||
const takeVerb = askerName + ".take"
|
||||
|
||||
// key is the folder on its machine: what one ask is kept for.
|
||||
func (t folderTake) key() string { return t.Node + "\x00" + filepath.Clean(t.Path) }
|
||||
// The filesystem is part of it: a folder that moved to another disk is asked about again.
|
||||
func (t folderTake) key() string { return t.Node + "\x00" + filepath.Clean(t.Path) + "\x00" + t.Placed }
|
||||
|
||||
// digest binds everything the take does and was said for: the machine, the folder, the access it names and the
|
||||
// modules the operator was told.
|
||||
func (t folderTake) digest() string {
|
||||
raw, _ := json.Marshal(struct {
|
||||
Node, Path, Access string
|
||||
Modules []string
|
||||
}{t.Node, filepath.Clean(t.Path), t.Access, t.Modules})
|
||||
Node, Path, Access, Placed string
|
||||
Modules []string
|
||||
}{t.Node, filepath.Clean(t.Path), t.Access, t.Placed, t.Modules})
|
||||
sum := sha256.Sum256(raw)
|
||||
return "sha256:" + hex.EncodeToString(sum[:])
|
||||
}
|
||||
@@ -246,7 +249,7 @@ func takeFolderIn(open *stores, ask func(ctx context.Context, act link.AccessAct
|
||||
return "", fmt.Errorf("%s refused to take the folder: %s", t.Node, answer.Refused)
|
||||
}
|
||||
if err := open.inventory.RecordTaken(ctx, t.Node, inventory.TakenFolder{Path: t.Path, By: by, Ask: askID,
|
||||
Warrant: askID, Said: answer.Said}); err != nil {
|
||||
Warrant: askID, Said: answer.Said, Placed: t.Placed}); err != nil {
|
||||
return "", fmt.Errorf("%s started the take (%s), and it could NOT be recorded: %w", t.Node, answer.Said, err)
|
||||
}
|
||||
return fmt.Sprintf("%s takes the folder for %s: %s", t.Node, strings.Join(t.Modules, ", "), answer.Said), nil
|
||||
@@ -256,8 +259,9 @@ func takeFolderIn(open *stores, ask func(ctx context.Context, act link.AccessAct
|
||||
// waitingFolder is one access wait excused for a folder not yet taken, as foldersWaiting gathers them.
|
||||
type waitingFolder struct {
|
||||
module, access string
|
||||
// resource is the access's resource id on the machine, as the engine stated it.
|
||||
resource string
|
||||
// resource is the access's resource id on the machine, as the engine stated it; placed the filesystem its folder
|
||||
// is on, without its path.
|
||||
resource, placed string
|
||||
}
|
||||
|
||||
// foldersWaiting is every folder waiting to be kept across the mesh (ADR 0325 §7, §8): from each machine's newest
|
||||
@@ -302,7 +306,7 @@ func foldersWaiting(ctx context.Context, inv *inventory.Inventory, healths map[s
|
||||
out[machine] = map[string][]waitingFolder{}
|
||||
}
|
||||
out[machine][path] = append(out[machine][path], waitingFolder{module: r.Module, access: w.Access,
|
||||
resource: r.Resource})
|
||||
resource: r.Resource, placed: w.Placed})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -327,26 +331,33 @@ func foldersIn(open *stores) func(ctx context.Context) ([]folderTake, error) {
|
||||
func takesOf(waiting map[string]map[string][]waitingFolder) []folderTake {
|
||||
var out []folderTake
|
||||
for machine, folders := range waiting {
|
||||
for path, ws := range folders {
|
||||
t := folderTake{Node: machine, Path: path}
|
||||
seenM, seenA := map[string]bool{}, map[string]bool{}
|
||||
var resources []string
|
||||
for _, w := range ws {
|
||||
if !seenM[w.module] {
|
||||
seenM[w.module] = true
|
||||
t.Modules = append(t.Modules, w.module)
|
||||
for path, all := range folders {
|
||||
// One take per filesystem the waits name: they agree but for a moment while a disk is swapped.
|
||||
byPlaced := map[string][]waitingFolder{}
|
||||
for _, w := range all {
|
||||
byPlaced[w.placed] = append(byPlaced[w.placed], w)
|
||||
}
|
||||
for placed, ws := range byPlaced {
|
||||
t := folderTake{Node: machine, Path: path, Placed: placed}
|
||||
seenM, seenA := map[string]bool{}, map[string]bool{}
|
||||
var resources []string
|
||||
for _, w := range ws {
|
||||
if !seenM[w.module] {
|
||||
seenM[w.module] = true
|
||||
t.Modules = append(t.Modules, w.module)
|
||||
}
|
||||
if !seenA[w.access] {
|
||||
seenA[w.access] = true
|
||||
t.Accesses = append(t.Accesses, w.access)
|
||||
}
|
||||
resources = append(resources, w.resource)
|
||||
}
|
||||
if !seenA[w.access] {
|
||||
seenA[w.access] = true
|
||||
t.Accesses = append(t.Accesses, w.access)
|
||||
}
|
||||
resources = append(resources, w.resource)
|
||||
sort.Strings(t.Modules)
|
||||
sort.Strings(t.Accesses)
|
||||
sort.Strings(resources)
|
||||
t.Access = resources[0]
|
||||
out = append(out, t)
|
||||
}
|
||||
sort.Strings(t.Modules)
|
||||
sort.Strings(t.Accesses)
|
||||
sort.Strings(resources)
|
||||
t.Access = resources[0]
|
||||
out = append(out, t)
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].key() < out[j].key() })
|
||||
|
||||
@@ -33,21 +33,28 @@ func keepWait(access string) inventory.Wait {
|
||||
return inventory.Wait{Part: "radarr", Access: access, What: "let the mesh keep the shared folder " + access}
|
||||
}
|
||||
|
||||
func placedWait(access, placed string) inventory.Wait {
|
||||
w := keepWait(access)
|
||||
w.Placed = placed
|
||||
return w
|
||||
}
|
||||
|
||||
func diskWait(access string) inventory.Wait {
|
||||
return inventory.Wait{Part: "radarr", Access: access, Disk: true, What: "mount the disk that holds " + access}
|
||||
}
|
||||
|
||||
// accessFacts is the facts with each folder given taken on the filesystem fs-000000000001.
|
||||
func accessFacts(taken ...string) operatorWaitFacts {
|
||||
kept := map[string]bool{}
|
||||
kept := map[string]string{}
|
||||
for _, p := range taken {
|
||||
kept[p] = true
|
||||
kept[p] = "fs-000000000001"
|
||||
}
|
||||
return operatorWaitFacts{
|
||||
manifests: map[string]catalogue.Manifest{"radarr": radarrManifest, "plex": plexManifest},
|
||||
given: map[string]map[string]time.Time{"radarr@ace": {}, "plex@ace": {}},
|
||||
placed: map[string]map[string]string{"radarr@ace": {"movies": moviesAt}, "plex@ace": {"films": moviesAt},
|
||||
"radarr@novox": {}},
|
||||
taken: map[string]map[string]bool{"ace": kept},
|
||||
taken: map[string]map[string]string{"ace": kept},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +73,10 @@ func TestAnAccessWaitIsExcusedOnlyForAFolderTheMeshMayNotKeepYet(t *testing.T) {
|
||||
{"its disk not there", "ace", diskWait("movies"), accessFacts(), ""},
|
||||
{"its disk not there, the folder taken before", "ace", diskWait("movies"), accessFacts(moviesAt), ""},
|
||||
{"already taken", "ace", keepWait("movies"), accessFacts(moviesAt), "taken on ace already"},
|
||||
{"already taken on the filesystem it is on", "ace", placedWait("movies", "fs-000000000001"),
|
||||
accessFacts(moviesAt), "taken on ace already"},
|
||||
{"taken on another filesystem: it moved, and is asked about again", "ace",
|
||||
placedWait("movies", "fs-0000000000ff"), accessFacts(moviesAt), ""},
|
||||
{"undeclared", "ace", keepWait("books"), accessFacts(), "does not declare"},
|
||||
{"unplaced", "novox", keepWait("movies"), accessFacts(), "not placed on novox"},
|
||||
{"taken folders unread", "ace", keepWait("movies"), operatorWaitFacts{manifests: accessFacts().manifests,
|
||||
@@ -358,3 +369,55 @@ func TestTheDeclarationSaysItsIdentitiesOnlyWhenComposed(t *testing.T) {
|
||||
t.Fatalf("the contract the engine states for ADR 0325 is 5, and this controller reads %d", link.IdentityContract)
|
||||
}
|
||||
}
|
||||
|
||||
// A folder that moved to another filesystem is asked about again: one ask per folder, machine and filesystem, and the
|
||||
// take records the filesystem it was for.
|
||||
func TestAFolderThatMovedIsAskedAboutAgain(t *testing.T) {
|
||||
placed := "fs-000000000001"
|
||||
r, taken := takeRig(t, func() map[string]map[string][]waitingFolder {
|
||||
return waitingOn("ace", waitingFolder{module: "radarr", access: "movies", resource: "radarr.access-movies",
|
||||
placed: placed})
|
||||
})
|
||||
ctx := context.Background()
|
||||
if err := r.a.reconcile(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
placed = "fs-0000000000ff"
|
||||
if err := r.a.reconcile(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var open []asked
|
||||
for _, a := range takeAsks(r) {
|
||||
if a.State == askOpen {
|
||||
open = append(open, a)
|
||||
}
|
||||
}
|
||||
if len(open) != 1 || open[0].Take.Placed != "fs-0000000000ff" || len(takeAsks(r)) != 2 {
|
||||
t.Fatalf("after the folder moved: %d asks, open %+v", len(takeAsks(r)), open)
|
||||
}
|
||||
answerWith(t, r, warrantOn(open[0], "let-the-mesh-keep-it", r.now))
|
||||
if len(*taken) != 1 {
|
||||
t.Fatalf("taken %v", *taken)
|
||||
}
|
||||
}
|
||||
|
||||
// The mesh's entries left while a disk was away (kind kept-entries), stated before the send, do not hold a delivery
|
||||
// that did not bring them; stated since, they do — and they may name an access no longer declared.
|
||||
func TestEntriesLeftWhileADiskWasAwayDoNotHoldADeliveryThatDidNotBringThem(t *testing.T) {
|
||||
now := time.Now()
|
||||
since := now.Add(-time.Minute)
|
||||
left := inventory.ResourceHealth{Module: "radarr", Resource: "radarr.access-gone~entries", Kind: link.KindKeptEntries,
|
||||
Target: moviesAt, State: link.StateUnhealthy, Reason: "the mesh's entries could not be taken out: its disk is away",
|
||||
Since: since.Add(-time.Hour)}
|
||||
f := gateFacts{now: now, waits: accessFacts(), groupsAdded: map[string]bool{"radarr": false},
|
||||
health: map[string]inventory.NodeHealth{"ace": {Node: "ace", HeardAt: now,
|
||||
Resources: []inventory.ResourceHealth{left}}}}
|
||||
if h, why := moduleHealthWord("radarr", "ace", since, f); h != healthPerson || !strings.Contains(why, "access-gone~entries") {
|
||||
t.Fatalf("entries left before the send read %v %q; want carried in a passing verdict", h, why)
|
||||
}
|
||||
left.Since = now
|
||||
f.health["ace"] = inventory.NodeHealth{Node: "ace", HeardAt: now, Resources: []inventory.ResourceHealth{left}}
|
||||
if h, _ := moduleHealthWord("radarr", "ace", since, f); h != healthNotYet {
|
||||
t.Fatalf("entries left by this send read %v; want judged unhealthy", h)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ func stateHealth(ctx context.Context, inv *inventory.Inventory, k *conditions.Ke
|
||||
// access by its module's id, and whether it waits for a disk rather than for the operator's word.
|
||||
for _, w := range r.AccessWaits {
|
||||
kept.Waits = append(kept.Waits, inventory.Wait{Part: w.Part, Access: w.Access, Disk: w.WaitsForDisk(),
|
||||
What: w.What})
|
||||
Placed: w.Placed, What: w.What})
|
||||
}
|
||||
resources = append(resources, kept)
|
||||
}
|
||||
@@ -650,7 +650,7 @@ func moduleHealthWord(module, machine string, since time.Time, f gateFacts) (hea
|
||||
if waits && !f.groupsAdded[module] {
|
||||
waits = false
|
||||
}
|
||||
var found, onWaiting []string
|
||||
var found, onWaiting, leftover []string
|
||||
var forOperator []inventory.Wait
|
||||
for _, r := range resources {
|
||||
if r.Module != module {
|
||||
@@ -673,6 +673,14 @@ func moduleHealthWord(module, machine string, since time.Time, f gateFacts) (hea
|
||||
found = append(found, r.Resource)
|
||||
continue
|
||||
}
|
||||
// **The mesh's entries left on a folder whose disk was away, stated before this send, are a condition this
|
||||
// build found** (novox/hq ADR 0325 §8): they may name an access no longer declared, they wait for the disk,
|
||||
// and a delivery that did not bring them is not held for them — said in the verdict, as a directory used as
|
||||
// found is. Stated since the send, the send brought them, and they are judged as unhealthy.
|
||||
if r.Kind == link.KindKeptEntries && r.State == link.StateUnhealthy && r.Since.Before(since) {
|
||||
leftover = append(leftover, r.Resource)
|
||||
continue
|
||||
}
|
||||
switch r.State {
|
||||
case link.StateHealthy:
|
||||
case link.StateStarting:
|
||||
@@ -696,7 +704,7 @@ func moduleHealthWord(module, machine string, since time.Time, f gateFacts) (hea
|
||||
reasonAfter(r.Reason))
|
||||
}
|
||||
}
|
||||
if waits || len(found) > 0 || len(forOperator) > 0 || len(onWaiting) > 0 {
|
||||
if waits || len(found) > 0 || len(forOperator) > 0 || len(onWaiting) > 0 || len(leftover) > 0 {
|
||||
said := onWaiting
|
||||
if waits {
|
||||
said = append(said, wait)
|
||||
@@ -708,6 +716,10 @@ func moduleHealthWord(module, machine string, since time.Time, f gateFacts) (hea
|
||||
said = append(said, fmt.Sprintf("on %s, %s uses %s as found and waits for the operator to hand it over "+
|
||||
"(`nox node hand-over %s <directory>` on the control-node)", machine, module, strings.Join(found, ", "), machine))
|
||||
}
|
||||
if len(leftover) > 0 {
|
||||
said = append(said, fmt.Sprintf("on %s, the mesh's entries %s could not be taken out while their disk was "+
|
||||
"away, stated before this send: they are taken out when it is back", machine, strings.Join(leftover, ", ")))
|
||||
}
|
||||
return healthPerson, strings.Join(said, "; ")
|
||||
}
|
||||
return healthGood, ""
|
||||
|
||||
@@ -45,10 +45,11 @@ type operatorWaitFacts struct {
|
||||
manifests map[string]catalogue.Manifest
|
||||
given map[string]map[string]time.Time
|
||||
// placed is, per "<module>@<machine>", where each access of the module is on that machine, by id (novox/hq ADR
|
||||
// 0325 §7); taken is, per machine, every folder the operator let the mesh keep there. A machine absent from
|
||||
// taken could not be read, and no access wait of it is excused.
|
||||
// 0325 §7); taken is, per machine, every folder the operator let the mesh keep there, with the filesystem it was
|
||||
// taken on (empty when the engine named none). A machine absent from taken could not be read, and no access wait
|
||||
// of it is excused.
|
||||
placed map[string]map[string]string
|
||||
taken map[string]map[string]bool
|
||||
taken map[string]map[string]string
|
||||
}
|
||||
|
||||
// checkWait is nil when a wait is excused, and otherwise why not, in words. Pure.
|
||||
@@ -119,7 +120,9 @@ func checkAccessWait(module, machine string, m catalogue.Manifest, w inventory.W
|
||||
return fmt.Errorf("says it waits for the access %s, and which folders are taken on %s could not be read",
|
||||
w.Access, machine)
|
||||
}
|
||||
if taken[path] {
|
||||
// A folder taken on another filesystem than the one it is on now (it moved to another disk) is not taken by that
|
||||
// warrant: the wait is excused, and the operator is asked again (ADR 0325 §8).
|
||||
if on, was := taken[path]; was && (w.Placed == "" || on == w.Placed) {
|
||||
return fmt.Errorf("says it waits for the access %s, whose folder is taken on %s already: a module that gains "+
|
||||
"an access to a taken folder is given its entries without a wait", w.Access, machine)
|
||||
}
|
||||
@@ -284,7 +287,7 @@ func readWaitFacts(ctx context.Context, inv *inventory.Inventory, machine string
|
||||
f.placed = map[string]map[string]string{}
|
||||
}
|
||||
if f.taken == nil {
|
||||
f.taken = map[string]map[string]bool{}
|
||||
f.taken = map[string]map[string]string{}
|
||||
}
|
||||
if inv == nil {
|
||||
return
|
||||
@@ -330,9 +333,9 @@ func readWaitFacts(ctx context.Context, inv *inventory.Inventory, machine string
|
||||
}
|
||||
if _, read := f.taken[machine]; !read {
|
||||
if taken, err := inv.KeptFolders(ctx, machine); err == nil {
|
||||
kept := map[string]bool{}
|
||||
for path := range taken {
|
||||
kept[path] = true
|
||||
kept := map[string]string{}
|
||||
for path, t := range taken {
|
||||
kept[path] = t.Placed
|
||||
}
|
||||
f.taken[machine] = kept
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -1066,6 +1067,20 @@ func folderFacts(ctx context.Context, inv *inventory.Inventory, node string, pla
|
||||
for path := range taken {
|
||||
kept[path] = true
|
||||
}
|
||||
// **A folder that moved to another filesystem is not taken there until its new warrant** (ADR 0325 §8): the
|
||||
// machine's newest statement names, without a path, the filesystem each folder waiting for the operator is on now.
|
||||
if stated, had, err := inv.HealthOf(ctx, node); err != nil {
|
||||
return false, nil, nil, err
|
||||
} else if had {
|
||||
for _, r := range stated.Resources {
|
||||
for _, w := range r.Waits {
|
||||
path := filepath.Clean(r.Target)
|
||||
if t, was := taken[path]; was && w.Access != "" && !w.Disk && w.Placed != "" && w.Placed != t.Placed {
|
||||
kept[path] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true, identities, kept, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -35,4 +35,4 @@ require (
|
||||
// committed. Every build (the build agent's `go build`, the Dockerfile) compiles from vendor/ and
|
||||
// fetches nothing; go refuses to build when vendor/ and this file disagree, so a pin moved without
|
||||
// `go mod vendor` fails loudly, at once, everywhere.
|
||||
replace github.com/novox/mesh-host => git.novox.be/novox/mesh-host v0.0.0-20261012164304-51e97a8c1405
|
||||
replace github.com/novox/mesh-host => git.novox.be/novox/mesh-host v0.0.0-20261012170302-d2d9fbbd7d1c
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-host v0.0.0-20261012164304-51e97a8c1405 h1:aC+TkJr9B6qYbeLNmzs4FtOm8/hIJw0ZrIpbU+bFWZE=
|
||||
git.novox.be/novox/mesh-host v0.0.0-20261012164304-51e97a8c1405/go.mod h1:tcTK4LMs1d6JpZUwy3hSHMFG/MIuDr/XFs7/nbeaW/c=
|
||||
git.novox.be/novox/mesh-host v0.0.0-20261012170302-d2d9fbbd7d1c h1:5k5y4bML7BQtFVPstHVHkQUdjKUpOQII4yTbu1MKMGY=
|
||||
git.novox.be/novox/mesh-host v0.0.0-20261012170302-d2d9fbbd7d1c/go.mod h1:tcTK4LMs1d6JpZUwy3hSHMFG/MIuDr/XFs7/nbeaW/c=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261010191001-33917f91ac3c h1:l5onJwoIeH8yE/PjVlEeoPyXBsHp2NQiWLllz2fwX7s=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261010191001-33917f91ac3c/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
|
||||
@@ -48,6 +48,9 @@ type Wait struct {
|
||||
// waits for the disk that holds its folder rather than for the operator's word (novox/hq ADR 0325 §7, §9).
|
||||
Access string `json:"access,omitempty"`
|
||||
Disk bool `json:"disk,omitempty"`
|
||||
// Placed is the filesystem the folder is on now, without its path (ADR 0325 §8): a warrant for another does not
|
||||
// answer the wait.
|
||||
Placed string `json:"placed,omitempty"`
|
||||
What string `json:"what"`
|
||||
}
|
||||
|
||||
|
||||
@@ -78,12 +78,14 @@ type TakenFolder struct {
|
||||
Ask string
|
||||
Warrant string
|
||||
Said string
|
||||
// Placed is the filesystem the folder was on when taken, without its path (ADR 0325 §8).
|
||||
Placed string
|
||||
}
|
||||
|
||||
// KeptFolders is every folder taken on a machine, by its clean path.
|
||||
func (i *Inventory) KeptFolders(ctx context.Context, nodeName string) (map[string]TakenFolder, error) {
|
||||
rows, err := i.store.Pool().Query(ctx,
|
||||
`select k.path, k.taken_by, k.ask, k.warrant, k.said from kept_folder k join node n on n.id = k.node
|
||||
`select k.path, k.taken_by, k.ask, k.warrant, k.said, k.placed from kept_folder k join node n on n.id = k.node
|
||||
where n.name = $1`, nodeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -92,7 +94,7 @@ func (i *Inventory) KeptFolders(ctx context.Context, nodeName string) (map[strin
|
||||
out := map[string]TakenFolder{}
|
||||
for rows.Next() {
|
||||
var t TakenFolder
|
||||
if err := rows.Scan(&t.Path, &t.By, &t.Ask, &t.Warrant, &t.Said); err != nil {
|
||||
if err := rows.Scan(&t.Path, &t.By, &t.Ask, &t.Warrant, &t.Said, &t.Placed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[t.Path] = t
|
||||
@@ -100,8 +102,9 @@ func (i *Inventory) KeptFolders(ctx context.Context, nodeName string) (map[strin
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// RecordTaken keeps that a folder was taken on a machine, under the operator's warrant. A take is recorded once:
|
||||
// a second for the same folder keeps the first.
|
||||
// RecordTaken keeps that a folder was taken on a machine, under the operator's warrant. A take is recorded once per
|
||||
// filesystem: a second for the same folder on the same filesystem keeps the first, and one for the folder on another
|
||||
// filesystem (it moved, and the operator was asked again) replaces it.
|
||||
func (i *Inventory) RecordTaken(ctx context.Context, nodeName string, t TakenFolder) error {
|
||||
switch {
|
||||
case !strings.HasPrefix(t.Path, "/"):
|
||||
@@ -114,7 +117,11 @@ func (i *Inventory) RecordTaken(ctx context.Context, nodeName string, t TakenFol
|
||||
return err
|
||||
}
|
||||
_, err = i.store.Pool().Exec(ctx,
|
||||
`insert into kept_folder (node, path, taken_by, ask, warrant, said) values ($1, $2, $3, $4, $5, $6)
|
||||
on conflict (node, path) do nothing`, node.ID, filepath.Clean(t.Path), t.By, t.Ask, t.Warrant, t.Said)
|
||||
`insert into kept_folder (node, path, taken_by, ask, warrant, said, placed) values ($1, $2, $3, $4, $5, $6, $7)
|
||||
on conflict (node, path) do update
|
||||
set taken_at = now(), taken_by = excluded.taken_by, ask = excluded.ask, warrant = excluded.warrant,
|
||||
said = excluded.said, placed = excluded.placed
|
||||
where kept_folder.placed is distinct from excluded.placed`, node.ID, filepath.Clean(t.Path), t.By, t.Ask,
|
||||
t.Warrant, t.Said, t.Placed)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -96,6 +96,18 @@ func TestAFolderIsTakenOnceUnderAWarrant(t *testing.T) {
|
||||
if !has || got.Ask != "c01" || got.By != "the operator" {
|
||||
t.Fatalf("the taken folder reads %+v (%v): the first take, by its clean path", got, has)
|
||||
}
|
||||
// The folder moved to another filesystem and the operator was asked again: the new take replaces the old.
|
||||
if err := inv.RecordTaken(ctx, "ace", TakenFolder{Path: "/storage/media/movies", By: "the operator", Ask: "c03",
|
||||
Warrant: "c03", Placed: "fs-0000000000ff"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
kept, err = inv.KeptFolders(ctx, "ace")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := kept["/storage/media/movies"]; got.Ask != "c03" || got.Placed != "fs-0000000000ff" {
|
||||
t.Fatalf("after a take on another filesystem the folder reads %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq issue 575: a module registered without the access movies, whose newest build on its branch declares it —
|
||||
|
||||
+5
-1
@@ -25,7 +25,7 @@ create table module_identity (
|
||||
-- everything there, and the folder is taken: kept from then on without asking, a module that gains the access
|
||||
-- added with no wait. The row is the record of that act — who approved it, under which warrant — and what tells a
|
||||
-- later module's access that it joins a taken folder (`taken` on the access). The path is the folder as placed,
|
||||
-- clean and absolute.
|
||||
-- clean and absolute. A new warrant for the same folder on another filesystem replaces the row.
|
||||
create table kept_folder (
|
||||
node uuid not null references node (id) on delete cascade,
|
||||
path text not null check (path like '/%'),
|
||||
@@ -36,5 +36,9 @@ create table kept_folder (
|
||||
warrant text not null,
|
||||
-- What the node-engine answered when it started the take.
|
||||
said text not null default '',
|
||||
-- The filesystem the folder was on when it was taken, as the engine names it without a path ("fs-<12 hex>"):
|
||||
-- a wait naming another (the folder moved to another disk) is not answered by this take, and the operator is
|
||||
-- asked again; until the new warrant the folder is not taken there. Empty from an engine that names none.
|
||||
placed text not null default '',
|
||||
primary key (node, path)
|
||||
);
|
||||
|
||||
@@ -601,6 +601,10 @@ type AccessWait struct {
|
||||
Access string `json:"access"`
|
||||
Part string `json:"part"`
|
||||
What string `json:"what"`
|
||||
// Placed names, with no path, the filesystem the folder is on now ("fs-<12 hex>"), on a wait for the operator's
|
||||
// word: a warrant given for another filesystem (the folder moved) does not answer it, and the controller asks
|
||||
// again (mesh-host internal/link AccessWait.Placed).
|
||||
Placed string `json:"placed,omitempty"`
|
||||
}
|
||||
|
||||
// WaitsForDisk says an access wait is for a disk that is not there (ADR 0325 §9) rather than for the operator's word
|
||||
@@ -609,6 +613,11 @@ func (w AccessWait) WaitsForDisk() bool {
|
||||
return strings.HasPrefix(strings.TrimSpace(w.What), "mount the disk")
|
||||
}
|
||||
|
||||
// KindKeptEntries is the mesh's entries on a shared folder that nobody wants and that could not be taken out yet
|
||||
// because its disk was away (novox/hq ADR 0325 §8; mesh-host internal/link): unhealthy, said under the access that
|
||||
// last named it (`<access id>~entries`), which may no longer be declared. The node-engine's own.
|
||||
const KindKeptEntries = "kept-entries"
|
||||
|
||||
// KeptEntry is one user of a kept folder as the engine's listing says it (ADR 0325 §4), with a note beside the tool
|
||||
// runner's entry.
|
||||
type KeptEntry struct {
|
||||
|
||||
Vendored
+1
-1
@@ -8,7 +8,7 @@ private addresses. That is acceptable while mesh-controller is private; if it ev
|
||||
a second place to clean besides the catalogue itself.
|
||||
|
||||
mesh-catalog b9de001833b1b61b297182b8e3bcdae1cbdeace9 modules/*/module.json, modules/nats/Dockerfile
|
||||
mesh-host 51e97a8c14058fd58e108a045b5d94744f23a6ee examples/foundation-first-node-nats.lock
|
||||
mesh-host d2d9fbbd7d1cb92efaf404ebe0bd8dd91896277f examples/foundation-first-node-nats.lock
|
||||
mesh-sdk 33917f91ac3cc213b879b32f3c70e2aa70395f98 conformance/events/module-event.json
|
||||
|
||||
To move them, from this repository's root, with the two repositories checked out beside it:
|
||||
|
||||
Vendored
+2
-2
@@ -78,7 +78,7 @@ github.com/nats-io/nkeys
|
||||
# github.com/nats-io/nuid v1.0.1
|
||||
## explicit
|
||||
github.com/nats-io/nuid
|
||||
# github.com/novox/mesh-host v0.0.0 => git.novox.be/novox/mesh-host v0.0.0-20261012164304-51e97a8c1405
|
||||
# github.com/novox/mesh-host v0.0.0 => git.novox.be/novox/mesh-host v0.0.0-20261012170302-d2d9fbbd7d1c
|
||||
## explicit; go 1.26.0
|
||||
github.com/novox/mesh-host/internal/declaration
|
||||
github.com/novox/mesh-host/rootsearch
|
||||
@@ -135,4 +135,4 @@ golang.org/x/text/width
|
||||
# golang.org/x/time v0.15.0
|
||||
## explicit; go 1.25.0
|
||||
golang.org/x/time/rate
|
||||
# github.com/novox/mesh-host => git.novox.be/novox/mesh-host v0.0.0-20261012164304-51e97a8c1405
|
||||
# github.com/novox/mesh-host => git.novox.be/novox/mesh-host v0.0.0-20261012170302-d2d9fbbd7d1c
|
||||
|
||||
Reference in New Issue
Block a user