A-316, A-317 (assigner): the passer writes each candidate with the version and parts of the modules it adds; the propose verb writes the proposal alone
Builds: design 47 G §379, §421, A-316, A-317 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
mwc "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshwantsclient"
|
||||
plc "mesh-delivery-assigner/internal/assignment"
|
||||
"mesh-delivery-assigner/internal/buckets"
|
||||
)
|
||||
|
||||
// addedOf is the `added` of node's candidate (design 47 G A-316, §379, §421): by module, each module the candidate adds to node
|
||||
// (laid over current) with no want there, the version and parts the want it would write carries, taken from the same code that
|
||||
// writes a want's (namedBuild, the want's build at the module's trunk fingerprint). A module whose build names none is not listed.
|
||||
func (p *passer) addedOf(node string, laid *mma.Assignments, current map[string]mma.Assignments) map[string]mma.AddedModule {
|
||||
if laid == nil {
|
||||
return nil
|
||||
}
|
||||
trunk := p.w.trunk()
|
||||
var out map[string]mma.AddedModule
|
||||
for _, module := range plc.ChangeOf(current[node], *laid).Added {
|
||||
if key, err := mwc.WantKey(node, module); err != nil || p.w.has(buckets.Wants(), key) {
|
||||
continue
|
||||
}
|
||||
fp := trunk[module].Fp2
|
||||
if fp == "" {
|
||||
continue
|
||||
}
|
||||
k, ok := p.namedBuild(module, fp)(fp)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if out == nil {
|
||||
out = map[string]mma.AddedModule{}
|
||||
}
|
||||
out[module] = mma.AddedModule{Version: k.Version, Parts: k.Parts}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// candidateOf is the candidate of proposal id for node: the assignments laid over it, and what it adds (addedOf).
|
||||
func (p *passer) candidateOf(id, node string, laid *mma.Assignments, current map[string]mma.Assignments) mma.CandidatesByProposalNodeValue {
|
||||
return mma.CandidatesByProposalNodeValue{Proposal: id, Assignments: laid, Added: p.addedOf(node, laid, current)}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/graph"
|
||||
"mesh-delivery-assigner/internal/buckets"
|
||||
)
|
||||
|
||||
// Design 47 G A-316: the candidate of a node carries, for each module it adds there without a want, the version and parts the
|
||||
// want it would write carries; a module with a want there, or a build that names none, is not listed.
|
||||
func TestACandidateThatAddsAModuleCarriesItsParts(t *testing.T) {
|
||||
w := newWorld()
|
||||
for _, m := range []string{"mail", "web", "db"} {
|
||||
w.putModule("main", m, meshcatalog.ModulesByModuleValue{Fp2: "fp-" + m}, graph.Node{}, nil)
|
||||
}
|
||||
w.first = "main"
|
||||
rel, _ := json.Marshal(meshcatalog.VersionReleasedEvent{Module: "mail", Source: 7, Version: "1.2.3", Artifacts: []meshcatalog.VersionArtifact{{Artifact: "image", Digest: "sha256:9"}}})
|
||||
if err := w.released.put(rel); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
end, _ := json.Marshal(builds.BuildEnded{Module: "db", Fingerprint: "fp-db", Made: map[string]string{"program": "sha256:3"}, KeptOn: "g14"})
|
||||
if err := w.ends.put(end); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.put(buckets.Wants(), "want.ace.web", []byte(`{}`))
|
||||
p := &passer{w: w, graph: &graphKeeper{ok: true, source: 7}}
|
||||
laid := &mma.Assignments{Node: "ace", Modules: []mma.PlacedModule{{Module: "mail"}, {Module: "web"}, {Module: "db"}, {Module: "old"}}}
|
||||
current := map[string]mma.Assignments{"ace": {Node: "ace", Modules: []mma.PlacedModule{{Module: "old"}}}}
|
||||
c := p.candidateOf("p1", "ace", laid, current)
|
||||
want := map[string]mma.AddedModule{
|
||||
"mail": {Version: "1.2.3", Parts: map[string]string{"mail:image": "sha256:9"}},
|
||||
"db": {Parts: map[string]string{"db:program": "sha256:3"}},
|
||||
}
|
||||
if c.Proposal != "p1" || c.Assignments != laid || !reflect.DeepEqual(c.Added, want) {
|
||||
t.Errorf("candidate = %+v, added want %+v", c, want)
|
||||
}
|
||||
if got := p.candidateOf("p1", "ace", nil, current); got.Added != nil {
|
||||
t.Errorf("a candidate taking the key away adds %+v", got.Added)
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,8 @@ type passer struct {
|
||||
// saidWaits is the last said of the seat's wait for its states (sayWaits).
|
||||
saidWaits string
|
||||
intended map[string]map[string]map[string]bool
|
||||
// laidHere are the proposals whose candidates this process wrote (design 47 G A-317), by id.
|
||||
laidHere map[string]bool
|
||||
// reads keeps what each node's engine states it reads, asked through the node-engine seat's verb (ADR 0618 §3); nil reads
|
||||
// each node's record, as the rules did before the verb. online is, by node, whether the last pass saw it online.
|
||||
reads *enginereads.Keeper
|
||||
|
||||
@@ -310,6 +310,13 @@ func (p *passer) stepProposal(ctx context.Context, prop mma.Proposal, rev uint64
|
||||
}
|
||||
cands, candRevs, written := p.candidatesOf(prop.ID)
|
||||
if len(prop.Must) == 0 {
|
||||
for _, node := range plc.Nodes(prop) {
|
||||
if _, ok := cands[node]; !ok && !p.laidHere[prop.ID] {
|
||||
// The verb writes the proposal alone: its candidates have one writer, this process (design 47 G A-317).
|
||||
p.lay(ctx, prop, rev, current, revs, prop.Words, true)
|
||||
return
|
||||
}
|
||||
}
|
||||
p.mustAnswer(ctx, prop, rev, current, cands)
|
||||
return
|
||||
}
|
||||
@@ -437,6 +444,12 @@ func (p *passer) mustAnswer(ctx context.Context, prop mma.Proposal, rev uint64,
|
||||
// layAgain lays a proposal over the keys as they stand now (§5: a key moved): its candidates are written again, the
|
||||
// nodes that must answer found again, and it is judged and asked again; a proposal that no longer lays ends.
|
||||
func (p *passer) layAgain(ctx context.Context, prop mma.Proposal, rev uint64, current map[string]mma.Assignments, revs map[string]uint64, why string) {
|
||||
p.lay(ctx, prop, rev, current, revs, why, false)
|
||||
}
|
||||
|
||||
// lay lays a proposal over the keys as they stand now and writes its candidates, each with what it adds (A-316); first says it is the
|
||||
// proposal's first laying, the verb having written the proposal alone (A-317).
|
||||
func (p *passer) lay(ctx context.Context, prop mma.Proposal, rev uint64, current map[string]mma.Assignments, revs map[string]uint64, why string, first bool) {
|
||||
held, recorded := p.lays(ctx)
|
||||
again, laid, findings := assign.Propose(assign.ProposeIn{ID: prop.ID, By: prop.By, Why: prop.Why, At: prop.At, Changes: prop.Changes,
|
||||
Current: current, Revisions: revs, Held: held, Recorded: recorded, SeatListRead: p.seats.Heard()})
|
||||
@@ -451,12 +464,19 @@ func (p *passer) layAgain(ctx context.Context, prop mma.Proposal, rev uint64, cu
|
||||
}
|
||||
p.deleteCandidates(ctx, prop.ID)
|
||||
p.deletePulses(ctx, prop.ID)
|
||||
if p.laidHere == nil {
|
||||
p.laidHere = map[string]bool{}
|
||||
}
|
||||
p.laidHere[prop.ID] = true
|
||||
for _, node := range plc.Nodes(again) {
|
||||
key, _ := plc.CandidateKey(prop.ID, node)
|
||||
c := mma.CandidatesByProposalNodeValue{Proposal: prop.ID, Assignments: laid[node]}
|
||||
c := p.candidateOf(prop.ID, node, laid[node], current)
|
||||
p.put(ctx, plc.CandidatesBucket(), key, func() ([]byte, error) { return plc.EncodeCandidate(c) })
|
||||
}
|
||||
again.Written, again.Words = ptr(p.clock().UTC()), why+": laid again over the keys as they stand, to be judged and asked again"
|
||||
if first {
|
||||
again.Words = why
|
||||
}
|
||||
p.writeProposal(ctx, again, rev)
|
||||
}
|
||||
|
||||
|
||||
@@ -179,23 +179,11 @@ func (s *seatTools) proposal(id string) (mma.Proposal, error) {
|
||||
return plc.DecodeProposal(raw)
|
||||
}
|
||||
|
||||
// write writes a proposal laid by assign.Propose: each candidate, then the proposal, whose write the assigner's process
|
||||
// takes as the event to compose and judge it.
|
||||
func (s *seatTools) write(p mma.Proposal, laid map[string]*mma.Assignments) error {
|
||||
// write writes a proposal laid by assign.Propose, whose write the assigner's process takes as the event to lay it: it writes the
|
||||
// candidates, each with what it adds (design 47 G A-317, A-316); the verb writes none, so no node-engine hears a candidate before
|
||||
// its parts.
|
||||
func (s *seatTools) write(p mma.Proposal) error {
|
||||
p.Written = ptr(s.now().UTC())
|
||||
for _, node := range plc.Nodes(p) {
|
||||
key, err := plc.CandidateKey(p.ID, node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
raw, err := plc.EncodeCandidate(mma.CandidatesByProposalNodeValue{Proposal: p.ID, Assignments: laid[node]})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.st.put(mma.StateCandidates, key, raw); err != nil {
|
||||
return fmt.Errorf("the candidate %s is not written: %w", key, err)
|
||||
}
|
||||
}
|
||||
raw, err := plc.EncodeProposal(p)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -265,7 +253,7 @@ func (s *seatTools) doPropose(args map[string]any, by string) (any, error) {
|
||||
}
|
||||
enrollers := plc.EnrollersOf(current)
|
||||
now := s.now()
|
||||
p, laid, findings := assign.Propose(assign.ProposeIn{ID: plc.NewProposalID(now), By: by, Why: arg(args, "why"), At: now,
|
||||
p, _, findings := assign.Propose(assign.ProposeIn{ID: plc.NewProposalID(now), By: by, Why: arg(args, "why"), At: now,
|
||||
Changes: changes, Current: current, Revisions: revs, Held: held, Recorded: recorded, Warrant: warrant, SeatListRead: s.seatList.Heard()})
|
||||
if refusal := plc.AddNodeRefusal(p, moduleOfCaller(by, enrollers), enrollers); refusal != "" {
|
||||
findings = append(findings, refusal)
|
||||
@@ -273,7 +261,7 @@ func (s *seatTools) doPropose(args map[string]any, by string) (any, error) {
|
||||
if len(findings) > 0 {
|
||||
return nil, fmt.Errorf("the proposal is refused: %s", strings.Join(findings, "; "))
|
||||
}
|
||||
if err := s.write(p, laid); err != nil {
|
||||
if err := s.write(p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return proposed{Proposal: p, Follow: followProposal(p.ID)}, nil
|
||||
|
||||
@@ -127,7 +127,7 @@ func TestTheSeatsVerbs(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
move := got.(proposed).Proposal
|
||||
if move.By != "operator@laptop" || move.Base["anchor"] != int64(len(st[mma.StateAssignments].values["anchor.node"])) || move.Base["laptop"] != 0 || len(st[mma.StateCandidates].values) != 2 {
|
||||
if move.By != "operator@laptop" || move.Base["anchor"] != int64(len(st[mma.StateAssignments].values["anchor.node"])) || move.Base["laptop"] != 0 || len(st[mma.StateCandidates].values) != 0 {
|
||||
t.Fatalf("propose = %+v", move)
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ func TestTheSeatsVerbs(t *testing.T) {
|
||||
t.Error("a node with no key was answered")
|
||||
}
|
||||
ps, err := s.doProposals(map[string]any{"id": move.ID})
|
||||
if list := ps.(map[string]any)["proposals"].([]openProposal); err != nil || len(list) != 1 || len(list[0].Candidates) != 2 {
|
||||
if list := ps.(map[string]any)["proposals"].([]openProposal); err != nil || len(list) != 1 || len(list[0].Candidates) != 0 {
|
||||
t.Errorf("proposals = %+v, %v", ps, err)
|
||||
} else {
|
||||
reads[mma.StateProposals], reads[mma.StateCandidates] = true, true
|
||||
|
||||
Reference in New Issue
Block a user