Merge pull request 'Each node's engine writes its own declared packages and answers packages (ADR 0565 §6, issue 1953)' (#193) from feat/node-packages into main
This commit was merged in pull request #193.
This commit is contained in:
@@ -329,3 +329,38 @@ func composeFrom(current *agentNow, composer *atomic.Pointer[selfcompose.Compose
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// packagesReadFor bounds the verb packages' reads and its write.
|
||||||
|
const packagesReadFor = 20 * time.Second
|
||||||
|
|
||||||
|
// packagesFrom answers the verb packages (novox/hq ADR 0565 §6, §15): this node's key of the engine's state packages
|
||||||
|
// beside the controller's mirror's key for it and their difference; with write, the key written again from what the
|
||||||
|
// composer that runs now handed, refused in words while the node composes none of its own.
|
||||||
|
func packagesFrom(node string, session *link.Session, composer *atomic.Pointer[selfcompose.Composer]) func(write bool) (any, error) {
|
||||||
|
return func(write bool) (any, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), packagesReadFor)
|
||||||
|
defer cancel()
|
||||||
|
var wrote *bool
|
||||||
|
if write {
|
||||||
|
c := composer.Load()
|
||||||
|
if c == nil {
|
||||||
|
return nil, errors.New("this node composes none of its own modules now (it applies the controller's " +
|
||||||
|
"compositions, or its node agent is starting), so it has no packages of its own to write")
|
||||||
|
}
|
||||||
|
w, err := c.WritePackages(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("this node's key of %s is not written: %w", link.PackagesBucket, err)
|
||||||
|
}
|
||||||
|
wrote = &w
|
||||||
|
}
|
||||||
|
key, keyErr := link.EnginePackages{Session: session}.Get(ctx, node)
|
||||||
|
var mirror []byte
|
||||||
|
h, mirrorErr := session.KVHistory(ctx, link.MirrorPackagesBucket, node)
|
||||||
|
if mirrorErr == nil && len(h) > 0 && !h[len(h)-1].Deleted {
|
||||||
|
mirror = h[len(h)-1].Value
|
||||||
|
}
|
||||||
|
v := link.PackagesAnswer(node, key, mirror, keyErr, mirrorErr)
|
||||||
|
v.Wrote = wrote
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1143,6 +1143,7 @@ func runLink(ctx context.Context, opts options) error {
|
|||||||
current := &agentNow{}
|
current := &agentNow{}
|
||||||
queue.Acts.Composition = compositionFrom(current)
|
queue.Acts.Composition = compositionFrom(current)
|
||||||
queue.Acts.Compose = composeFrom(current, &composerNow)
|
queue.Acts.Compose = composeFrom(current, &composerNow)
|
||||||
|
queue.Acts.Packages = packagesFrom(mine.Node, queue, &composerNow)
|
||||||
setRunsHere(runsFromAgent(current))
|
setRunsHere(runsFromAgent(current))
|
||||||
factsOfNow := func(ctx context.Context, ag *agent.Agent) {
|
factsOfNow := func(ctx context.Context, ag *agent.Agent) {
|
||||||
sayFacts(ctx, opts, mine.Membership.Signer, queue, say, ag)
|
sayFacts(ctx, opts, mine.Membership.Signer, queue, say, ag)
|
||||||
|
|||||||
@@ -1024,6 +1024,9 @@ func selfComposer(ctx context.Context, session *link.Session, node string, say f
|
|||||||
// And it keeps each placed module's data items in its own state data, which the node's disk-load watches (novox/hq
|
// And it keeps each placed module's data items in its own state data, which the node's disk-load watches (novox/hq
|
||||||
// ADR 0577 clause 1); the shadow composes for candidates and keeps none.
|
// ADR 0577 clause 1); the shadow composes for candidates and keeps none.
|
||||||
env.Data = link.EngineData{Session: session}
|
env.Data = link.EngineData{Session: session}
|
||||||
|
// And the packages its compositions declare in its own key of the state packages, which pacman reads (novox/hq ADR
|
||||||
|
// 0565 §6).
|
||||||
|
env.Packages = link.EnginePackages{Session: session}
|
||||||
unreadable := ""
|
unreadable := ""
|
||||||
placed, err := selfcompose.Placed(ctx, env.Get, node, link.Backoff, func(m string) {
|
placed, err := selfcompose.Placed(ctx, env.Get, node, link.Backoff, func(m string) {
|
||||||
if strings.Contains(m, "cannot be read") {
|
if strings.Contains(m, "cannot be read") {
|
||||||
|
|||||||
@@ -405,7 +405,19 @@ func TestNatsANodeThePlacementNamesComposesAndAppliesItsOwn(t *testing.T) {
|
|||||||
placement, resolved, candidates := st.placement, st.resolved, st.candidates
|
placement, resolved, candidates := st.placement, st.resolved, st.candidates
|
||||||
hkey, place, fp := st.hkey, st.place, st.fp
|
hkey, place, fp := st.hkey, st.place, st.fp
|
||||||
within(t, 5*time.Second, "the node did not compose and apply its own module once placed", func() bool { return b.file("web") == "greeting=bonjour\n" })
|
within(t, 5*time.Second, "the node did not compose and apply its own module once placed", func() bool { return b.file("web") == "greeting=bonjour\n" })
|
||||||
|
// ADR 0565 §6 (1953): the node's engine writes its own key of the state packages, under the grant the grants part
|
||||||
|
// composes for it, with each package its compositions declare and the module declaring it; once, while nothing changes.
|
||||||
|
wantPkgs := `{"node":"` + itNode + `","packages":[{"package":"web-tools","modules":["web"],"absent":false}]}`
|
||||||
|
within(t, 5*time.Second, "the node did not write its key of the state packages", func() bool {
|
||||||
|
e, err := st.packages.Get(itNode)
|
||||||
|
return err == nil && string(e.Value()) == wantPkgs
|
||||||
|
})
|
||||||
|
written, _ := st.packages.Get(itNode)
|
||||||
h8rest(t, b, resolved, candidates, hkey, place, fp)
|
h8rest(t, b, resolved, candidates, hkey, place, fp)
|
||||||
|
// web composed again for its changed config value declares the same packages: the key is not written again.
|
||||||
|
if e, _ := st.packages.Get(itNode); e == nil || written == nil || e.Revision() != written.Revision() {
|
||||||
|
t.Errorf("the node's packages key was written again with no package changed: %v, was %v", e, written)
|
||||||
|
}
|
||||||
t.Run("one answer per candidate revision", func(t *testing.T) { h8once(t, b, resolved, candidates, place) })
|
t.Run("one answer per candidate revision", func(t *testing.T) { h8once(t, b, resolved, candidates, place) })
|
||||||
h8back(t, b, placement, resolved, candidates, hkey)
|
h8back(t, b, placement, resolved, candidates, hkey)
|
||||||
}
|
}
|
||||||
@@ -552,9 +564,11 @@ type Have1126 struct {
|
|||||||
// h8state is what h8 set up that the rest of H8 writes to.
|
// h8state is what h8 set up that the rest of H8 writes to.
|
||||||
type h8state struct {
|
type h8state struct {
|
||||||
placement, resolved, candidates nats.KeyValue
|
placement, resolved, candidates nats.KeyValue
|
||||||
hkey string
|
// packages is the engine's state packages (ADR 0565 §6), as the grants part creates it from the engine's manifest.
|
||||||
place []byte
|
packages nats.KeyValue
|
||||||
fp string
|
hkey string
|
||||||
|
place []byte
|
||||||
|
fp string
|
||||||
}
|
}
|
||||||
|
|
||||||
// h8 sets H8's bus up and starts the agent, then places the node. With checkBuild, the placed module's build key is
|
// h8 sets H8's bus up and starts the agent, then places the node. With checkBuild, the placed module's build key is
|
||||||
@@ -572,13 +586,15 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) {
|
|||||||
resolved := kvOf(selfcompose.ResolvedBucket)
|
resolved := kvOf(selfcompose.ResolvedBucket)
|
||||||
builds := kvOf(delivery.BuilderBuildsBucket)
|
builds := kvOf(delivery.BuilderBuildsBucket)
|
||||||
candidates := kvOf(delivery.PlacementCandidatesBucket)
|
candidates := kvOf(delivery.PlacementCandidatesBucket)
|
||||||
|
packages := kvOf(link.PackagesBucket)
|
||||||
// The controller's stream the node's facts go into, so the node's key carries the facts it says.
|
// The controller's stream the node's facts go into, so the node's key carries the facts it says.
|
||||||
if _, err := b.js.AddStream(&nats.StreamConfig{Name: "CONTROL", Subjects: []string{link.ReportSubject(itNode)}}); err != nil {
|
if _, err := b.js.AddStream(&nats.StreamConfig{Name: "CONTROL", Subjects: []string{link.ReportSubject(itNode)}}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
path := filepath.Join(b.dir, "web.conf")
|
path := filepath.Join(b.dir, "web.conf")
|
||||||
man := `{"module":"web","version":"1","config":{"greeting":{"type":"string","description":"what the file says","default":"hello"}},` +
|
man := `{"module":"web","version":"1","config":{"greeting":{"type":"string","description":"what the file says","default":"hello"}},` +
|
||||||
`"resources":[{"id":"conf","type":"file","path":"` + path + `","mode":"0644","content":"greeting=${config:greeting}\n"}],` +
|
`"resources":[{"id":"conf","type":"file","path":"` + path + `","mode":"0644","content":"greeting=${config:greeting}\n"},` +
|
||||||
|
`{"id":"tools","type":"package","package":"web-tools"}],` +
|
||||||
`"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}}`
|
`"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}}`
|
||||||
digest := delivery.ManifestDigest([]byte(man))
|
digest := delivery.ManifestDigest([]byte(man))
|
||||||
mraw, _ := delivery.EncodeManifest(delivery.Manifest{Module: "web", JSON: man})
|
mraw, _ := delivery.EncodeManifest(delivery.Manifest{Module: "web", JSON: man})
|
||||||
@@ -586,7 +602,7 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) {
|
|||||||
fp := fpIt("a")
|
fp := fpIt("a")
|
||||||
modv, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/web", Commit: strings.Repeat("1", 40)})
|
modv, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/web", Commit: strings.Repeat("1", 40)})
|
||||||
place, _ := delivery.EncodePlacement(delivery.Placement{Node: itNode, Modules: []delivery.PlacedModule{{Module: "web"}}})
|
place, _ := delivery.EncodePlacement(delivery.Placement{Node: itNode, Modules: []delivery.PlacedModule{{Module: "web"}}})
|
||||||
rec, _ := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: itNode, Capabilities: []string{"service-manager"}}})
|
rec, _ := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: itNode, Capabilities: []string{"service-manager", "package-manager"}}})
|
||||||
// it1 in one key per module and node (research 072 row 37), it2 in the form before: a node reads both.
|
// it1 in one key per module and node (research 072 row 37), it2 in the form before: a node reads both.
|
||||||
pkey, _ := delivery.PlacementNodeKey(itNode)
|
pkey, _ := delivery.PlacementNodeKey(itNode)
|
||||||
places, _ := delivery.PlacementValues(delivery.Placement{Node: itNode, Modules: []delivery.PlacedModule{{Module: "web"}}})
|
places, _ := delivery.PlacementValues(delivery.Placement{Node: itNode, Modules: []delivery.PlacedModule{{Module: "web"}}})
|
||||||
@@ -659,7 +675,7 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return stop, h8state{placement: placement, resolved: resolved, candidates: candidates, hkey: hkey, place: place, fp: fp}
|
return stop, h8state{placement: placement, resolved: resolved, candidates: candidates, packages: packages, hkey: hkey, place: place, fp: fp}
|
||||||
}
|
}
|
||||||
|
|
||||||
// h8rest is H8 once the node composes its own: the have, a config revision, and the candidate answered "same".
|
// h8rest is H8 once the node composes its own: the have, a config revision, and the candidate answered "same".
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module github.com/novox/mesh-host
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101151945-b2a27ed5cec4
|
||||||
github.com/nats-io/nats-server/v2 v2.11.17
|
github.com/nats-io/nats-server/v2 v2.11.17
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
golang.org/x/crypto v0.57.0
|
golang.org/x/crypto v0.57.0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915 h1:x8k7IeMirMTxUo8BH+xUO+MDnHYgU+636jl1NIw1wvs=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101151945-b2a27ed5cec4 h1:xeuMBwOHNKrVUdGVg7Bdv5tkdueBvngwJFyk6n10rp0=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101151945-b2a27ed5cec4/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package link
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---- the packages this node's compositions declare, in the bus's own storage (novox/hq ADR 0565 §6) ---------------
|
||||||
|
|
||||||
|
// PackagesBucket is the node-engine's declared state packages, created by the grants part from the engine's manifest as
|
||||||
|
// its states health and data are: one key, the node's name, written by that node's engine alone, read by pacman as
|
||||||
|
// mesh-host.packages (delivery.EnginePackagesRead).
|
||||||
|
const PackagesBucket = "mesh-host_" + delivery.EnginePackagesState
|
||||||
|
|
||||||
|
// MirrorPackagesBucket is the controller's declared-packages mirror (ADR 0507 §5), which the engine reads its own node's
|
||||||
|
// key of until the lease write, to compare (the node seat's verb packages).
|
||||||
|
const MirrorPackagesBucket = "mesh-controller_declared-packages"
|
||||||
|
|
||||||
|
// packagesStore binds PackagesBucket over the link open now: bound, never created.
|
||||||
|
func (l *natsLink) packagesStore() (nats.KeyValue, error) {
|
||||||
|
kv, err := l.js.KeyValue(PackagesBucket)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("the node-engine's state %s cannot be bound (it is created from the node-engine's "+
|
||||||
|
"manifest; written again at the next composition that changes, or by the node seat's packages with write): %w",
|
||||||
|
PackagesBucket, err)
|
||||||
|
}
|
||||||
|
return kv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// packagesStore binds PackagesBucket over a link made of a connection the caller holds (OverNATS), as natsLink does.
|
||||||
|
func (b OverNATS) packagesStore() (nats.KeyValue, error) {
|
||||||
|
kv, err := b.JS.KeyValue(PackagesBucket)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("the node-engine's state %s cannot be bound: %w", PackagesBucket, err)
|
||||||
|
}
|
||||||
|
return kv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnginePackages is this node's key of PackagesBucket over whichever link the session has open
|
||||||
|
// (selfcompose.PackagesStore); with none open every read and write is refused, and nothing is kept anywhere else.
|
||||||
|
type EnginePackages struct{ Session *Session }
|
||||||
|
|
||||||
|
func (p EnginePackages) kv() (nats.KeyValue, error) {
|
||||||
|
p.Session.init()
|
||||||
|
p.Session.mu.Lock()
|
||||||
|
bus := p.Session.bus
|
||||||
|
p.Session.mu.Unlock()
|
||||||
|
l, ok := bus.(interface{ packagesStore() (nats.KeyValue, error) })
|
||||||
|
if bus == nil || !ok {
|
||||||
|
return nil, ErrNoLink
|
||||||
|
}
|
||||||
|
return l.packagesStore()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get is the key's value, nil when it has none.
|
||||||
|
func (p EnginePackages) Get(_ context.Context, key string) ([]byte, error) {
|
||||||
|
kv, err := p.kv()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
e, err := kv.Get(key)
|
||||||
|
if errors.Is(err, nats.ErrKeyNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return e.Value(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put writes the key.
|
||||||
|
func (p EnginePackages) Put(_ context.Context, key string, value []byte) error {
|
||||||
|
kv, err := p.kv()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = kv.Put(key, value)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackagesView is the node seat's verb packages' answer (ADR 0565 §6): this node's key of the engine's state packages,
|
||||||
|
// and until the lease write the controller's mirror's key for the node and every package they hold differently.
|
||||||
|
type PackagesView struct {
|
||||||
|
Node string `json:"node"`
|
||||||
|
// Key is the node's key as the bus holds it (Source); null when the engine has written none.
|
||||||
|
Source string `json:"source"`
|
||||||
|
Key *delivery.DeclaredPackages `json:"key"`
|
||||||
|
KeyError string `json:"key_error,omitempty"`
|
||||||
|
// Mirror is the controller's mirror's key for the node (MirrorSource); null when it holds none or cannot be read.
|
||||||
|
MirrorSource string `json:"mirror_source"`
|
||||||
|
Mirror *delivery.DeclaredPackages `json:"mirror"`
|
||||||
|
MirrorError string `json:"mirror_error,omitempty"`
|
||||||
|
// Differences is every package the two hold differently, read only when both are; Agree when there is none.
|
||||||
|
Differences []delivery.PackageDifference `json:"differences"`
|
||||||
|
Agree bool `json:"agree"`
|
||||||
|
// Wrote says, for write, whether the key was written again (false: it already held what the engine composed).
|
||||||
|
Wrote *bool `json:"wrote,omitempty"`
|
||||||
|
Note string `json:"note,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackagesAnswer is the verb's answer from the two keys as read (nil: no key), each with its read's error.
|
||||||
|
func PackagesAnswer(node string, key, mirror []byte, keyErr, mirrorErr error) PackagesView {
|
||||||
|
v := PackagesView{Node: node, Source: delivery.EnginePackagesRead, MirrorSource: "mesh-controller.declared-packages",
|
||||||
|
Differences: []delivery.PackageDifference{}}
|
||||||
|
read := func(raw []byte, err error, into **delivery.DeclaredPackages, said *string) {
|
||||||
|
if err == nil && raw != nil {
|
||||||
|
var d delivery.DeclaredPackages
|
||||||
|
if err = json.Unmarshal(raw, &d); err == nil {
|
||||||
|
*into = &d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
*said = err.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read(key, keyErr, &v.Key, &v.KeyError)
|
||||||
|
read(mirror, mirrorErr, &v.Mirror, &v.MirrorError)
|
||||||
|
switch {
|
||||||
|
case v.Key != nil && v.Mirror != nil:
|
||||||
|
v.Differences = delivery.DiffDeclaredPackages(*v.Key, *v.Mirror)
|
||||||
|
v.Agree = len(v.Differences) == 0
|
||||||
|
case v.Key == nil && v.KeyError == "":
|
||||||
|
v.Note = "the engine has written no key for this node yet: pacman reads the controller's mirror for it"
|
||||||
|
case v.Mirror == nil && v.MirrorError == "":
|
||||||
|
v.Note = "the controller's mirror holds no key for this node: nothing to compare"
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
@@ -42,6 +42,9 @@ const (
|
|||||||
ActComposition = "composition"
|
ActComposition = "composition"
|
||||||
// ActCompose composes the node now, the recovery of a failed composition (issue 774).
|
// ActCompose composes the node now, the recovery of a failed composition (issue 774).
|
||||||
ActCompose = "compose"
|
ActCompose = "compose"
|
||||||
|
// ActPackages reads the packages this node's compositions declare, and with write writes its key again (novox/hq
|
||||||
|
// ADR 0565 §6, §15).
|
||||||
|
ActPackages = "packages"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The states of an act.
|
// The states of an act.
|
||||||
@@ -137,6 +140,9 @@ type HandActs struct {
|
|||||||
// Compose makes a pass of the node's composition now and says how it went; with module, its apply is also tried
|
// Compose makes a pass of the node's composition now and says how it went; with module, its apply is also tried
|
||||||
// again (the verb compose, issue 774).
|
// again (the verb compose, issue 774).
|
||||||
Compose func(module string) (any, error)
|
Compose func(module string) (any, error)
|
||||||
|
// Packages answers the node's key of the engine's state packages beside the controller's mirror's key and their
|
||||||
|
// difference, and with write writes the key again from what the engine composed (the verb packages, ADR 0565 §6, §15).
|
||||||
|
Packages func(write bool) (any, error)
|
||||||
// Say says one of the seat's events, asked, done or refused; nil says nothing.
|
// Say says one of the seat's events, asked, done or refused; nil says nothing.
|
||||||
Say func(event string, body any)
|
Say func(event string, body any)
|
||||||
Now func() time.Time
|
Now func() time.Time
|
||||||
@@ -200,6 +206,15 @@ func (h *HandActs) Call(c EngineCall) (any, error) {
|
|||||||
return nil, errors.New("this engine composes nothing on a call yet: its node agent has not started")
|
return nil, errors.New("this engine composes nothing on a call yet: its node agent has not started")
|
||||||
}
|
}
|
||||||
return h.Compose(arg("module"))
|
return h.Compose(arg("module"))
|
||||||
|
case ActPackages:
|
||||||
|
if h.Packages == nil {
|
||||||
|
return nil, errors.New("this engine keeps no packages yet: its node agent has not started")
|
||||||
|
}
|
||||||
|
write := arg("write") == "true"
|
||||||
|
if b, ok := c.Args["write"].(bool); ok {
|
||||||
|
write = b
|
||||||
|
}
|
||||||
|
return h.Packages(write)
|
||||||
case "acts":
|
case "acts":
|
||||||
if id := arg("id"); id != "" {
|
if id := arg("id"); id != "" {
|
||||||
return h.Act(id)
|
return h.Act(id)
|
||||||
|
|||||||
@@ -240,7 +240,20 @@ func TestALinkThatHearsHandOversIsGivenTheQueues(t *testing.T) {
|
|||||||
Composition: func(module, digest string, diff bool) (any, error) {
|
Composition: func(module, digest string, diff bool) (any, error) {
|
||||||
return map[string]any{"module": module, "digest": digest, "diff": diff}, nil
|
return map[string]any{"module": module, "digest": digest, "diff": diff}, nil
|
||||||
},
|
},
|
||||||
Compose: func(module string) (any, error) { return map[string]any{"composed": true, "retried": module}, nil }}
|
Compose: func(module string) (any, error) { return map[string]any{"composed": true, "retried": module}, nil },
|
||||||
|
// ADR 0565 §6, §15: the node's key beside the controller's mirror's, the mirror's from its writer's test.
|
||||||
|
Packages: func(write bool) (any, error) {
|
||||||
|
key := []byte(`{"node":"n1","packages":[{"package":"curl","modules":["backup","web"],"absent":false}]}`)
|
||||||
|
mirror := []byte(`{"node":"n1","packages":[{"package":"curl","modules":["backup","web"],"absent":false},` +
|
||||||
|
`{"package":"nano","modules":["web"],"absent":true}]}`)
|
||||||
|
if !write {
|
||||||
|
return PackagesAnswer("n1", key, mirror, nil, nil), nil
|
||||||
|
}
|
||||||
|
v := PackagesAnswer("n1", nil, nil, nil, errors.New("not granted"))
|
||||||
|
wrote := true
|
||||||
|
v.Wrote = &wrote
|
||||||
|
return v, nil
|
||||||
|
}}
|
||||||
for _, row := range []struct{ verb, subject, want string }{
|
for _, row := range []struct{ verb, subject, want string }{
|
||||||
{"judge-directories", "mesh.seat.node-engine.call.judge-directories.n1.shanks~node-tools", `"said":"judged for shanks.node-tools"`},
|
{"judge-directories", "mesh.seat.node-engine.call.judge-directories.n1.shanks~node-tools", `"said":"judged for shanks.node-tools"`},
|
||||||
{"judge-directories", EngineToolSubject("judge-directories", "n1"), `"said":"judged for unproven"`},
|
{"judge-directories", EngineToolSubject("judge-directories", "n1"), `"said":"judged for unproven"`},
|
||||||
@@ -252,6 +265,12 @@ func TestALinkThatHearsHandOversIsGivenTheQueues(t *testing.T) {
|
|||||||
// The recovery verb compose (issue 774) makes a pass now, with a module's apply tried again when named.
|
// The recovery verb compose (issue 774) makes a pass now, with a module's apply tried again when named.
|
||||||
{"compose", EngineToolSubject("compose", "n1"), `"composed":true,"retried":""`},
|
{"compose", EngineToolSubject("compose", "n1"), `"composed":true,"retried":""`},
|
||||||
{"compose", EngineToolSubject("compose", "n1") + " " + `{"module":"route-proxy"}`, `"retried":"route-proxy"`},
|
{"compose", EngineToolSubject("compose", "n1") + " " + `{"module":"route-proxy"}`, `"retried":"route-proxy"`},
|
||||||
|
// The read verb packages (ADR 0565 §6) names both sources and their difference; with write, what it wrote, and a
|
||||||
|
// key the engine has not written, or a mirror it may not read, is said and not an error.
|
||||||
|
{"packages", EngineToolSubject("packages", "n1"), `"differences":[{"package":"nano","absent":true,"engine":[],"mirror":["web"]}],"agree":false`},
|
||||||
|
{"packages", EngineToolSubject("packages", "n1"), `"source":"mesh-host.packages","key":{"node":"n1"`},
|
||||||
|
{"packages", EngineToolSubject("packages", "n1") + " " + `{"write":true}`, `"mirror":null,"mirror_error":"not granted","differences":[],"agree":false,"wrote":true`},
|
||||||
|
{"packages", EngineToolSubject("packages", "n1") + " " + `{"write":"true"}`, `"note":"the engine has written no key for this node yet`},
|
||||||
{"unserved", "mesh.seat.node-engine.call.unserved.n1.x", `"error":"the node seat node-engine serves no verb \"unserved\""`},
|
{"unserved", "mesh.seat.node-engine.call.unserved.n1.x", `"error":"the node seat node-engine serves no verb \"unserved\""`},
|
||||||
} {
|
} {
|
||||||
subject, body, ok := strings.Cut(row.subject, " ")
|
subject, body, ok := strings.Cut(row.subject, " ")
|
||||||
|
|||||||
@@ -177,6 +177,56 @@ func TestTheEngineKeepsEachPlacedModulesDataItems(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// ADR 0565 §6 (1953): the node's key of the engine's state packages is written after a pass that handed something new
|
||||||
|
// when the packages its compositions declare differ, and only then: a composition adding a package writes it once,
|
||||||
|
// one that changes no package writes nothing; a write refused is owed after the growing bound; the verb's write reads
|
||||||
|
// the key again, so a key lost on the bus is written back. Another node's key is never touched.
|
||||||
|
pkgStore := &fakeData{kv: map[string][]byte{"n2": []byte(`{"node":"n2","packages":[]}`)}}
|
||||||
|
pc := New(Env{Node: "n1", Packages: pkgStore})
|
||||||
|
web := func(pkgs ...string) delivery.Composed {
|
||||||
|
comp := delivery.Composed{Node: "n1", Module: "web"}
|
||||||
|
for _, p := range pkgs {
|
||||||
|
comp.Resources = append(comp.Resources, []byte(`{"id":"`+p+`","type":"package","package":"`+p+`"}`))
|
||||||
|
}
|
||||||
|
comp.Resources = append(comp.Resources, []byte(`{"id":"conf","type":"file","path":"/etc/web.conf"}`))
|
||||||
|
return packagesOf(comp)
|
||||||
|
}
|
||||||
|
for _, step := range []struct {
|
||||||
|
name string
|
||||||
|
comp delivery.Composed
|
||||||
|
fail error
|
||||||
|
puts int
|
||||||
|
owed bool
|
||||||
|
holds string
|
||||||
|
}{
|
||||||
|
{name: "the run's first pass writes the key", comp: web("curl"), puts: 1,
|
||||||
|
holds: `{"node":"n1","packages":[{"package":"curl","modules":["web"],"absent":false}]}`},
|
||||||
|
{name: "a pass that changes no package writes nothing", comp: web("curl"), puts: 1},
|
||||||
|
{name: "a refused write is owed after the bound", comp: web("curl", "jq"), fail: errors.New("no grant yet"), puts: 1, owed: true},
|
||||||
|
{name: "a composition adding a package writes the key once", comp: web("curl", "jq"), puts: 2,
|
||||||
|
holds: `{"node":"n1","packages":[{"package":"curl","modules":["web"],"absent":false},{"package":"jq","modules":["web"],"absent":false}]}`},
|
||||||
|
} {
|
||||||
|
pkgStore.mu.Lock()
|
||||||
|
pkgStore.fail = step.fail
|
||||||
|
pkgStore.mu.Unlock()
|
||||||
|
pc.paid()
|
||||||
|
pc.mu.Lock()
|
||||||
|
pc.pkgs, pc.pkgsComposed, pc.pkgsDue = map[string]delivery.Composed{webKey: step.comp}, true, true
|
||||||
|
pc.mu.Unlock()
|
||||||
|
pc.publishPackages(context.Background())
|
||||||
|
if len(pkgStore.puts) != step.puts || pc.owed.Load() != step.owed ||
|
||||||
|
(step.holds != "" && string(pkgStore.kv["n1"]) != step.holds) || string(pkgStore.kv["n2"]) != `{"node":"n2","packages":[]}` {
|
||||||
|
t.Errorf("%s: %d writes, owed %v, n1 holds %s", step.name, len(pkgStore.puts), pc.owed.Load(), pkgStore.kv["n1"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(pkgStore.kv, "n1")
|
||||||
|
if wrote, err := pc.WritePackages(context.Background()); !wrote || err != nil || pkgStore.kv["n1"] == nil {
|
||||||
|
t.Errorf("the verb's write of a lost key wrote %v (%v)", wrote, err)
|
||||||
|
}
|
||||||
|
if wrote, _ := pc.WritePackages(context.Background()); wrote {
|
||||||
|
t.Error("the verb's write of a key holding what was composed wrote it again")
|
||||||
|
}
|
||||||
|
|
||||||
// The items are worked out from what the pass read: a done build is read over the bus once, not again for the data
|
// The items are worked out from what the pass read: a done build is read over the bus once, not again for the data
|
||||||
// items after the pass (a module in a running build is read again, since it may land).
|
// items after the pass (a module in a running build is read again, since it may land).
|
||||||
b := newFakeBus()
|
b := newFakeBus()
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package selfcompose
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
|
)
|
||||||
|
|
||||||
|
// **The packages this node's compositions declare, kept by its engine** (novox/hq ADR 0565 §6, §15; issue 1953). The
|
||||||
|
// composer keeps the package resources of each composition it handed, and after a pass that handed something new (or
|
||||||
|
// the run's first) it works out the node's key of the engine's state packages from them (delivery.DeclaredPackagesOf,
|
||||||
|
// the controller mirror's layout and order) and writes it only when the value differs from what the key holds (ADR 0289
|
||||||
|
// §1): no timer, and no composing of another node. A module held keeps the packages of what was handed of it, as it keeps
|
||||||
|
// running what it runs. pacman reads the key as mesh-host.packages; the node seat's verb packages answers it beside the
|
||||||
|
// controller's mirror until the lease write, and with write writes it again (ADR 0565 §15).
|
||||||
|
|
||||||
|
// PackagesStore is this node's key of the engine's state packages (link.EnginePackages).
|
||||||
|
type PackagesStore interface {
|
||||||
|
// Get is the key's value, nil when it has none.
|
||||||
|
Get(ctx context.Context, key string) ([]byte, error)
|
||||||
|
Put(ctx context.Context, key string, value []byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// packagesOf is a composition's package resources alone, with its module: what the node's key is worked out from.
|
||||||
|
func packagesOf(comp delivery.Composed) delivery.Composed {
|
||||||
|
out := delivery.Composed{Node: comp.Node, Module: comp.Module}
|
||||||
|
for _, raw := range comp.Resources {
|
||||||
|
var head struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(raw, &head) == nil && head.Type == "package" {
|
||||||
|
out.Resources = append(out.Resources, raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeclaredPackages is the node's key as this composer would write it now, from what it handed.
|
||||||
|
func (c *Composer) DeclaredPackages() (delivery.DeclaredPackages, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
keys := make([]string, 0, len(c.pkgs))
|
||||||
|
for k := range c.pkgs {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
comps := make([]delivery.Composed, 0, len(keys))
|
||||||
|
for _, k := range keys {
|
||||||
|
comps = append(comps, c.pkgs[k])
|
||||||
|
}
|
||||||
|
composed := c.pkgsComposed
|
||||||
|
c.mu.Unlock()
|
||||||
|
if !composed {
|
||||||
|
return delivery.DeclaredPackages{}, errors.New("this node's composer has handed no composition yet in this run, so it " +
|
||||||
|
"declares no packages yet")
|
||||||
|
}
|
||||||
|
return delivery.DeclaredPackagesOf(c.env.Node, comps)
|
||||||
|
}
|
||||||
|
|
||||||
|
// publishPackages writes the node's key of the state packages when a pass asked it (pkgsDue) and the value differs.
|
||||||
|
func (c *Composer) publishPackages(ctx context.Context) {
|
||||||
|
c.mu.Lock()
|
||||||
|
due := c.pkgsDue
|
||||||
|
c.pkgsDue = false
|
||||||
|
c.mu.Unlock()
|
||||||
|
if !due || c.env.Packages == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wrote, err := c.writePackages(ctx, false)
|
||||||
|
if err != nil {
|
||||||
|
c.sayPackages("this node's packages are not kept in " + enginePackagesBucket + "; " + c.owePackages() + ": " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.mu.Lock()
|
||||||
|
c.pkgsTries = 0
|
||||||
|
c.mu.Unlock()
|
||||||
|
c.sayPackages("")
|
||||||
|
if wrote {
|
||||||
|
c.env.Say("this node's packages: its key of " + enginePackagesBucket + " written")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// enginePackagesBucket is the engine's state packages as the bus names it, for what the composer says.
|
||||||
|
const enginePackagesBucket = "mesh-host_" + delivery.EnginePackagesState
|
||||||
|
|
||||||
|
// WritePackages writes the node's key again from what the composer handed, when it differs from what the bus holds
|
||||||
|
// now (the node seat's verb packages with write, ADR 0565 §15): the key is read again rather than trusted, so a key lost
|
||||||
|
// or changed on the bus is written back. It answers whether it wrote.
|
||||||
|
func (c *Composer) WritePackages(ctx context.Context) (bool, error) {
|
||||||
|
if c.env.Packages == nil {
|
||||||
|
return false, errors.New("this engine keeps no packages key")
|
||||||
|
}
|
||||||
|
return c.writePackages(ctx, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writePackages writes the key when the value worked out differs from what it holds: read once per run (pkgsKept), or
|
||||||
|
// again when fresh says so.
|
||||||
|
func (c *Composer) writePackages(ctx context.Context, fresh bool) (bool, error) {
|
||||||
|
v, err := c.DeclaredPackages()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
raw, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
c.pkgsWrite.Lock()
|
||||||
|
defer c.pkgsWrite.Unlock()
|
||||||
|
if fresh || !c.pkgsRead {
|
||||||
|
kept, err := c.env.Packages.Get(ctx, c.env.Node)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("%s cannot be read: %w", c.env.Node, err)
|
||||||
|
}
|
||||||
|
c.pkgsKept, c.pkgsRead = kept, true
|
||||||
|
}
|
||||||
|
if bytes.Equal(c.pkgsKept, raw) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if err := c.env.Packages.Put(ctx, c.env.Node, raw); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
c.pkgsKept = raw
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// owePackages keeps the packages due and has the loop make a pass again after the growing bound, as oweData does for the
|
||||||
|
// data items: the first write after a delivery may be refused while the state or its grant is not on the bus yet.
|
||||||
|
func (c *Composer) owePackages() string {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.pkgsDue = true
|
||||||
|
c.wait = LaterWait(c.pkgsTries)
|
||||||
|
c.pkgsTries++
|
||||||
|
c.owed.Store(true)
|
||||||
|
return fmt.Sprintf("tried again at %s (try %d)", time.Now().Add(c.wait).Format("15:04:05"), c.pkgsTries)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sayPackages says a line about the packages once until it changes; "" clears it.
|
||||||
|
func (c *Composer) sayPackages(line string) {
|
||||||
|
c.mu.Lock()
|
||||||
|
same := c.pkgsSaid == line
|
||||||
|
c.pkgsSaid = line
|
||||||
|
c.mu.Unlock()
|
||||||
|
if !same && line != "" {
|
||||||
|
c.env.Say(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -90,6 +90,9 @@ type Env struct {
|
|||||||
// Data is this node's keys of the engine's state data (novox/hq ADR 0577 clause 1), where each placed module's data
|
// Data is this node's keys of the engine's state data (novox/hq ADR 0577 clause 1), where each placed module's data
|
||||||
// items are kept after a pass that composed something new. Nil keeps none.
|
// items are kept after a pass that composed something new. Nil keeps none.
|
||||||
Data DataStore
|
Data DataStore
|
||||||
|
// Packages is this node's key of the engine's state packages (novox/hq ADR 0565 §6), written after a pass that
|
||||||
|
// handed something new when the packages its compositions declare differ. Nil keeps none.
|
||||||
|
Packages PackagesStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// VaultSecretChanged is the vault's event that a secret changed: a name and its generation, never a value.
|
// VaultSecretChanged is the vault's event that a secret changed: a name and its generation, never a value.
|
||||||
@@ -161,6 +164,17 @@ type Composer struct {
|
|||||||
dataSaid string
|
dataSaid string
|
||||||
dataScope frombus.Scope
|
dataScope frombus.Scope
|
||||||
dataKept map[string][]byte
|
dataKept map[string][]byte
|
||||||
|
// pkgs is the package resources of each composition handed, by its composed key, and pkgsComposed whether this run
|
||||||
|
// handed any (ADR 0565 §6); pkgsDue, pkgsTries and pkgsSaid are as the data items' are. Under mu. pkgsKept is what
|
||||||
|
// the node's key of the state packages held when last read or written (pkgsRead), under pkgsWrite.
|
||||||
|
pkgs map[string]delivery.Composed
|
||||||
|
pkgsComposed bool
|
||||||
|
pkgsDue bool
|
||||||
|
pkgsTries int
|
||||||
|
pkgsSaid string
|
||||||
|
pkgsWrite sync.Mutex
|
||||||
|
pkgsKept []byte
|
||||||
|
pkgsRead bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass is how one pass went, as the node-engine's verb compose answers it.
|
// Pass is how one pass went, as the node-engine's verb compose answers it.
|
||||||
@@ -241,7 +255,7 @@ func New(env Env) *Composer {
|
|||||||
return &Composer{env: env, wants: map[string]delivery.Want{}, placements: map[string]delivery.Placement{}, placementKeys: delivery.NewPlacementKeys(),
|
return &Composer{env: env, wants: map[string]delivery.Want{}, placements: map[string]delivery.Placement{}, placementKeys: delivery.NewPlacementKeys(),
|
||||||
records: map[string]delivery.NodeRecord{}, inOwn: map[string]bool{}, modules: map[string]delivery.Module{}, values: map[string]map[string]any{},
|
records: map[string]delivery.NodeRecord{}, inOwn: map[string]bool{}, modules: map[string]delivery.Module{}, values: map[string]map[string]any{},
|
||||||
manifests: map[string][]byte{}, archives: map[string][]byte{}, builds: map[string]delivery.BuilderBuild{}, handed: map[string]string{}, sealedOwn: map[string]ownSealed{},
|
manifests: map[string][]byte{}, archives: map[string][]byte{}, builds: map[string]delivery.BuilderBuild{}, handed: map[string]string{}, sealedOwn: map[string]ownSealed{},
|
||||||
asked: make(chan chan Pass, 8)}
|
pkgs: map[string]delivery.Composed{}, asked: make(chan chan Pass, 8)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ComposeNow asks the composer for a pass now (the node-engine's verb compose, issue 774): the growing bound after a seat
|
// ComposeNow asks the composer for a pass now (the node-engine's verb compose, issue 774): the growing bound after a seat
|
||||||
@@ -453,6 +467,7 @@ func (c *Composer) Watch(ctx context.Context, _ string) (<-chan agent.Update, er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.publishData(ctx)
|
c.publishData(ctx)
|
||||||
|
c.publishPackages(ctx)
|
||||||
if c.owed.Load() {
|
if c.owed.Load() {
|
||||||
wait, _ := c.waitNow()
|
wait, _ := c.waitNow()
|
||||||
c.env.Say(fmt.Sprintf("the node composes again in %s: a read on the bus did not answer", wait))
|
c.env.Say(fmt.Sprintf("the node composes again in %s: a read on the bus did not answer", wait))
|
||||||
@@ -903,6 +918,7 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
|
|||||||
}
|
}
|
||||||
c.rev++
|
c.rev++
|
||||||
c.handed[key] = dig
|
c.handed[key] = dig
|
||||||
|
c.pkgs[key] = packagesOf(comp)
|
||||||
out = append(out, agent.Update{Key: key, Value: raw, Revision: c.rev})
|
out = append(out, agent.Update{Key: key, Value: raw, Revision: c.rev})
|
||||||
}
|
}
|
||||||
for key := range c.handed {
|
for key := range c.handed {
|
||||||
@@ -914,12 +930,16 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
|
|||||||
if !now[key] {
|
if !now[key] {
|
||||||
c.rev++
|
c.rev++
|
||||||
delete(c.handed, key)
|
delete(c.handed, key)
|
||||||
|
delete(c.pkgs, key)
|
||||||
out = append(out, agent.Update{Key: key, Deleted: true, Revision: c.rev})
|
out = append(out, agent.Update{Key: key, Deleted: true, Revision: c.rev})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pass.Handed = len(out)
|
pass.Handed = len(out)
|
||||||
// The data items are kept again after a pass that handed something new, and after the run's first (ADR 0577 clause 1).
|
// The data items are kept again after a pass that handed something new, and after the run's first (ADR 0577 clause 1).
|
||||||
c.dataDue = c.dataDue || len(out) > 0 || c.dataKept == nil
|
c.dataDue = c.dataDue || len(out) > 0 || c.dataKept == nil
|
||||||
|
// And the node's packages after the same passes (ADR 0565 §6): written only when they differ.
|
||||||
|
c.pkgsDue = c.pkgsDue || len(out) > 0 || !c.pkgsComposed
|
||||||
|
c.pkgsComposed = true
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -393,7 +393,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
proxy := `{"module":"proxy","version":"1","capabilities":["systemd"],"provides":[{"name":"route","scope":"mesh"},{"name":"smtp","scope":"mesh"}],` +
|
proxy := `{"module":"proxy","version":"1","capabilities":["systemd"],"provides":[{"name":"route","scope":"mesh"},{"name":"smtp","scope":"mesh"}],` +
|
||||||
`"resources":[{"id":"service","type":"service","unit":"proxy.service","state":"running","boot":"enabled"}]}`
|
`"resources":[{"id":"service","type":"service","unit":"proxy.service","state":"running","boot":"enabled"},` +
|
||||||
|
`{"id":"pkg","type":"package","package":"haproxy"}]}`
|
||||||
c3, told := fresh(env.Get)
|
c3, told := fresh(env.Get)
|
||||||
for m, raw := range map[string]string{"de-spiegel": string(spiegel), "proxy": proxy} {
|
for m, raw := range map[string]string{"de-spiegel": string(spiegel), "proxy": proxy} {
|
||||||
dig := delivery.ManifestDigest([]byte(raw))
|
dig := delivery.ManifestDigest([]byte(raw))
|
||||||
@@ -430,6 +431,12 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
|||||||
if u, ok := handed3[spiegelKey]; ok {
|
if u, ok := handed3[spiegelKey]; ok {
|
||||||
t.Errorf("held de-spiegel is handed %+v, want what was handed of it kept", u)
|
t.Errorf("held de-spiegel is handed %+v, want what was handed of it kept", u)
|
||||||
}
|
}
|
||||||
|
// ADR 0565 §6 (1953): the pass keeps the packages each handed composition declares, with their module, for the node's
|
||||||
|
// key of the engine's state packages.
|
||||||
|
if d, err := c3.DeclaredPackages(); err != nil || len(d.Packages) != 1 || d.Packages[0].Package != "haproxy" ||
|
||||||
|
strings.Join(d.Packages[0].Modules, ",") != "proxy" || d.Node != "n1" {
|
||||||
|
t.Errorf("the node's packages after the pass are %+v (%v)", d, err)
|
||||||
|
}
|
||||||
if len(*told) != 1 || !strings.HasPrefix((*told)[0], "held de-spiegel: ") || !strings.Contains((*told)[0], "${config:praktijk-name}") ||
|
if len(*told) != 1 || !strings.HasPrefix((*told)[0], "held de-spiegel: ") || !strings.Contains((*told)[0], "${config:praktijk-name}") ||
|
||||||
!strings.Contains((*told)[0], "without what it contributes to route") || strings.Contains((*told)[0], "held proxy") ||
|
!strings.Contains((*told)[0], "without what it contributes to route") || strings.Contains((*told)[0], "held proxy") ||
|
||||||
strings.Contains((*told)[0], "held lemurs") {
|
strings.Contains((*told)[0], "held lemurs") {
|
||||||
|
|||||||
@@ -23,6 +23,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "data"
|
"name": "data"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "packages"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"config": {
|
"config": {
|
||||||
|
|||||||
@@ -79,8 +79,10 @@ func TestTheManifestConsumesTheBackupEvent(t *testing.T) {
|
|||||||
// written again (no TTL: a node that stops writing keeps its last word, which the watchman reads with its time).
|
// written again (no TTL: a node that stops writing keeps its last word, which the watchman reads with its time).
|
||||||
// ADR 0577 clause 1: and the state data, one key per module placed on the node that declares data items, kept until
|
// ADR 0577 clause 1: and the state data, one key per module placed on the node that declares data items, kept until
|
||||||
// the module leaves the node; created by the grants part from this manifest as health is.
|
// the module leaves the node; created by the grants part from this manifest as health is.
|
||||||
if len(parsed.State) != 2 || parsed.State[0].Name != "health" || parsed.State[0].TTLSeconds != 0 ||
|
// ADR 0565 clause 6: and the state packages, one key per node, its compositions' declared packages, read by pacman.
|
||||||
parsed.State[1].Name != "data" || parsed.State[1].TTLSeconds != 0 {
|
if len(parsed.State) != 3 || parsed.State[0].Name != "health" || parsed.State[0].TTLSeconds != 0 ||
|
||||||
|
parsed.State[1].Name != "data" || parsed.State[1].TTLSeconds != 0 || parsed.State[2].Name != "packages" ||
|
||||||
|
parsed.State[2].TTLSeconds != 0 {
|
||||||
t.Fatalf("the manifest declares the state %+v", parsed.State)
|
t.Fatalf("the manifest declares the state %+v", parsed.State)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user