Bound a proposal's answers and ask by pulse keys whose expiry the holder hears, and name a hand-back proposal, so no timer waits (ADR 0567 §4, §7, issue 1126)
This commit is contained in:
@@ -26,8 +26,34 @@ const (
|
||||
PlacementProposalsRead = AssignerModule + "." + PlacementProposalsState
|
||||
// PlacementProposalsHistory is the revisions the bucket keeps per key.
|
||||
PlacementProposalsHistory = 5
|
||||
|
||||
// PlacementPulsesState is the holder's state of bounds (§4): a key per node a proposal waits on, written with a time
|
||||
// to live of the answer bound, and a key per raised ask, with its ask's; the server's delete marker when one ends is
|
||||
// the event that fails a silent node or ends an unanswered ask, so the holder keeps no timer. Its bucket allows a
|
||||
// time to live per key (catalogue.StateDeclaration.KeyTTL).
|
||||
PlacementPulsesState = "pulses"
|
||||
PlacementPulsesBucket = AssignerModule + "_" + PlacementPulsesState
|
||||
)
|
||||
|
||||
// PulseAnswerKey is the key of the bound on one node's answer to a proposal: `answer.<proposal>.<node>`.
|
||||
func PulseAnswerKey(proposal, node string) (string, error) {
|
||||
if err := CheckProposalID(proposal); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := checkName("node", node); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "answer." + proposal + "." + node, nil
|
||||
}
|
||||
|
||||
// PulseAskKey is the key of the bound on a proposal's raised ask: `ask.<proposal>`.
|
||||
func PulseAskKey(proposal string) (string, error) {
|
||||
if err := CheckProposalID(proposal); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "ask." + proposal, nil
|
||||
}
|
||||
|
||||
// The operations a proposal's change names (ADR 0567 §2).
|
||||
const (
|
||||
OpPlace = "place" // place a module on a node, with its host ports and bindings
|
||||
@@ -360,6 +386,9 @@ type PlacementProposal struct {
|
||||
// Must is every node it must hear from (§3): each whose key it changes and each whose composition it changes
|
||||
// through another's, sorted; set by the holder once it composed, before then the nodes of Base.
|
||||
Must []string `json:"must,omitempty"`
|
||||
// HandBack says it is the operator's hand-back (§7): every key of Base deleted on its warrant, with no change and no
|
||||
// node's answer.
|
||||
HandBack bool `json:"hand-back,omitempty"`
|
||||
// State is ProposalAnswering, ProposalFailed or ProposalAsked; Verdict is the latest judgement, Ask the authorising
|
||||
// ask raised on a passing one.
|
||||
State string `json:"state"`
|
||||
@@ -385,8 +414,8 @@ func (p PlacementProposal) check() error {
|
||||
if strings.TrimSpace(p.By) == "" || strings.TrimSpace(p.Why) == "" {
|
||||
return fmt.Errorf("delivery: the proposal %s names no proposer or no why", p.ID)
|
||||
}
|
||||
if len(p.Changes) == 0 || len(p.Base) == 0 {
|
||||
return fmt.Errorf("delivery: the proposal %s changes nothing", p.ID)
|
||||
if len(p.Base) == 0 || (len(p.Changes) == 0) != p.HandBack || (p.HandBack && p.Seed) {
|
||||
return fmt.Errorf("delivery: the proposal %s changes nothing, or is a hand-back with changes", p.ID)
|
||||
}
|
||||
for i, c := range p.Changes {
|
||||
if err := c.check(); err != nil {
|
||||
|
||||
@@ -535,20 +535,32 @@ func TestPlacement(t *testing.T) {
|
||||
if ExportDigest(current) != ExportDigest(map[string]Placement{"novox": novox, "g14": g14}) || ExportDigest(current) == ExportDigest(nil) {
|
||||
t.Error("the export digest depends on order or not on the placements")
|
||||
}
|
||||
back := p
|
||||
back.HandBack, back.Changes = true, nil
|
||||
if _, err := EncodePlacementProposal(back); err != nil {
|
||||
t.Errorf("a hand-back: %v", err)
|
||||
}
|
||||
if k, err := PulseAnswerKey(id, "g14"); err != nil || k != "answer."+id+".g14" {
|
||||
t.Errorf("PulseAnswerKey = %q, %v", k, err)
|
||||
}
|
||||
if k, err := PulseAskKey(id); err != nil || k != "ask."+id || PlacementPulsesBucket != "mesh-delivery-assigner_pulses" {
|
||||
t.Errorf("PulseAskKey = %q, %v", k, err)
|
||||
}
|
||||
if ChangesDigest(p) == ChangesDigest(seed) {
|
||||
t.Error("two proposals' changes digest alike")
|
||||
}
|
||||
for name, mut := range map[string]func(*PlacementProposal){
|
||||
"no id": func(p *PlacementProposal) { p.ID = "" },
|
||||
"no proposer": func(p *PlacementProposal) { p.By = "" },
|
||||
"no why": func(p *PlacementProposal) { p.Why = " " },
|
||||
"no change": func(p *PlacementProposal) { p.Changes = nil },
|
||||
"no base": func(p *PlacementProposal) { p.Base = map[string]uint64{"novox": 1} },
|
||||
"no state": func(p *PlacementProposal) { p.State = "" },
|
||||
"asked, no ask": func(p *PlacementProposal) { p.State = ProposalAsked },
|
||||
"seed mixed": func(p *PlacementProposal) { p.Seed, p.ExportDigest = true, "d" },
|
||||
"bad change": func(p *PlacementProposal) { p.Changes[0].Op = "swap" },
|
||||
"seed, no digest": func(p *PlacementProposal) { *p = seed; p.ExportDigest = "" },
|
||||
"no id": func(p *PlacementProposal) { p.ID = "" },
|
||||
"no proposer": func(p *PlacementProposal) { p.By = "" },
|
||||
"no why": func(p *PlacementProposal) { p.Why = " " },
|
||||
"no change": func(p *PlacementProposal) { p.Changes = nil },
|
||||
"no base": func(p *PlacementProposal) { p.Base = map[string]uint64{"novox": 1} },
|
||||
"no state": func(p *PlacementProposal) { p.State = "" },
|
||||
"asked, no ask": func(p *PlacementProposal) { p.State = ProposalAsked },
|
||||
"seed mixed": func(p *PlacementProposal) { p.Seed, p.ExportDigest = true, "d" },
|
||||
"bad change": func(p *PlacementProposal) { p.Changes[0].Op = "swap" },
|
||||
"seed, no digest": func(p *PlacementProposal) { *p = seed; p.ExportDigest = "" },
|
||||
"hand-back with changes": func(p *PlacementProposal) { p.HandBack = true },
|
||||
} {
|
||||
q := p
|
||||
q.Changes = append([]PlacementProposalChange(nil), p.Changes...)
|
||||
|
||||
Reference in New Issue
Block a user