Hold a joining module at the build it ran, own its directories by its own group, and take a library only when its act is done (hq ADR 0328 review, ADR 0334)
A held module is composed exactly as before it joined, at the build its machine runs, so its port, resources and contributions stay. A joining module's directories belong to its user and its own group, and its tools' directories are refused closed to that group; the runner joins it where the engine can say so. A take records the root and is taken only when the engine states the act done. Libraries' paths and verdicts count modules pulled in by a requirement, and a unit takes the identity.
This commit is contained in:
@@ -179,13 +179,17 @@ func (i *Inventory) LibraryNamed(ctx context.Context, module string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// LibraryPaths is every sub-path a module assigned anywhere asks of each library (§6): what its home keeps.
|
||||
// LibraryPaths is every sub-path a module on any machine asks of each library (§6): what its home keeps. A module on a
|
||||
// machine is one assigned there or one a requirement pulled in — what the machine was last sent.
|
||||
func (i *Inventory) LibraryPaths(ctx context.Context) (map[string][]string, error) {
|
||||
shelf, err := i.Catalogue(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := i.store.Pool().Query(ctx, `select distinct module from assignment`)
|
||||
// Every module assigned anywhere, and every module a machine was last sent — those a requirement pulled in too.
|
||||
rows, err := i.store.Pool().Query(ctx,
|
||||
`select module from assignment
|
||||
union select jsonb_object_keys(sent_builds) from node where sent_builds is not null and jsonb_typeof(sent_builds) = 'object'`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -219,17 +223,20 @@ func (i *Inventory) LibraryPaths(ctx context.Context) (map[string][]string, erro
|
||||
type TakenLibrary struct {
|
||||
Library string
|
||||
Home string
|
||||
Root string
|
||||
Placed string
|
||||
By string
|
||||
Ask string
|
||||
Warrant string
|
||||
Said string
|
||||
// Done is whether the engine stated the take's act done: only then is the library taken.
|
||||
Done bool
|
||||
}
|
||||
|
||||
// TakenLibraries is every library taken, by name.
|
||||
// TakenLibraries is every library whose take was started, by name; Done says it is taken.
|
||||
func (i *Inventory) TakenLibraries(ctx context.Context) (map[string]TakenLibrary, error) {
|
||||
rows, err := i.store.Pool().Query(ctx,
|
||||
`select library, home, placed, taken_by, ask, warrant, said from kept_library`)
|
||||
`select library, home, root, placed, taken_by, ask, warrant, said, done from kept_library`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -237,7 +244,7 @@ func (i *Inventory) TakenLibraries(ctx context.Context) (map[string]TakenLibrary
|
||||
out := map[string]TakenLibrary{}
|
||||
for rows.Next() {
|
||||
var t TakenLibrary
|
||||
if err := rows.Scan(&t.Library, &t.Home, &t.Placed, &t.By, &t.Ask, &t.Warrant, &t.Said); err != nil {
|
||||
if err := rows.Scan(&t.Library, &t.Home, &t.Root, &t.Placed, &t.By, &t.Ask, &t.Warrant, &t.Said, &t.Done); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[t.Library] = t
|
||||
@@ -245,23 +252,41 @@ func (i *Inventory) TakenLibraries(ctx context.Context) (map[string]TakenLibrary
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// RecordTaken keeps that a library was taken on its home, under the operator's warrant. Once per home and filesystem:
|
||||
// a second take of the same keeps the first, and one on another home or filesystem (it moved, and the operator was
|
||||
// asked again) replaces it.
|
||||
func (i *Inventory) RecordTaken(ctx context.Context, t TakenLibrary) error {
|
||||
// RecordTakeStarted keeps that a take of a library was started on its home, under the operator's warrant: the
|
||||
// engine said it began. The library is taken only once the engine states the act done (TakeDone). Once per home, root
|
||||
// and filesystem: a second start of the same keeps the first, and one on another (it moved, and the operator was asked
|
||||
// again) replaces it, not done.
|
||||
func (i *Inventory) RecordTakeStarted(ctx context.Context, t TakenLibrary) error {
|
||||
switch {
|
||||
case t.Library == "" || t.Home == "":
|
||||
return errors.New("a taken library names itself and its home")
|
||||
case t.Library == "" || t.Home == "" || !strings.HasPrefix(t.Root, "/"):
|
||||
return errors.New("a taken library names itself, its home and its root")
|
||||
case strings.TrimSpace(t.Warrant) == "" || strings.TrimSpace(t.Ask) == "":
|
||||
return errors.New("a library is taken only on the operator's warrant, and this names none")
|
||||
}
|
||||
_, err := i.store.Pool().Exec(ctx,
|
||||
`insert into kept_library (library, home, placed, taken_by, ask, warrant, said)
|
||||
values ($1, $2, $3, $4, $5, $6, $7)
|
||||
`insert into kept_library (library, home, root, placed, taken_by, ask, warrant, said, done)
|
||||
values ($1, $2, $3, $4, $5, $6, $7, $8, false)
|
||||
on conflict (library) do update
|
||||
set home = excluded.home, placed = excluded.placed, taken_at = now(), taken_by = excluded.taken_by,
|
||||
ask = excluded.ask, warrant = excluded.warrant, said = excluded.said
|
||||
where kept_library.home <> excluded.home or kept_library.placed <> excluded.placed`,
|
||||
t.Library, t.Home, t.Placed, t.By, t.Ask, t.Warrant, t.Said)
|
||||
set home = excluded.home, root = excluded.root, placed = excluded.placed, taken_at = now(),
|
||||
taken_by = excluded.taken_by, ask = excluded.ask, warrant = excluded.warrant, said = excluded.said,
|
||||
done = false
|
||||
where kept_library.home <> excluded.home or kept_library.root <> excluded.root
|
||||
or kept_library.placed <> excluded.placed`,
|
||||
t.Library, t.Home, t.Root, t.Placed, t.By, t.Ask, t.Warrant, t.Said)
|
||||
return err
|
||||
}
|
||||
|
||||
// TakeDone keeps that the engine stated a library's take done under a warrant: the library is taken. A warrant the
|
||||
// recorded take was not started under changes nothing.
|
||||
func (i *Inventory) TakeDone(ctx context.Context, library, warrant string) error {
|
||||
_, err := i.store.Pool().Exec(ctx,
|
||||
`update kept_library set done = true where library = $1 and warrant = $2 and not done`, library, warrant)
|
||||
return err
|
||||
}
|
||||
|
||||
// TakeFailed forgets a take the engine stated failed under a warrant, never one done: the library is asked about again.
|
||||
func (i *Inventory) TakeFailed(ctx context.Context, library, warrant string) error {
|
||||
_, err := i.store.Pool().Exec(ctx,
|
||||
`delete from kept_library where library = $1 and warrant = $2 and not done`, library, warrant)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package inventory
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -157,17 +158,17 @@ func TestALibraryIsDefinedOnceForTheMeshAndNeverOverlaps(t *testing.T) {
|
||||
func TestALibraryIsTakenOnceUnderAWarrant(t *testing.T) {
|
||||
inv := fresh(t)
|
||||
ctx := t.Context()
|
||||
if err := inv.RecordTaken(ctx, TakenLibrary{Library: "media", Home: "ace", By: "the operator"}); err == nil {
|
||||
if err := inv.RecordTakeStarted(ctx, TakenLibrary{Library: "media", Home: "ace", By: "the operator"}); err == nil {
|
||||
t.Fatal("a library was recorded taken with no warrant")
|
||||
}
|
||||
first := TakenLibrary{Library: "media", Home: "ace", Placed: "fs-000000000001", By: "the operator", Ask: "c01",
|
||||
Warrant: "c01"}
|
||||
if err := inv.RecordTaken(ctx, first); err != nil {
|
||||
first := TakenLibrary{Library: "media", Home: "ace", Root: "/storage/media", Placed: "fs-000000000001",
|
||||
By: "the operator", Ask: "c01", Warrant: "c01"}
|
||||
if err := inv.RecordTakeStarted(ctx, first); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
again := first
|
||||
again.Ask, again.Warrant, again.By = "c02", "c02", "someone else"
|
||||
if err := inv.RecordTaken(ctx, again); err != nil {
|
||||
if err := inv.RecordTakeStarted(ctx, again); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
taken, err := inv.TakenLibraries(ctx)
|
||||
@@ -179,12 +180,25 @@ func TestALibraryIsTakenOnceUnderAWarrant(t *testing.T) {
|
||||
}
|
||||
moved := first
|
||||
moved.Placed, moved.Ask, moved.Warrant = "fs-0000000000ff", "c03", "c03"
|
||||
if err := inv.RecordTaken(ctx, moved); err != nil {
|
||||
if err := inv.RecordTakeStarted(ctx, moved); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if taken, err = inv.TakenLibraries(ctx); err != nil || taken["media"].Ask != "c03" {
|
||||
t.Fatalf("a take on another filesystem: %+v (%v)", taken["media"], err)
|
||||
}
|
||||
// The review of 7ea6d36e, item 3: the root is part of what was taken — a library whose root changed is taken
|
||||
// again, and the new take replaces the old.
|
||||
if got := taken["media"].Root; got != "/storage/media" {
|
||||
t.Fatalf("the take recorded the root %q", got)
|
||||
}
|
||||
rooted := moved
|
||||
rooted.Root, rooted.Ask, rooted.Warrant = "/storage/media2", "c04", "c04"
|
||||
if err := inv.RecordTakeStarted(ctx, rooted); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if taken, err = inv.TakenLibraries(ctx); err != nil || taken["media"].Ask != "c04" || taken["media"].Root != "/storage/media2" {
|
||||
t.Fatalf("a take at another root: %+v (%v)", taken["media"], err)
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq issue 575: a module registered without the access movies, whose newest build on its branch declares it —
|
||||
@@ -298,3 +312,82 @@ func TestOnlyABuildOfTheFollowedBranchJudgesAPlacementAhead(t *testing.T) {
|
||||
t.Fatalf("a module with no branch recorded: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The review of 7ea6d36e, item 4: every sub-path a module asks of a library is kept on its home, whether the module
|
||||
// was assigned or pulled in by a requirement (it is in what its machine was last sent).
|
||||
func TestTheSubPathsOfModulesPulledInByARequirementAreKept(t *testing.T) {
|
||||
inv := fresh(t)
|
||||
ctx := t.Context()
|
||||
record, err := inv.AddNode(ctx, "ace")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, m := range []catalogue.Manifest{
|
||||
{Module: "radarr", Version: "1", Libraries: []catalogue.LibraryUse{{ID: "m", Library: "media", Path: "movies", Use: "read-write"}}},
|
||||
{Module: "importer", Version: "1", Libraries: []catalogue.LibraryUse{{ID: "i", Library: "media", Path: "incoming", Use: "read-write"}}},
|
||||
} {
|
||||
if err := inv.RegisterModule(ctx, m, Source{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if _, err := inv.Assign(ctx, "ace", "radarr"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.RecordSentWith(ctx, record.ID, "d1", map[string]string{"radarr": "c1", "importer": "c2"}, 1, 1,
|
||||
Send{Sequence: 1}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
paths, err := inv.LibraryPaths(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := append([]string{}, paths["media"]...)
|
||||
sort.Strings(got)
|
||||
if strings.Join(got, ",") != "incoming,movies" {
|
||||
t.Fatalf("the paths kept of media are %v; want the importer's, pulled in by a requirement, too", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The review of 7ea6d36e, item 5: a take the engine says it started is recorded as started, not taken; it is taken when
|
||||
// the engine states the act done under that warrant, and forgotten when it states it failed, so the operator is asked
|
||||
// again.
|
||||
func TestALibraryIsTakenWhenTheActIsDone(t *testing.T) {
|
||||
inv := fresh(t)
|
||||
ctx := t.Context()
|
||||
started := TakenLibrary{Library: "media", Home: "ace", Root: "/storage/media", Placed: "fs-000000000001",
|
||||
By: "the operator", Ask: "c01", Warrant: "c01"}
|
||||
if err := inv.RecordTakeStarted(ctx, started); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
taken, err := inv.TakenLibraries(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := taken["media"]; got.Done || got.Ask != "c01" {
|
||||
t.Fatalf("a take only started reads %+v; want started, not done", got)
|
||||
}
|
||||
if err := inv.TakeDone(ctx, "media", "c99"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if taken, _ = inv.TakenLibraries(ctx); taken["media"].Done {
|
||||
t.Fatal("another warrant's act made the take done")
|
||||
}
|
||||
if err := inv.TakeDone(ctx, "media", "c01"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if taken, _ = inv.TakenLibraries(ctx); !taken["media"].Done {
|
||||
t.Fatal("the act done under the take's warrant did not make it taken")
|
||||
}
|
||||
// A failed act forgets a take not done, so it is asked again; never one done.
|
||||
again := started
|
||||
again.Root, again.Ask, again.Warrant = "/storage/media2", "c02", "c02"
|
||||
if err := inv.RecordTakeStarted(ctx, again); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.TakeFailed(ctx, "media", "c02"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if taken, _ = inv.TakenLibraries(ctx); taken["media"].Ask == "c02" {
|
||||
t.Fatalf("a failed take is still recorded: %+v", taken["media"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,12 +26,14 @@ create table library_group (
|
||||
-- **A library taken** (§7). The first time a library's root on its home holds data not yet in its group, the operator
|
||||
-- is asked once for that library; on the warrant the node-engine sets the group, group write and setgid under the root,
|
||||
-- and the library is taken: kept from then on without asking. The row is the record of that act — on which machine
|
||||
-- and filesystem, who approved it, under which warrant. A library whose home or filesystem changed since (it moved to
|
||||
-- another machine or another disk) is not taken by that warrant, and the operator is asked again; a new take replaces
|
||||
-- the row.
|
||||
-- root and filesystem, who approved it, under which warrant. A library whose home, root or filesystem changed since
|
||||
-- (it moved to another machine, path or disk) is not taken by that warrant, and the operator is asked again; a new take
|
||||
-- replaces the row.
|
||||
create table kept_library (
|
||||
library text primary key,
|
||||
home text not null,
|
||||
-- The root it was taken at: a library whose root changed since is taken again.
|
||||
root text not null check (root like '/%'),
|
||||
-- The filesystem the root was on when taken, as the engine names it without a path ("fs-<12 hex>"); empty when
|
||||
-- the engine named none.
|
||||
placed text not null default '',
|
||||
@@ -40,5 +42,9 @@ create table kept_library (
|
||||
ask text not null,
|
||||
warrant text not null,
|
||||
-- What the node-engine answered when it started the take.
|
||||
said text not null default ''
|
||||
said text not null default '',
|
||||
-- Whether the engine has stated the take's act done under this warrant: only then is the library taken. Until
|
||||
-- then the take is under way — the library still waits, and the operator is not asked again — and an act stated
|
||||
-- failed forgets the row, so the operator is asked again.
|
||||
done boolean not null default false
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user