diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat.go index cbd84b1..c8ef722 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat.go @@ -10,7 +10,6 @@ import ( "slices" "sort" "strings" - "sync" "time" "github.com/nats-io/nats.go" @@ -58,67 +57,23 @@ func streamPublisher(ctx context.Context, js jetstream.JetStream, node string) p } } -// seatActs keeps what the seat's calls came to while this process runs: the export read again at a seed's warrant. -// The seat calls no verb of the controller to keep its assignments in step: placement is the one source (ADR 0567 §8, -// issue 774). +// seatActs keeps the seat's acts while this process runs: the warrant recorded in the hand-act log. The seat calls no verb of +// the controller: placement is the one source (ADR 0567 §8, issue 774). type seatActs struct { - mu sync.Mutex - bus caller - wake func() - logf func(string, ...any) - export map[string]exportCheck + bus caller + wake func() + logf func(string, ...any) // rec records the seat's acts in the hand-act log (ADR 0565 §11). rec handacts.Recorder } -// exportCheck is the export read again for one proposal: running, its answer, or why it was not read. -type exportCheck struct { - running bool - export map[string]delivery.Placement - err string -} - func newSeatActs(bus caller, wake func(), logf func(string, ...any)) *seatActs { - return &seatActs{bus: bus, wake: wake, logf: logf, export: map[string]exportCheck{}, rec: handacts.Recorder{Ask: bus.Call}} -} - -// exportOf is the export read again for a proposal, asked once in the background. -func (a *seatActs) exportOf(id string) exportCheck { - a.mu.Lock() - defer a.mu.Unlock() - if c, ok := a.export[id]; ok { - return c - } - a.export[id] = exportCheck{running: true} - go func() { - defer a.wake() - raw, err := a.bus.Call(exportCall, map[string]any{}) - c := exportCheck{} - if err != nil { - c.err = "the controller's placement-export did not answer: " + err.Error() - } else if c.export, err = assign.ExportOf(raw); err != nil { - c.err = err.Error() - } - a.mu.Lock() - a.export[id] = c - a.mu.Unlock() - }() - return exportCheck{running: true} -} - -// forget drops what is kept of a proposal's export, so a later warrant reads it again. -func (a *seatActs) forget(id string) { - if a == nil { - return - } - a.mu.Lock() - delete(a.export, id) - a.mu.Unlock() + return &seatActs{bus: bus, wake: wake, logf: logf, rec: handacts.Recorder{Ask: bus.Call}} } // record records once, in the hand-act log, that placement was written on the operator's warrant (§5): the router's // record of the teller's ask says who chose and how (ADR 0274). The log is the seat mesh-hand-acts' from its delivery, and -// the controller's while that seat has no holder and the controller holds its lease (ADR 0565 §11, handacts.Recorder). +// the controller's while that seat has no holder (ADR 0565 §11, handacts.Recorder). func (a *seatActs) record(warrant string) { if a == nil || warrant == "" { return @@ -133,18 +88,6 @@ func (a *seatActs) record(warrant string) { }() } -// handAct records a hand-back in the hand-act log (§7), as record does. -func (a *seatActs) handAct(prop delivery.PlacementProposal) { - go func() { - if taken, err := a.rec.Record(handacts.Request{What: "mesh-placement hand-back of " + strings.Join(prop.Nodes(), ", "), - Why: prop.Why, Cause: "asked by " + prop.By + ", approved by the operator"}); err != nil { - a.logf("the hand-back %s is not recorded in the hand-act log: %v", prop.ID, err) - } else { - a.logf("the hand-back %s is recorded in the log of %s", prop.ID, taken.Log) - } - }() -} - // clearStaleCalls clears, once, the conditions of the refused controller calls a previous process raised (findings of the // kinds controller-call and controller-pin): the assigner calls no verb of the controller any more (ADR 0567 §8, issue 774), // so nothing would clear them. It is tried again at the next pass while the findings cannot be read. @@ -320,17 +263,18 @@ func (p *passer) stepProposal(ctx context.Context, prop delivery.PlacementPropos p.endProposal(ctx, prop, prop.Withdrawn) return } + if prop.Seed || prop.HandBack { + // Adopt and hand-back are gone since the lease write (ADR 0547 §11, ADR 0567 §11): a proposal one left open ends. + p.endProposal(ctx, prop, "adopt and hand-back are gone since the lease write: placement is the mesh's alone") + return + } akey, _ := delivery.PlacementAskKey(prop.ID) again := !prop.Again.IsZero() && (prop.Verdict == nil || prop.Again.After(prop.Verdict.At)) if prop.State == delivery.ProposalAsked { askPulse, _ := delivery.PulseAskKey(prop.ID) switch { case p.answeredWith(akey, assign.OptionYes): - if prop.HandBack { - p.handBack(ctx, prop, akey, revs) - } else { - p.warranted(ctx, prop, rev, akey, current, revs) - } + p.warranted(ctx, prop, rev, akey, current, revs) return case p.answeredWith(akey, assign.OptionNo): p.endProposal(ctx, prop, "declined by the operator") @@ -340,19 +284,6 @@ func (p *passer) stepProposal(ctx context.Context, prop delivery.PlacementPropos return } } - if prop.HandBack { - // The operator's alone (§7): no node answers it; its ask is raised at once, and again on again. - if prop.State != delivery.ProposalAsked || again { - brief := fmt.Sprintf("%s (asked by %s): every node's placement key is deleted, %s, and each node returns to the "+ - "controller's compositions; the history is kept", prop.Why, prop.By, strings.Join(prop.Nodes(), ", ")) - now := p.clock().UTC() - prop.Verdict = &delivery.PlacementVerdict{State: delivery.VerdictPass, At: now} - if prop, ok := p.ask(ctx, prop, akey, brief); ok { - p.writeProposal(ctx, prop, rev) - } - } - return - } if moved(prop, revs) { p.layAgain(ctx, prop, rev, current, revs, "a key it was laid over moved since") return @@ -523,8 +454,7 @@ func (p *passer) mustAnswer(ctx context.Context, prop delivery.PlacementProposal func (p *passer) layAgain(ctx context.Context, prop delivery.PlacementProposal, rev uint64, current map[string]delivery.Placement, revs map[string]uint64, why string) { 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, - Seed: prop.Seed, ExportDigest: prop.ExportDigest}) + Current: current, Revisions: revs, Held: held, Recorded: recorded}) if len(findings) > 0 { p.endProposal(ctx, prop, why+", and laid over the keys now it is refused: "+strings.Join(findings, "; ")) return @@ -537,7 +467,6 @@ func (p *passer) layAgain(ctx context.Context, prop delivery.PlacementProposal, p.put(ctx, delivery.PlacementCandidatesBucket, key, func() ([]byte, error) { return delivery.EncodePlacementCandidate(c) }) } again.Written, again.Words = p.clock().UTC(), why+": laid again over the keys as they stand, to be judged and asked again" - p.seat.forget(prop.ID) p.writeProposal(ctx, again, rev) } @@ -554,8 +483,7 @@ func (p *passer) lays(ctx context.Context) (func(string) bool, func(string) bool return func(m string) bool { return held[m] }, func(n string) bool { _, ok := records[n]; return ok } } -// warranted writes an approved proposal (§5): for the seed, after the export is read again and found unchanged (§7); -// each changed key by compare-and-set at the revision it was laid over; then the warrant recorded, the candidates and the +// warranted writes an approved proposal (§5): each changed key by compare-and-set at the revision it was laid over; then the warrant recorded, the candidates and the // proposal deleted, and placement-changed said. func (p *passer) warranted(ctx context.Context, prop delivery.PlacementProposal, rev uint64, akey string, current map[string]delivery.Placement, revs map[string]uint64) { if err := p.performs(akey, assign.PlacementAct(prop)); err != nil { @@ -563,26 +491,6 @@ func (p *passer) warranted(ctx context.Context, prop delivery.PlacementProposal, return } answer, _ := p.w.answer(akey) - if prop.Seed { - if p.seat == nil { - return - } - c := p.seat.exportOf(prop.ID) - switch { - case c.running: - return // its answer wakes the next pass - case c.err != "": - if words := "the export cannot be read again at the warrant: " + c.err; prop.Words != words { - prop.Words = words - p.writeProposal(ctx, prop, rev) - } - return - case delivery.ExportDigest(c.export) != prop.ExportDigest: - p.endProposal(ctx, prop, "the controller's export changed since the seed was proposed: the new export is proposed as the seed") - p.proposeSeed(ctx, prop, c.export) - return - } - } if moved(prop, revs) { p.layAgain(ctx, prop, rev, current, revs, "a key moved since it was approved, so its warrant is spent") return @@ -655,44 +563,6 @@ func sortedNodes(m map[string]*delivery.Placement) []string { return out } -// handBack deletes every key on the operator's warrant (§7) at the revision the hand-back read, records it in the -// controller's hand-act log and says placement-changed; every node returns to the controller's compositions. A key moved -// since ends it, and the operator hands back again. -func (p *passer) handBack(ctx context.Context, prop delivery.PlacementProposal, akey string, revs map[string]uint64) { - if err := p.performs(akey, assign.PlacementAct(prop)); err != nil { - p.endProposal(ctx, prop, "its warrant does not bind this hand-back, so nothing is deleted: "+err.Error()) - return - } - answer, _ := p.w.answer(akey) - current, held := p.seatPlacements() - var changes []delivery.PlacementChange - for _, node := range prop.Nodes() { - key, _ := delivery.PlacementNodeKey(node) - err := error(nil) - if held[node] != prop.Base[node] { - err = fmt.Errorf("its placement is at revision %d, the hand-back read %d", held[node], prop.Base[node]) - } else { - _, err = p.writeNode(ctx, node, nil) - } - if err != nil { - p.sayChanged(prop, answer, changes) - p.endProposal(ctx, prop, fmt.Sprintf("%s moved since the hand-back was asked (%v): hand back again", key, err)) - return - } - c := delivery.ChangeOf(current[node], delivery.Placement{Node: node}) - c.Before = prop.Base[node] - changes = append(changes, c) - } - if p.seat != nil { - p.seat.handAct(prop) - } - p.seat.record(answer.Warrant) - _ = p.kvs[delivery.PlacementProposalsBucket].Delete(ctx, prop.ID) - p.w.put(delivery.PlacementProposalsBucket, prop.ID, nil) - p.deletePulses(ctx, prop.ID) - p.sayChanged(prop, answer, changes) -} - // sayChanged says placement-changed for the keys written. func (p *passer) sayChanged(prop delivery.PlacementProposal, answer delivery.Answer, changes []delivery.PlacementChange) { if len(changes) == 0 { @@ -709,26 +579,6 @@ func (p *passer) sayChanged(prop delivery.PlacementProposal, answer delivery.Ans Warrant: &delivery.PlacementWarrant{Warrant: answer.Warrant, Words: answer.Words, At: at}}, prop.ID+".changed") } -// proposeSeed proposes an export as the seed again, by the same proposer and why. -func (p *passer) proposeSeed(ctx context.Context, was delivery.PlacementProposal, export map[string]delivery.Placement) { - held, recorded := p.lays(ctx) - now := p.clock() - seed, laid, findings := assign.Propose(assign.ProposeIn{ID: delivery.NewProposalID(now), By: was.By, Why: was.Why, At: now, - Changes: assign.SeedChanges(export), Current: map[string]delivery.Placement{}, Revisions: map[string]uint64{}, - Held: held, Recorded: recorded, Seed: true, ExportDigest: delivery.ExportDigest(export)}) - if len(findings) > 0 { - p.logf("the new export is not proposed as the seed: %s", strings.Join(findings, "; ")) - return - } - for _, node := range seed.Nodes() { - key, _ := delivery.PlacementCandidateKey(seed.ID, node) - c := delivery.PlacementCandidate{Proposal: seed.ID, Placement: laid[node]} - p.put(ctx, delivery.PlacementCandidatesBucket, key, func() ([]byte, error) { return delivery.EncodePlacementCandidate(c) }) - } - seed.Written = now.UTC() - p.writeProposal(ctx, seed, 0) -} - // endProposal ends a proposal: its ask, its candidates and its key deleted, and placement-withdrawn said carrying it // whole with why it ended. func (p *passer) endProposal(ctx context.Context, prop delivery.PlacementProposal, why string) { @@ -744,7 +594,6 @@ func (p *passer) endProposal(ctx context.Context, prop delivery.PlacementProposa return } p.w.put(delivery.PlacementProposalsBucket, prop.ID, nil) - p.seat.forget(prop.ID) p.logf("the proposal %s ends: %s", prop.ID, why) p.sayEvent(delivery.PlacementWithdrawnEvent, delivery.PlacementEvent{Proposal: prop, Ended: why}, prop.ID+".withdrawn") } diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools.go index 332a234..15a565e 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools.go @@ -206,7 +206,7 @@ func (s *seatTools) doPropose(args map[string]any, by string) (any, error) { } for _, c := range changes { if c.Op == delivery.OpSeed { - return nil, errors.New("the seed is adopt's alone: propose takes place, take-off, pin, unpin, hold, unhold, decide and config") + return nil, errors.New("the seed is gone with adopt: propose takes place, take-off, pin, unpin, hold, unhold, decide and config") } } current, revs, err := s.placements() @@ -269,54 +269,15 @@ func (s *seatTools) doAgain(args map[string]any) (any, error) { return map[string]any{"proposal": p.ID, "again": p.Again, "follow": followProposal(p.ID)}, nil } -// exportCall is the controller's verb the seed is read from, by request and reply on its seat (§7). -const exportCall = "seat:" + catalogue.ControllerSeatName + ".placement-export" - -// doAdopt is the verb adopt (§7): the controller's export proposed whole as the seed. +// doAdopt is the verb adopt (§7): gone since the lease write. The seed was the controller's export proposed whole, and the +// mesh has no controller export to take over (ADR 0547 §11, ADR 0567 §11); nothing is written. func (s *seatTools) doAdopt(args map[string]any, by string) (any, error) { - current, _, err := s.placements() - if err != nil { - return nil, err - } - if len(current) > 0 { - return nil, fmt.Errorf("placement is taken over already (%d keys): propose a change instead, or hand it back first", len(current)) - } - if ids, err := s.st.keys(delivery.PlacementProposalsState); err == nil { - for _, id := range ids { - if p, err := s.proposal(id); err == nil && p.Seed { - return nil, fmt.Errorf("the seed %s is open: withdraw it first, or answer its ask", id) - } - } - } - raw, err := s.ask(exportCall, map[string]any{}) - if err != nil { - return nil, fmt.Errorf("the controller's placement-export did not answer: %w", err) - } - export, err := assign.ExportOf(raw) - if err != nil { - return nil, err - } - held, recorded, err := s.lays() - if err != nil { - return nil, err - } - now := s.now() - p, laid, findings := assign.Propose(assign.ProposeIn{ID: delivery.NewProposalID(now), By: by, Why: arg(args, "why"), At: now, - Changes: assign.SeedChanges(export), Current: map[string]delivery.Placement{}, Revisions: map[string]uint64{}, - Held: held, Recorded: recorded, Seed: true, ExportDigest: delivery.ExportDigest(export)}) - if len(findings) > 0 { - return nil, fmt.Errorf("the seed is refused: %s", strings.Join(findings, "; ")) - } - if err := s.write(p, laid); err != nil { - return nil, err - } - return proposed{Proposal: p, Follow: followProposal(p.ID)}, nil + return nil, errors.New("adopt is gone: placement is the mesh's alone since the lease write, and there is no controller to take it over from (ADR 0547 §11, ADR 0567 §11)") } -// doHandBack is the verb hand-back (§7): since the lease write placement is the mesh's alone, and there is no controller to -// hand it back to (ADR 0547 §11). +// doHandBack is the verb hand-back (§7): gone since the lease write, as adopt is; nothing is deleted. func (s *seatTools) doHandBack(args map[string]any, by string) (any, error) { - return nil, errors.New("placement is the mesh's alone since the lease write: there is no controller to hand it back to") + return nil, errors.New("hand-back is gone: placement is the mesh's alone since the lease write, and there is no controller to hand it back to (ADR 0547 §11, ADR 0567 §11)") } // placementAnswer is what the verb placement answers. diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go index 8c72782..6e1d736 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go @@ -36,8 +36,8 @@ func (s memStore) history(state, key string) ([]stdio.StateRevision, error) { } // ADR 0567 §2, §7, §10: the seat's seven verbs over the states. propose lays a proposal and writes it with its candidates, -// or refuses naming every finding and writes nothing; withdraw and again mark it for the holder; adopt proposes the -// controller's export whole as the seed, once; hand-back records the hand act and deletes every key; placement and +// or refuses naming every finding and writes nothing; withdraw and again mark it for the holder; adopt and hand-back are +// answered as gone and change nothing (ADR 0547 §11, ADR 0567 §11); placement and // proposals read every state the seat keeps, and history names the stream for what the state no longer keeps. func TestTheSeatsVerbs(t *testing.T) { anchor := delivery.Placement{V: delivery.Version, Node: "anchor", Modules: []delivery.PlacedModule{{Module: "web"}}} @@ -52,32 +52,35 @@ func TestTheSeatsVerbs(t *testing.T) { raw, _ := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: n}}) st[haveState].values[n+".node"] = raw } - export, _ := json.Marshal(map[string]any{"anchor": anchor}) var asked []string s := newSeatTools(st, func(key string, body any) (json.RawMessage, error) { asked = append(asked, key) - if key == exportCall { - out, _ := json.Marshal(map[string]any{"ok": true, "output": string(export)}) - return out, nil - } return json.RawMessage(`{"ok":true}`), nil }) s.now = func() time.Time { return time.Date(2026, 10, 28, 20, 0, 0, 0, time.UTC) } - // adopt: the export proposed whole as the seed, once. - got, err := s.doAdopt(map[string]any{"why": "take over"}, "operator@laptop") + // 774/S28/AC1: adopt and hand-back are answered as gone, naming their records, and change nothing and ask nobody. + raw, _ := delivery.EncodePlacement(anchor) + st[delivery.PlacementState].values["placement.anchor"] = raw + for verb, call := range map[string]func() (any, error){ + "adopt": func() (any, error) { return s.doAdopt(map[string]any{"why": "take over"}, "operator@laptop") }, + "hand-back": func() (any, error) { return s.doHandBack(map[string]any{"why": "back"}, "op") }, + } { + if _, err := call(); err == nil || !strings.Contains(err.Error(), verb+" is gone") || !strings.Contains(err.Error(), "ADR 0567 §11") { + t.Errorf("%s: %v", verb, err) + } + } + if len(asked) != 0 || len(st[delivery.PlacementProposalsState].values) != 0 || len(st[delivery.PlacementCandidatesState].values) != 0 || + len(st[delivery.PlacementState].values) != 1 { + t.Fatalf("adopt or hand-back changed something: asked %v", asked) + } + + // withdraw and again mark the proposal; the holder ends or judges it. + got, err := s.doPropose(map[string]any{"why": "hold", "changes": `[{"op":"hold","node":"anchor","module":"web","claim":"node-shell","scope":"node"}]`}, "op") if err != nil { t.Fatal(err) } seed := got.(proposed).Proposal - if !seed.Seed || seed.ExportDigest != delivery.ExportDigest(map[string]delivery.Placement{"anchor": anchor}) || !slices.Equal(asked, []string{exportCall}) || - len(st[delivery.PlacementCandidatesState].values) != 1 || st[delivery.PlacementProposalsState].values[seed.ID] == nil { - t.Fatalf("adopt = %+v, asked %v", seed, asked) - } - if _, err := s.doAdopt(map[string]any{"why": "again"}, "op"); err == nil || !strings.Contains(err.Error(), "is open") { - t.Errorf("a second adopt with a seed open: %v", err) - } - // withdraw and again mark the proposal; the holder ends or judges it. if _, err := s.doWithdraw(map[string]any{"id": seed.ID, "why": "not yet"}, "op"); err != nil { t.Fatal(err) } @@ -94,8 +97,6 @@ func TestTheSeatsVerbs(t *testing.T) { st[delivery.PlacementCandidatesState].values = map[string][]byte{} // propose: refused whole, naming every finding; a valid move laid over the keys at their revisions. - raw, _ := delivery.EncodePlacement(anchor) - st[delivery.PlacementState].values["placement.anchor"] = raw if _, err := s.doPropose(map[string]any{"why": "x", "changes": `[{"op":"place","node":"anchor","module":"ghost"},{"op":"hold","node":"anchor","module":"web","claim":"x"}]`}, "op"); err == nil || !strings.Contains(err.Error(), "ghost") || len(st[delivery.PlacementProposalsState].values) != 0 { t.Errorf("a refused proposal: %v", err) diff --git a/modules/mesh-delivery-assigner/internal/assign/seat.go b/modules/mesh-delivery-assigner/internal/assign/seat.go index 43ab240..435ac4e 100644 --- a/modules/mesh-delivery-assigner/internal/assign/seat.go +++ b/modules/mesh-delivery-assigner/internal/assign/seat.go @@ -1,7 +1,6 @@ package assign import ( - "encoding/json" "fmt" "strings" "time" @@ -18,15 +17,13 @@ import ( // revision of its key; which modules the indexer holds and which nodes have a record; and, for the seed, the export's // digest. type ProposeIn struct { - ID, By, Why string - At time.Time - Changes []delivery.PlacementProposalChange - Current map[string]delivery.Placement - Revisions map[string]uint64 - Held func(module string) bool - Recorded func(node string) bool - Seed bool - ExportDigest string + ID, By, Why string + At time.Time + Changes []delivery.PlacementProposalChange + Current map[string]delivery.Placement + Revisions map[string]uint64 + Held func(module string) bool + Recorded func(node string) bool } // Propose lays a proposal over the current keys (§2): the proposal (state answering, each changed node's base revision), @@ -36,7 +33,7 @@ type ProposeIn struct { // record). A key whose value the proposal leaves as it is changes nothing and is not laid. func Propose(in ProposeIn) (delivery.PlacementProposal, map[string]*delivery.Placement, []string) { p := delivery.PlacementProposal{ID: in.ID, By: strings.TrimSpace(in.By), Why: strings.TrimSpace(in.Why), At: in.At.UTC(), - Changes: in.Changes, Seed: in.Seed, ExportDigest: in.ExportDigest, Base: map[string]uint64{}, State: delivery.ProposalAnswering} + Changes: in.Changes, Base: map[string]uint64{}, State: delivery.ProposalAnswering} var findings []string if p.By == "" { findings = append(findings, "the runtime names no caller: a proposal is made by someone") @@ -48,9 +45,8 @@ func Propose(in ProposeIn) (delivery.PlacementProposal, map[string]*delivery.Pla findings = append(findings, "a proposal changes something: it names no change") } for i, c := range in.Changes { - if (c.Op == delivery.OpSeed) != in.Seed { - findings = append(findings, fmt.Sprintf("change %d: the seed is adopt's alone, and never mixed with other changes", i+1)) - continue + if c.Op == delivery.OpSeed { + findings = append(findings, fmt.Sprintf("change %d: the seed is gone with adopt, the controller's export being gone", i+1)) } } laid, lay := delivery.LayChanges(in.Current, in.Changes) @@ -142,9 +138,6 @@ func PlacementAsk(key string, p delivery.PlacementProposal, brief string, asking } nodes := p.Nodes() headline := fmt.Sprintf("Change placement on %s?", strings.Join(nodes, ", ")) - if p.Seed { - headline = "Take placement over from the controller?" - } if n := len([]rune(headline)); n > asks.HeadlineLength { headline = fmt.Sprintf("Change placement on %d machines?", len(nodes)) } @@ -164,57 +157,6 @@ func PlacementAsk(key string, p delivery.PlacementProposal, brief string, asking }, Asking: asking}, nil } -// ExportOf reads the controller's answer to placement-export (§7): its verb answers what the command printed, the -// JSON of every node's placement, in `output` (the controller's verbAnswer; `answer` when it carries it as data). A -// refused or unreadable answer is an error naming it. -func ExportOf(raw []byte) (map[string]delivery.Placement, error) { - var a struct { - Output string `json:"output"` - OK *bool `json:"ok"` - Answer json.RawMessage `json:"answer"` - } - if err := json.Unmarshal(raw, &a); err != nil { - return nil, fmt.Errorf("the controller's placement-export answered no verb answer: %w", err) - } - if a.OK != nil && !*a.OK { - return nil, fmt.Errorf("the controller's placement-export refused: %s", strings.TrimSpace(a.Output)) - } - var byNode map[string]json.RawMessage - if len(a.Answer) > 0 && string(a.Answer) != "null" { - if err := json.Unmarshal(a.Answer, &byNode); err != nil { - return nil, fmt.Errorf("the controller's placement-export is no placement by node: %w", err) - } - } else if err := json.NewDecoder(strings.NewReader(a.Output)).Decode(&byNode); err != nil { - // The command prints its JSON first; what its standard error said after it is left. - return nil, fmt.Errorf("the controller's placement-export printed no placement by node: %w", err) - } - out := map[string]delivery.Placement{} - for node, rawP := range byNode { - p, err := delivery.DecodePlacement(rawP) - if err != nil { - return nil, fmt.Errorf("the controller's placement-export of %s cannot be read: %w", node, err) - } - if p.Node != node { - return nil, fmt.Errorf("the controller's placement-export names %s under %s", p.Node, node) - } - out[node] = p - } - if len(out) == 0 { - return nil, fmt.Errorf("the controller's placement-export names no node") - } - return out, nil -} - -// SeedChanges is the seed's changes: every node's placement of the export, whole, in node order. -func SeedChanges(export map[string]delivery.Placement) []delivery.PlacementProposalChange { - var out []delivery.PlacementProposalChange - for _, node := range sortedKeys(export) { - p := export[node] - out = append(out, delivery.PlacementProposalChange{Op: delivery.OpSeed, Node: node, Whole: &p}) - } - return out -} - // Rest is what a proposal leaves to write once the keys of written are: its changes of the other nodes, to be laid again // over the keys as the write left them (§5: a key moved while it was written). False when nothing is left. func Rest(p delivery.PlacementProposal, written map[string]*delivery.Placement) (delivery.PlacementProposal, bool) { diff --git a/modules/mesh-delivery-assigner/internal/assign/seat_test.go b/modules/mesh-delivery-assigner/internal/assign/seat_test.go index b95d049..9dea2d4 100644 --- a/modules/mesh-delivery-assigner/internal/assign/seat_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/seat_test.go @@ -42,7 +42,7 @@ func TestThePlacementSeatDecides(t *testing.T) { "nothing changes": {in(delivery.PlacementProposalChange{Op: delivery.OpPlace, Node: "anchor", Module: "web"}), "leave every placement as it is"}, "no why": {func() ProposeIn { i := in(move...); i.Why = " "; return i }(), "says why"}, "no caller": {func() ProposeIn { i := in(move...); i.By = ""; return i }(), "names no caller"}, - "a seed by hand": {in(delivery.PlacementProposalChange{Op: delivery.OpSeed, Node: "laptop", Whole: &laptop}), "adopt's alone"}, + "a seed by hand": {in(delivery.PlacementProposalChange{Op: delivery.OpSeed, Node: "laptop", Whole: &laptop}), "gone with adopt"}, "a pin of nobody placed": {in(delivery.PlacementProposalChange{Op: delivery.OpPin, Node: "laptop", Provision: "db", Provider: &delivery.Provider{Node: "laptop", Module: "store"}}), "not placed there"}, } { if _, _, f := Propose(c.in); !slices.ContainsFunc(f, func(s string) bool { return strings.Contains(s, c.want) }) { @@ -82,16 +82,6 @@ func TestThePlacementSeatDecides(t *testing.T) { } } - // Issue 1126: the seed places the private network, which no repository holds; it is the mesh's own, and passes. - wired := laptop - wired.Modules = append(slices.Clone(laptop.Modules), delivery.PlacedModule{Module: "mesh-wireguard"}) - seed := in(SeedChanges(map[string]delivery.Placement{"laptop": wired})...) - seed.Seed, seed.ExportDigest, seed.Current, seed.Revisions = true, "sha256:x", map[string]delivery.Placement{}, map[string]uint64{} - seed.Held = func(m string) bool { return m != "mesh-wireguard" } // the indexer holds every module but it - if _, _, f := Propose(seed); len(f) != 0 { - t.Errorf("a seed placing the private network is refused: %v", f) - } - writes, err := Writes(p, laid) if err != nil || len(writes) != 2 || writes[0].Key != "anchor.node" || writes[0].Base != 4 || writes[1].Base != 9 { t.Fatalf("writes = %+v, %v", writes, err) @@ -105,29 +95,4 @@ func TestThePlacementSeatDecides(t *testing.T) { if a.Ask.Options[0].Performs(PlacementAct(q)) == nil { t.Error("the ask's yes binds changes it was not asked about") } - - export, _ := json.Marshal(map[string]any{"anchor": anchor, "laptop": laptop}) - for name, c := range map[string]struct { - raw string - want int - }{ - "in output": {`{"ok":true,"output":` + quote(string(export)+"\n") + `}`, 2}, - "as answer": {`{"ok":true,"output":"","answer":` + string(export) + `}`, 2}, - "refused": {`{"ok":false,"output":"no store"}`, 0}, - "no node": {`{"ok":true,"output":"{}"}`, 0}, - "no version": {`{"ok":true,"output":"{\"anchor\":{\"node\":\"anchor\",\"modules\":[]}}"}`, 0}, - } { - got, err := ExportOf([]byte(c.raw)) - if len(got) != c.want || (c.want == 0) != (err != nil) { - t.Errorf("%s: %d nodes, %v", name, len(got), err) - } - } - if seed := SeedChanges(map[string]delivery.Placement{"laptop": laptop, "anchor": anchor}); len(seed) != 2 || seed[0].Node != "anchor" || seed[0].Op != delivery.OpSeed { - t.Errorf("the seed's changes = %+v", seed) - } -} - -func quote(s string) string { - raw, _ := json.Marshal(s) - return string(raw) } diff --git a/modules/mesh-delivery-assigner/manifest_test.go b/modules/mesh-delivery-assigner/manifest_test.go index 9d6bb18..bb2b26d 100644 --- a/modules/mesh-delivery-assigner/manifest_test.go +++ b/modules/mesh-delivery-assigner/manifest_test.go @@ -82,7 +82,7 @@ func TestManifest(t *testing.T) { !slices.Equal(slices.Sorted(slices.Values(m.Streams[0].Events)), slices.Sorted(slices.Values(delivery.PlacementEvents))) { t.Errorf("streams = %+v, want placement carrying the seat's four events for %d days", m.Streams, delivery.PlacementStreamDays) } - if want := []string{"seat:mesh-controller.placement-export", + if want := []string{ "seat:mesh-hand-acts.hand-act", "seat:mesh-hand-acts.hand-acts", "seat:mesh-hand-acts.warranted", // ADR 0565 §9, §11: the seat alone "seat:node-backup.backed-up", "seat:node-backup.now"}; !slices.Equal(slices.Sorted(slices.Values(m.Invokes)), want) { t.Errorf("invokes = %v, want %v", m.Invokes, want) diff --git a/modules/mesh-delivery-assigner/module.json b/modules/mesh-delivery-assigner/module.json index 037ecd5..7dc241c 100644 --- a/modules/mesh-delivery-assigner/module.json +++ b/modules/mesh-delivery-assigner/module.json @@ -46,7 +46,6 @@ "operator-channel" ], "invokes": [ - "seat:mesh-controller.placement-export", "seat:mesh-hand-acts.hand-act", "seat:mesh-hand-acts.hand-acts", "seat:mesh-hand-acts.warranted",