Compare commits
10
Commits
revert/774-l3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5588875c3f | ||
|
|
a2dd7a5c37 | ||
|
|
9b60eda451 | ||
|
|
541ad1d2e2 | ||
|
|
883d976b5f | ||
|
|
ba4bfea0a4 | ||
|
|
fb7ad5cd73 | ||
|
|
5e931eac19 | ||
|
|
d54eab4052 | ||
|
|
c1b4533201 |
@@ -40,6 +40,9 @@ type passer struct {
|
|||||||
// noWant is, by node and module, the reason last said why the node gets no want for the module (issue 1945): a reason is
|
// noWant is, by node and module, the reason last said why the node gets no want for the module (issue 1945): a reason is
|
||||||
// said in the journal when it appears or changes, and its clearing when the node has a want again; one line per change.
|
// said in the journal when it appears or changes, and its clearing when the node has a want again; one line per change.
|
||||||
noWant map[string]string
|
noWant map[string]string
|
||||||
|
// unreached is, by node and module, the words last said why a wanted module may not be reached there (issue 1960: its
|
||||||
|
// provider is down, which holds no want): said once per words, and once more when it is reached again.
|
||||||
|
unreached map[string]string
|
||||||
// nodeFailed is, by node and module, the words of the condition last raised for a failure that is the node's own (ADR 0557
|
// nodeFailed is, by node and module, the words of the condition last raised for a failure that is the node's own (ADR 0557
|
||||||
// §1, issue 1496): raised once per words, cleared once when it ends.
|
// §1, issue 1496): raised once per words, cleared once when it ends.
|
||||||
nodeFailed map[string]string
|
nodeFailed map[string]string
|
||||||
@@ -196,6 +199,7 @@ func (p *passer) wants(ctx context.Context) {
|
|||||||
gatherNeither(neither, name, out)
|
gatherNeither(neither, name, out)
|
||||||
leftNeither(left, m, wanted[name], out)
|
leftNeither(left, m, wanted[name], out)
|
||||||
p.sayNoWant(name, m.Nodes, wanted[name], out)
|
p.sayNoWant(name, m.Nodes, wanted[name], out)
|
||||||
|
p.sayUnreached(name, m.Nodes, out)
|
||||||
p.sayNodeFailed(name, fp, m.Nodes, nodes, out, first)
|
p.sayNodeFailed(name, fp, m.Nodes, nodes, out, first)
|
||||||
if first {
|
if first {
|
||||||
startClears(stale, m, out)
|
startClears(stale, m, out)
|
||||||
@@ -274,6 +278,26 @@ func (p *passer) sayNoWant(module string, nodes []string, before map[string]deli
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sayUnreached says in the journal, once per words, that a module is wanted on a node and may not be reached there, since
|
||||||
|
// no provider of what it requires runs healthy (rule 5, which holds no want: issue 1960), and once when it is reached again.
|
||||||
|
func (p *passer) sayUnreached(module string, nodes []string, out assign.Outcome) {
|
||||||
|
if p.unreached == nil {
|
||||||
|
p.unreached = map[string]string{}
|
||||||
|
}
|
||||||
|
for _, node := range nodes {
|
||||||
|
key := node + "." + module
|
||||||
|
why, last := out.Unreached[node], p.unreached[key]
|
||||||
|
switch {
|
||||||
|
case why != "" && why != last:
|
||||||
|
p.logf("%s on %s is wanted and may not be reached: %s", module, node, why)
|
||||||
|
p.unreached[key] = why
|
||||||
|
case why == "" && last != "":
|
||||||
|
p.logf("%s on %s is reached: its providers run healthy", module, node)
|
||||||
|
delete(p.unreached, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// sayNodeFailed raises, once per node and words, the condition of a failed health check of a module's newest fingerprint fp
|
// sayNodeFailed raises, once per node and words, the condition of a failed health check of a module's newest fingerprint fp
|
||||||
// that is the node's own, since fp runs healthy elsewhere and is not held (ADR 0557 §1, issue 1496), and says it in the journal;
|
// that is the node's own, since fp runs healthy elsewhere and is not held (ADR 0557 §1, issue 1496), and says it in the journal;
|
||||||
// it clears the condition once when the node no longer fails alone, or is no longer one of the module's nodes. The first pass
|
// it clears the condition once when the node no longer fails alone, or is no longer one of the module's nodes. The first pass
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module mesh-delivery-assigner
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028225633-76f7c4d2238f
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771
|
||||||
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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028225633-76f7c4d2238f h1:jCd8WN+Olo4nlaB1cz1HYpEV4xc6Mzw7viXdwU+0fic=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771 h1:Tt16HJk8JZRce1i3JzzDSNdCWOrv7dw8FQ/bckNQ5Ig=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028225633-76f7c4d2238f/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771/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=
|
||||||
|
|||||||
@@ -89,14 +89,15 @@ func Canary(nodes []string, online map[string]bool) string {
|
|||||||
|
|
||||||
// Holds is whether every rule holds for moving node's want for m to m.FP, and if not, the first rule that does not,
|
// Holds is whether every rule holds for moving node's want for m to m.FP, and if not, the first rule that does not,
|
||||||
// with why (ADR 0429 §6). Rule 4, at most `step` others ahead, always holds: the mesh keeps no step, and step 0 is no
|
// with why (ADR 0429 §6). Rule 4, at most `step` others ahead, always holds: the mesh keeps no step, and step 0 is no
|
||||||
// limit (ADR 0528 clause 3).
|
// limit (ADR 0528 clause 3). Rule 5, every provider healthy, holds no want (issue 1960): a module whose provider is down
|
||||||
|
// is wanted all the same, and said as not reached (Unreached), since a site runs while the proxy in front of it is
|
||||||
|
// down and one module never blocks another.
|
||||||
func Holds(m Module, node string, n Nodes) (bool, int, string) {
|
func Holds(m Module, node string, n Nodes) (bool, int, string) {
|
||||||
return holds(m, node, n, true)
|
return holds(m, node, n, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// holds is Holds; with ordering false it leaves out the two rules that order a move among nodes and modules, the canary (3)
|
// holds is Holds; with ordering false it leaves out the canary (3), which orders a move among nodes, and keeps those
|
||||||
// and the providers (5), and keeps those without which there is nothing to want: the build done, not held, the grant and the
|
// without which there is nothing to want: the build done, not held, the grant and the composition.
|
||||||
// composition.
|
|
||||||
func holds(m Module, node string, n Nodes, ordering bool) (bool, int, string) {
|
func holds(m Module, node string, n Nodes, ordering bool) (bool, int, string) {
|
||||||
switch {
|
switch {
|
||||||
case !m.Built:
|
case !m.Built:
|
||||||
@@ -112,11 +113,6 @@ func holds(m Module, node string, n Nodes, ordering bool) (bool, int, string) {
|
|||||||
case node != canary && !healthy(n.Haves[canary][m.Name], m.FP):
|
case node != canary && !healthy(n.Haves[canary][m.Name], m.FP):
|
||||||
return false, delivery.RuleCanary, fmt.Sprintf("the canary %s does not run %s healthy yet", canary, short(m.FP))
|
return false, delivery.RuleCanary, fmt.Sprintf("the canary %s does not run %s healthy yet", canary, short(m.FP))
|
||||||
}
|
}
|
||||||
for _, provision := range m.Manifest.Requires {
|
|
||||||
if ok, why := providedHealthy(provision, node, n); !ok {
|
|
||||||
return false, delivery.RuleProviders, why
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !m.Granted {
|
if !m.Granted {
|
||||||
return false, delivery.RuleGrant, fmt.Sprintf("the controller has not issued the credential of %s for %s", m.Name, short(m.FP))
|
return false, delivery.RuleGrant, fmt.Sprintf("the controller has not issued the credential of %s for %s", m.Name, short(m.FP))
|
||||||
@@ -157,6 +153,17 @@ func providedHealthy(provision, node string, n Nodes) (bool, string) {
|
|||||||
return false, fmt.Sprintf("no provider of %s runs healthy on %s or at its canary", provision, node)
|
return false, fmt.Sprintf("no provider of %s runs healthy on %s or at its canary", provision, node)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// unreached is why m on node may be wanted and not reached: the first provision it requires that no provider runs
|
||||||
|
// healthy on node or at its canary (rule 5, said and never holding a want; issue 1960); empty when all do.
|
||||||
|
func unreached(m Module, node string, n Nodes) string {
|
||||||
|
for _, provision := range m.Manifest.Requires {
|
||||||
|
if ok, why := providedHealthy(provision, node, n); !ok {
|
||||||
|
return why
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// keepsData says whether a module holds data a set-back would lose: an own item of a class that keeps (irreplaceable,
|
// keepsData says whether a module holds data a set-back would lose: an own item of a class that keeps (irreplaceable,
|
||||||
// valuable or rebuildable; cache and none do not).
|
// valuable or rebuildable; cache and none do not).
|
||||||
func keepsData(m catalogue.Manifest) bool {
|
func keepsData(m catalogue.Manifest) bool {
|
||||||
@@ -186,6 +193,9 @@ type Outcome struct {
|
|||||||
Restore []string
|
Restore []string
|
||||||
// Rules is, by node, the rule holding the node back from m.FP and why.
|
// Rules is, by node, the rule holding the node back from m.FP and why.
|
||||||
Rules map[string]Holding
|
Rules map[string]Holding
|
||||||
|
// Unreached is, by node, why the module may be wanted there and not reached: no provider of a provision it requires runs
|
||||||
|
// healthy (rule 5, issue 1960). It holds no want back; the pass says it.
|
||||||
|
Unreached map[string]string
|
||||||
// Neither is, by node, the nodes the module is assigned to that run it (or whose have names it), hold no want for it after this
|
// Neither is, by node, the nodes the module is assigned to that run it (or whose have names it), hold no want for it after this
|
||||||
// pass and are not held (a held module is on the held list), with the rule holding the first want back: the node's own composition does not hold a module with a fingerprint of the new form, so such a module is on
|
// pass and are not held (a held module is on the held list), with the rule holding the first want back: the node's own composition does not hold a module with a fingerprint of the new form, so such a module is on
|
||||||
// neither list and nothing applies it on that node (issue 1945). A module with no fingerprint of the new form goes with the node's
|
// neither list and nothing applies it on that node (issue 1945). A module with no fingerprint of the new form goes with the node's
|
||||||
@@ -301,26 +311,33 @@ func Wants(m Module, n Nodes, wants map[string]delivery.Want, lastHealthy map[st
|
|||||||
}
|
}
|
||||||
ok, rule, words := Holds(mm, node, n)
|
ok, rule, words := Holds(mm, node, n)
|
||||||
target := m.FP
|
target := m.FP
|
||||||
if have, ran := n.Haves[node][m.Name]; !ok && (!had || before.Removed) && (rule == delivery.RuleCanary || rule == delivery.RuleProviders) && runs(have) {
|
if have, ran := n.Haves[node][m.Name]; !ok && (!had || before.Removed) && rule == delivery.RuleCanary {
|
||||||
// A node that runs the module and has no want for it has no want the ordering rules could hold it back to: holding its first
|
// The canary orders a move, never the first want of a node that does not run the module (issue 1960: de-spiegel,
|
||||||
// want back leaves the module on neither list, since the node's own composition gives up every module with a fingerprint of
|
// link2pay and photos on novox were left with no want). A node that runs an older fingerprint of the new form,
|
||||||
// the new form (issue 1945: on g14 the bundles of clipmenu, screen-lock and every module requiring x11-display were removed
|
// composed and granted, gets its first want at what it runs, and rule 3 then orders its move to F as from any want.
|
||||||
// and never made again, because xorg's health is unknown and screen-lock's canary never ran it healthy). Its first want names
|
// A node that runs an old-form build, which no want can name, stays under the canary (issue 1945, a design limit):
|
||||||
// what it runs, never a newer fingerprint, so the canary is never passed over: F when it runs F (by the rules without which
|
// its first want names F once the canary runs F healthy. Any other node that runs it (F itself, or an older
|
||||||
// there is nothing to want), or an older fingerprint of the new form whose composition and grant exist. Rules 3 and 5 then
|
// fingerprint with no composition or grant) gets F by the rules without which there is nothing to want.
|
||||||
// order the move from it as from any want.
|
older := ran && runs(have) && have.FP != m.FP && strings.HasPrefix(have.FP, delivery.SourcePrefix2)
|
||||||
switch _, composed := m.composition(node, have.FP); {
|
if older {
|
||||||
case !ran:
|
_, composed := m.composition(node, have.FP)
|
||||||
case have.FP == m.FP:
|
older = composed && m.GrantedAt != nil && m.GrantedAt(have.FP)
|
||||||
ok, rule, words = holds(mm, node, n, false)
|
|
||||||
case !strings.HasPrefix(have.FP, delivery.SourcePrefix2):
|
|
||||||
// An old-form build (not src2:) has no composition, so no want can ever name it: its first want names F, and only by every
|
|
||||||
// rule as usual, the canary (3) and the providers (5) included, which here hold it back. Nothing is written and the module
|
|
||||||
// stays on neither list until the canary runs F healthy; then Holds passes and the want names F (issue 1945, a design
|
|
||||||
// limit set by the lead: the canary stays in force and old-form modules leave neither list by themselves).
|
|
||||||
case composed && m.GrantedAt != nil && m.GrantedAt(have.FP):
|
|
||||||
ok, target = true, have.FP
|
|
||||||
}
|
}
|
||||||
|
oldForm := ran && runs(have) && have.FP != m.FP && !strings.HasPrefix(have.FP, delivery.SourcePrefix2)
|
||||||
|
switch {
|
||||||
|
case older:
|
||||||
|
ok, target = true, have.FP
|
||||||
|
case oldForm:
|
||||||
|
// held back by rule 3 as written: on neither list until the canary runs F healthy
|
||||||
|
default:
|
||||||
|
ok, rule, words = holds(mm, node, n, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if why := unreached(m, node, n); why != "" {
|
||||||
|
if out.Unreached == nil {
|
||||||
|
out.Unreached = map[string]string{}
|
||||||
|
}
|
||||||
|
out.Unreached[node] = why
|
||||||
}
|
}
|
||||||
if ok && target == m.FP && len(out.NodeFailed) > 0 && !(had && !before.Removed && before.FP == m.FP) &&
|
if ok && target == m.FP && len(out.NodeFailed) > 0 && !(had && !before.Removed && before.FP == m.FP) &&
|
||||||
n.Haves[node][m.Name].FP != m.FP {
|
n.Haves[node][m.Name].FP != m.FP {
|
||||||
|
|||||||
@@ -65,9 +65,6 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) {
|
|||||||
delivery.RuleCanary: func(_ *Module, n *Nodes) {
|
delivery.RuleCanary: func(_ *Module, n *Nodes) {
|
||||||
n.Haves["ace"]["mail"] = delivery.Have{FP: fOld, Phase: delivery.PhaseRunning, Health: delivery.HealthOK}
|
n.Haves["ace"]["mail"] = delivery.Have{FP: fOld, Phase: delivery.PhaseRunning, Health: delivery.HealthOK}
|
||||||
},
|
},
|
||||||
delivery.RuleProviders: func(_ *Module, n *Nodes) {
|
|
||||||
n.Haves["ace"]["store"] = delivery.Have{FP: fpOf('s'), Phase: delivery.PhaseRunning, Health: delivery.HealthWaits}
|
|
||||||
},
|
|
||||||
delivery.RuleGrant: func(m *Module, _ *Nodes) { m.Granted = false },
|
delivery.RuleGrant: func(m *Module, _ *Nodes) { m.Granted = false },
|
||||||
delivery.RuleComposed: func(m *Module, _ *Nodes) {
|
delivery.RuleComposed: func(m *Module, _ *Nodes) {
|
||||||
m.Composed = func(string, string) (delivery.ComposedDigest, bool) { return delivery.ComposedDigest{}, false }
|
m.Composed = func(string, string) (delivery.ComposedDigest, bool) { return delivery.ComposedDigest{}, false }
|
||||||
@@ -86,6 +83,50 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) {
|
|||||||
t.Errorf("rule %d broken: g14's want is %+v", rule, w)
|
t.Errorf("rule %d broken: g14's want is %+v", rule, w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 1960: rule 5 holds no want, and no module is left without a first want for a provider down or a node offline; the
|
||||||
|
// provider down is said (Unreached). novox on 2026-10-29: route-proxy down held de-spiegel and link2pay, minio not
|
||||||
|
// healthy held photos, and novox offline left a module with no canary.
|
||||||
|
for _, c := range []struct {
|
||||||
|
name, module, provision, provider string
|
||||||
|
have *delivery.Have // what novox runs of the module; nil: a new assignment
|
||||||
|
before map[string]delivery.Want
|
||||||
|
online bool
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"de-spiegel, route-proxy down, old form", "de-spiegel", "route", "route-proxy",
|
||||||
|
&delivery.Have{FP: "build-1", Phase: delivery.PhaseRunning, Health: delivery.HealthUnknown}, nil, true, fNew},
|
||||||
|
{"link2pay, route-proxy down, new assignment", "link2pay", "route", "route-proxy", nil, nil, true, fNew},
|
||||||
|
{"photos, minio not healthy, moving its want", "photos", "s3-bucket", "minio",
|
||||||
|
&delivery.Have{FP: fOld, Phase: delivery.PhaseRunning, Health: delivery.HealthOK},
|
||||||
|
map[string]delivery.Want{"novox": {FP: fOld, Composed: dig(fOld)}}, true, fNew},
|
||||||
|
{"novox offline, no canary, new assignment", "link2pay", "route", "route-proxy", nil, nil, false, fNew},
|
||||||
|
{"novox offline, no canary, moving its want", "photos", "s3-bucket", "minio",
|
||||||
|
&delivery.Have{FP: fOld, Phase: delivery.PhaseRunning, Health: delivery.HealthOK},
|
||||||
|
map[string]delivery.Want{"novox": {FP: fOld, Composed: dig(fOld)}}, false, fOld},
|
||||||
|
} {
|
||||||
|
m := Module{Name: c.module, FP: fNew, Built: true, Granted: true, Composed: composedEverywhere, Nodes: []string{"novox"},
|
||||||
|
Manifest: catalogue.Manifest{Module: c.module, Requires: []string{c.provision}}}
|
||||||
|
n := Nodes{Online: map[string]bool{"novox": c.online}, Haves: map[string]map[string]delivery.Have{"novox": {
|
||||||
|
c.provider: {FP: fpOf('p'), Phase: delivery.PhaseFailed, Health: delivery.HealthFailed}}},
|
||||||
|
Providers: map[string]map[string][]string{c.provision: {c.provider: {"novox"}}}}
|
||||||
|
if c.have != nil {
|
||||||
|
n.Haves["novox"][c.module] = *c.have
|
||||||
|
}
|
||||||
|
out := Wants(m, n, c.before, nil)
|
||||||
|
w, written := out.Wants["novox"]
|
||||||
|
if !written {
|
||||||
|
w = c.before["novox"]
|
||||||
|
}
|
||||||
|
if w.FP != c.want || w.Removed {
|
||||||
|
t.Errorf("%s: novox's want = %+v, want %s", c.name, w, short(c.want))
|
||||||
|
}
|
||||||
|
if _, on := out.Neither["novox"]; on {
|
||||||
|
t.Errorf("%s: on neither list: %+v", c.name, out.Neither)
|
||||||
|
}
|
||||||
|
if c.online && !strings.Contains(out.Unreached["novox"], c.provision) {
|
||||||
|
t.Errorf("%s: the provider down is not said: %q", c.name, out.Unreached["novox"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 1126/AC-SW1, AC-SW2, AC-SW3: a want written Self while g14 was placed, kept by a rule after the placement is gone, names
|
// 1126/AC-SW1, AC-SW2, AC-SW3: a want written Self while g14 was placed, kept by a rule after the placement is gone, names
|
||||||
// the controller's composition of the fingerprint the rule keeps (on 2026-10-27 g14's systemd-resolved, held, and forticlient,
|
// the controller's composition of the fingerprint the rule keeps (on 2026-10-27 g14's systemd-resolved, held, and forticlient,
|
||||||
@@ -108,7 +149,7 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) {
|
|||||||
{name: "rule 5 at F", before: fNew, set: func(m *Module, n *Nodes) {
|
{name: "rule 5 at F", before: fNew, set: func(m *Module, n *Nodes) {
|
||||||
atF(m, n)
|
atF(m, n)
|
||||||
n.Haves["ace"]["store"] = delivery.Have{FP: fpOf('s'), Phase: delivery.PhaseRunning, Health: delivery.HealthWaits}
|
n.Haves["ace"]["store"] = delivery.Have{FP: fpOf('s'), Phase: delivery.PhaseRunning, Health: delivery.HealthWaits}
|
||||||
}, want: delivery.Want{FP: fNew, Composed: dig(fNew)}, rule: delivery.RuleProviders},
|
}, want: delivery.Want{FP: fNew, Composed: dig(fNew)}},
|
||||||
{name: "rule 3 at F", before: fNew, set: func(m *Module, n *Nodes) {
|
{name: "rule 3 at F", before: fNew, set: func(m *Module, n *Nodes) {
|
||||||
atF(m, n)
|
atF(m, n)
|
||||||
n.Haves["ace"]["mail"] = delivery.Have{FP: fOld, Phase: delivery.PhaseRunning, Health: delivery.HealthOK}
|
n.Haves["ace"]["mail"] = delivery.Have{FP: fOld, Phase: delivery.PhaseRunning, Health: delivery.HealthOK}
|
||||||
@@ -962,10 +1003,12 @@ func x11() (Module, Nodes) {
|
|||||||
func TestANodeRunningAModuleWithNoWantGetsItsFirstWantAtWhatItRuns(t *testing.T) {
|
func TestANodeRunningAModuleWithNoWantGetsItsFirstWantAtWhatItRuns(t *testing.T) {
|
||||||
m, n := x11()
|
m, n := x11()
|
||||||
out := Wants(m, n, nil, nil)
|
out := Wants(m, n, nil, nil)
|
||||||
for _, node := range []string{"g14", "shanks"} {
|
// 1960: the providers rule holds no want: the canary g14 gets F, its provider's health unknown said
|
||||||
if w := out.Wants[node]; w.FP != fOld || w.Composed != dig(fOld) || w.Newest != fNew || w.Rule == 0 {
|
if w := out.Wants["g14"]; w.FP != fNew || out.Unreached["g14"] == "" {
|
||||||
t.Errorf("%s's want = %+v, want its first want at the fingerprint it runs, held back from F", node, w)
|
t.Errorf("g14's want = %+v (unreached %q), want F, the provider said", w, out.Unreached["g14"])
|
||||||
}
|
}
|
||||||
|
if w := out.Wants["shanks"]; w.FP != fOld || w.Composed != dig(fOld) || w.Newest != fNew || w.Rule != delivery.RuleCanary {
|
||||||
|
t.Errorf("shanks's want = %+v, want its first want at the fingerprint it runs, held back from F by the canary", w)
|
||||||
}
|
}
|
||||||
if len(out.Neither) != 0 {
|
if len(out.Neither) != 0 {
|
||||||
t.Errorf("left on neither list: %+v", out.Neither)
|
t.Errorf("left on neither list: %+v", out.Neither)
|
||||||
@@ -1003,11 +1046,22 @@ func TestNoFirstWantWithoutTheCompositionAndGrantOfWhatItRuns(t *testing.T) {
|
|||||||
m, n := x11()
|
m, n := x11()
|
||||||
change(&m, &n)
|
change(&m, &n)
|
||||||
out := Wants(m, n, nil, nil)
|
out := Wants(m, n, nil, nil)
|
||||||
if w, ok := out.Wants["shanks"]; ok {
|
if what == "an old-form build" {
|
||||||
t.Errorf("%s: shanks got a want: %+v", what, w)
|
// the canary g14 does not run F healthy: no want, on neither list by rule 3 (issue 1945)
|
||||||
|
if w, ok := out.Wants["shanks"]; ok {
|
||||||
|
t.Errorf("%s: shanks got a want: %+v", what, w)
|
||||||
|
}
|
||||||
|
if h, ok := out.Neither["shanks"]; !ok || h.Rule != delivery.RuleCanary {
|
||||||
|
t.Errorf("%s: shanks on neither list = %+v (named %v), want rule 3", what, h, ok)
|
||||||
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if h, ok := out.Neither["shanks"]; !ok || h.Rule == 0 {
|
// 1960: no want can name what it runs, so its first want names F rather than none: never on neither list
|
||||||
t.Errorf("%s: shanks on neither list = %+v (named %v)", what, h, ok)
|
if w := out.Wants["shanks"]; w.FP != fNew {
|
||||||
|
t.Errorf("%s: shanks's want = %+v, want F", what, w)
|
||||||
|
}
|
||||||
|
if h, ok := out.Neither["shanks"]; ok {
|
||||||
|
t.Errorf("%s: shanks on neither list = %+v", what, h)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m, n := x11()
|
m, n := x11()
|
||||||
@@ -1016,10 +1070,12 @@ func TestNoFirstWantWithoutTheCompositionAndGrantOfWhatItRuns(t *testing.T) {
|
|||||||
if len(out.Neither) != 0 {
|
if len(out.Neither) != 0 {
|
||||||
t.Errorf("nodes holding a want named on neither list: %+v", out.Neither)
|
t.Errorf("nodes holding a want named on neither list: %+v", out.Neither)
|
||||||
}
|
}
|
||||||
for node, w := range out.Wants {
|
// the canary g14 moves to F (its provider's health no rule); shanks waits for it
|
||||||
if w.FP != fOld {
|
if w := out.Wants["shanks"]; w.FP != "" && w.FP != fOld {
|
||||||
t.Errorf("%s's want moved from its want past the ordering rules: %+v", node, w)
|
t.Errorf("shanks's want moved from its want past the canary: %+v", w)
|
||||||
}
|
}
|
||||||
|
if w := out.Wants["g14"]; w.FP != fNew {
|
||||||
|
t.Errorf("the canary g14's want = %+v, want F", w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1030,11 +1086,12 @@ func TestANewAssignmentWaitingIsNotOnNeitherList(t *testing.T) {
|
|||||||
m, n := x11()
|
m, n := x11()
|
||||||
delete(n.Haves["shanks"], "screen-lock")
|
delete(n.Haves["shanks"], "screen-lock")
|
||||||
out := Wants(m, n, nil, nil)
|
out := Wants(m, n, nil, nil)
|
||||||
if w, ok := out.Wants["shanks"]; ok {
|
// 1960: the canary orders moves, never a first want: the new assignment is wanted at F
|
||||||
t.Errorf("a new assignment waiting for the canary got a want: %+v", w)
|
if w := out.Wants["shanks"]; w.FP != fNew {
|
||||||
|
t.Errorf("a new assignment while the canary does not run F: %+v, want F", w)
|
||||||
}
|
}
|
||||||
if _, ok := out.Neither["shanks"]; ok {
|
if _, ok := out.Neither["shanks"]; ok {
|
||||||
t.Errorf("a new assignment waiting for the canary is named on neither list: %+v", out.Neither)
|
t.Errorf("a new assignment is named on neither list: %+v", out.Neither)
|
||||||
}
|
}
|
||||||
m.Built = false
|
m.Built = false
|
||||||
if out := Wants(m, n, nil, nil); len(out.Neither) != 1 || out.Neither["g14"].Rule != delivery.RuleBuildDone {
|
if out := Wants(m, n, nil, nil); len(out.Neither) != 1 || out.Neither["g14"].Rule != delivery.RuleBuildDone {
|
||||||
@@ -1076,7 +1133,7 @@ func TestANewAssignmentWaitingIsNotOnNeitherList(t *testing.T) {
|
|||||||
// 1945/AC21: a node whose have names an old-form build (no fingerprint of the new form, so no composition of it) can never be
|
// 1945/AC21: a node whose have names an old-form build (no fingerprint of the new form, so no composition of it) can never be
|
||||||
// named in a want at what it runs; its first want names F, under the canary rule as usual and with everything F needs and the
|
// named in a want at what it runs; its first want names F, under the canary rule as usual and with everything F needs and the
|
||||||
// providers rule. Given the canary runs F healthy, shanks's first want names F; given it does not yet, shanks gets no want and
|
// providers rule. Given the canary runs F healthy, shanks's first want names F; given it does not yet, shanks gets no want and
|
||||||
// stays on neither list. The old-form node that is itself the canary gets F once its providers run healthy.
|
// stays on neither list; the providers hold no want (issue 1960). The old-form node that is itself the canary gets F once its providers run healthy.
|
||||||
func TestAnOldFormNodeGetsItsFirstWantAtFOnlyUnderTheCanary(t *testing.T) {
|
func TestAnOldFormNodeGetsItsFirstWantAtFOnlyUnderTheCanary(t *testing.T) {
|
||||||
oldForm := func(n Nodes, node string) {
|
oldForm := func(n Nodes, node string) {
|
||||||
n.Haves[node]["screen-lock"] = delivery.Have{FP: "build-1792260876956849831", Phase: delivery.PhaseRunning, Health: delivery.HealthUnknown}
|
n.Haves[node]["screen-lock"] = delivery.Have{FP: "build-1792260876956849831", Phase: delivery.PhaseRunning, Health: delivery.HealthUnknown}
|
||||||
@@ -1110,16 +1167,16 @@ func TestAnOldFormNodeGetsItsFirstWantAtFOnlyUnderTheCanary(t *testing.T) {
|
|||||||
if h, ok := out.Neither["shanks"]; !ok || h.Rule != delivery.RuleCanary {
|
if h, ok := out.Neither["shanks"]; !ok || h.Rule != delivery.RuleCanary {
|
||||||
t.Errorf("canary not healthy on F: shanks on neither list = %+v (named %v), want rule 3", h, ok)
|
t.Errorf("canary not healthy on F: shanks on neither list = %+v (named %v), want rule 3", h, ok)
|
||||||
}
|
}
|
||||||
// the canary healthy on F but the providers not healthy on shanks: rule 5 still holds it back
|
// 1960: the canary healthy on F but the providers not healthy on shanks: F all the same, the provider down said
|
||||||
m, n = x11()
|
m, n = x11()
|
||||||
oldForm(n, "shanks")
|
oldForm(n, "shanks")
|
||||||
n.Haves["g14"]["screen-lock"] = delivery.Have{FP: fNew, Phase: delivery.PhaseRunning, Health: delivery.HealthOK}
|
n.Haves["g14"]["screen-lock"] = delivery.Have{FP: fNew, Phase: delivery.PhaseRunning, Health: delivery.HealthOK}
|
||||||
out = Wants(m, n, nil, nil)
|
out = Wants(m, n, nil, nil)
|
||||||
if w, ok := out.Wants["shanks"]; ok {
|
if w := out.Wants["shanks"]; w.FP != fNew || out.Unreached["shanks"] == "" {
|
||||||
t.Errorf("providers not healthy: shanks got a want: %+v", w)
|
t.Errorf("providers not healthy: shanks's want = %+v (unreached %q), want F, the provider said", w, out.Unreached["shanks"])
|
||||||
}
|
}
|
||||||
if h := out.Neither["shanks"]; h.Rule != delivery.RuleProviders {
|
if _, on := out.Neither["shanks"]; on {
|
||||||
t.Errorf("providers not healthy: shanks on neither list = %+v, want rule 5", h)
|
t.Errorf("providers not healthy: shanks on neither list: %+v", out.Neither)
|
||||||
}
|
}
|
||||||
// F not granted: nothing, even with the canary healthy
|
// F not granted: nothing, even with the canary healthy
|
||||||
m, n = x11()
|
m, n = x11()
|
||||||
|
|||||||
@@ -94,6 +94,34 @@ func Tools(r build.StuckReader, askAgain func(key string, value []byte) error, q
|
|||||||
return map[string]any{"key": key, "revision": r.Revision, "asks": r.Asks, "answer": r.Answer,
|
return map[string]any{"key": key, "revision": r.Revision, "asks": r.Asks, "answer": r.Answer,
|
||||||
"next": build.StuckTool + " shows the key and its asks while they wait"}, nil
|
"next": build.StuckTool + " shows the key and its asks while they wait"}, nil
|
||||||
}},
|
}},
|
||||||
|
{Name: build.AnnounceAgainTool,
|
||||||
|
Description: "Announce a done build again (take in again, compose again): the builder writes the done key of module and fingerprint " +
|
||||||
|
"again, unchanged, as a new revision of the same key (its content, artifacts and manifest stay identical), so the controller's " +
|
||||||
|
"take-in hears it and composes the module and writes its grant again. Use it when a done build was taken in but its composition " +
|
||||||
|
"or grant is gone and every want is refused for it. Answers the new revision. Refuses a key that is not done: " + build.BuildAgainTool +
|
||||||
|
" builds a failed key again, " + build.AskAgainTool + " asks a waiting key's asks again.",
|
||||||
|
Input: map[string]any{"type": "object", "required": []string{"module", "fp", "why"}, "properties": map[string]any{
|
||||||
|
"module": map[string]any{"type": "string", "description": "the module's name"},
|
||||||
|
"fp": map[string]any{"type": "string", "description": "its new-form fingerprint, src2:<hex>"},
|
||||||
|
"why": map[string]any{"type": "string", "description": "why it is announced again, in plain words"}}},
|
||||||
|
Run: func(args map[string]any) (any, error) {
|
||||||
|
key, value, err := build.NewAnnounceAgain(str(args, "module"), str(args, "fp"), str(args, "why"), stdio.CallerOf(args), now())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
raw, err := buildAgain(key, value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r, err := build.DecodeReAsk(raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if r.Refused || r.Revision == 0 {
|
||||||
|
return nil, errors.New(r.Answer)
|
||||||
|
}
|
||||||
|
return map[string]any{"key": key, "revision": r.Revision, "answer": r.Answer}, nil
|
||||||
|
}},
|
||||||
{Name: build.SeatQueueTool,
|
{Name: build.SeatQueueTool,
|
||||||
Description: "What waits on a build seat's stream (SEAT_NODE_BUILD_<LANGUAGE>), as the builder reads it: the stream's message count and " +
|
Description: "What waits on a build seat's stream (SEAT_NODE_BUILD_<LANGUAGE>), as the builder reads it: the stream's message count and " +
|
||||||
"sequences, and per kind (delivery, check, recheck) its messages oldest first, at most limit each, with sequence, ask id, when asked " +
|
"sequences, and per kind (delivery, check, recheck) its messages oldest first, at most limit each, with sequence, ask id, when asked " +
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
type askAgainer interface {
|
type askAgainer interface {
|
||||||
AskAgain(key, why string) []string
|
AskAgain(key, why string) []string
|
||||||
BuildAgain(ctx context.Context, module, fp, why, by string, bound time.Duration) build.BuildAgainResult
|
BuildAgain(ctx context.Context, module, fp, why, by string, bound time.Duration) build.BuildAgainResult
|
||||||
|
AnnounceAgain(ctx context.Context, module, fp, why, by string) build.BuildAgainResult
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildAgainBound is how long the builder waits for the claim and the asks of a key built again by hand; the tool waits
|
// buildAgainBound is how long the builder waits for the claim and the asks of a key built again by hand; the tool waits
|
||||||
@@ -45,6 +46,10 @@ func reAsked(ctx context.Context, b askAgainer, kv jetstream.KeyValue, e jetstre
|
|||||||
go builtAgain(ctx, b, kv, e, r, logf)
|
go builtAgain(ctx, b, kv, e, r, logf)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if r.Mode == build.ModeAnnounce {
|
||||||
|
announcedAgain(ctx, b, kv, e, r, logf)
|
||||||
|
return
|
||||||
|
}
|
||||||
asked := b.AskAgain(e.Key(), r.Why)
|
asked := b.AskAgain(e.Key(), r.Why)
|
||||||
if len(asked) == 0 {
|
if len(asked) == 0 {
|
||||||
r.Answer = "no ask of this key waits in this builder now (it is not building, or its build has not reached its ask yet): nothing was asked; builder_stuck shows where it stands"
|
r.Answer = "no ask of this key waits in this builder now (it is not building, or its build has not reached its ask yet): nothing was asked; builder_stuck shows where it stands"
|
||||||
@@ -114,3 +119,22 @@ func builtAgain(ctx context.Context, b askAgainer, kv jetstream.KeyValue, e jets
|
|||||||
logf("%s.%s: the answer could not be written: %v", build.ReAsksState, e.Key(), err)
|
logf("%s.%s: the answer could not be written: %v", build.ReAsksState, e.Key(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// announcedAgain answers a reasks value of mode announce (the tool builder_announce_again): the done key is written again
|
||||||
|
// unchanged, and the value is written back with the new revision, or why nothing was done.
|
||||||
|
func announcedAgain(ctx context.Context, b askAgainer, kv jetstream.KeyValue, e jetstream.KeyValueEntry, r build.ReAsk, logf func(string, ...any)) {
|
||||||
|
res := b.AnnounceAgain(ctx, r.Module, r.FP, r.Why, r.By)
|
||||||
|
if res.Refused != "" {
|
||||||
|
r.Answer, r.Refused = res.Refused, true
|
||||||
|
} else {
|
||||||
|
r.Revision, r.Answer = res.Revision, fmt.Sprintf("announced again: the done key is written again, unchanged, as revision %d", res.Revision)
|
||||||
|
}
|
||||||
|
r.Answered = time.Now().UTC()
|
||||||
|
data, err := json.Marshal(r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := kv.Update(ctx, e.Key(), data, e.Revision()); err != nil && !errors.Is(err, jetstream.ErrKeyExists) && !errors.Is(err, jetstream.ErrKeyRevisionMismatch) {
|
||||||
|
logf("%s.%s: the answer could not be written: %v", build.ReAsksState, e.Key(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module mesh-delivery-builder
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028193502-f91340fe82ef
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771
|
||||||
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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028193502-f91340fe82ef h1:ggwHXCvAOhBCKAY+ErehKEhu/IQTS4poJBRDokGYnDc=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771 h1:Tt16HJk8JZRce1i3JzzDSNdCWOrv7dw8FQ/bckNQ5Ig=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028193502-f91340fe82ef/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771/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,53 @@
|
|||||||
|
package build
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
|
)
|
||||||
|
|
||||||
|
// **A done build is announced again by hand** (recovery of photos and link2pay on novox, 2026-10-29). The controller takes a
|
||||||
|
// build in when a write of the build key reaches its take-in consumer, and composes then; a done key is never written again
|
||||||
|
// by the builder, so a composition or a grant lost afterwards could not be made again by any tool. AnnounceAgain writes the
|
||||||
|
// done key again with its own bytes, as a new revision of the same key: its content, state, artifacts and manifest stay
|
||||||
|
// identical, and only the write is new. Who asked and why is in the builder's log and in the reasks answer.
|
||||||
|
|
||||||
|
// AnnounceAgain rewrites the done key of module at fp unchanged, as a new revision, and answers the revision. A key that is
|
||||||
|
// not done is refused with why: failed (builder_build_again builds it) or waiting (builder_ask_again).
|
||||||
|
func (b *Builder) AnnounceAgain(ctx context.Context, module, fp, why, by string) BuildAgainResult {
|
||||||
|
b.init()
|
||||||
|
key, err := delivery.BuilderBuildKey(module, fp)
|
||||||
|
if err != nil {
|
||||||
|
return BuildAgainResult{Refused: err.Error()}
|
||||||
|
}
|
||||||
|
value, rev, found, err := b.Claimer.Store.Get(ctx, key)
|
||||||
|
if err != nil {
|
||||||
|
return BuildAgainResult{Refused: fmt.Sprintf("%s cannot be read: %v", key, err)}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return BuildAgainResult{Refused: fmt.Sprintf("%s has no build key: nothing to announce", key)}
|
||||||
|
}
|
||||||
|
cur, err := delivery.DecodeBuilderBuild(value)
|
||||||
|
if err != nil {
|
||||||
|
return BuildAgainResult{Refused: fmt.Sprintf("%s holds what is not a build: %v", key, err)}
|
||||||
|
}
|
||||||
|
switch cur.State {
|
||||||
|
case delivery.BuildDone:
|
||||||
|
case delivery.BuildFailed:
|
||||||
|
return BuildAgainResult{Refused: fmt.Sprintf("%s is failed, not done: %s builds it again", key, BuildAgainTool)}
|
||||||
|
default:
|
||||||
|
return BuildAgainResult{Refused: fmt.Sprintf("%s is %q, not done: %s asks its waiting asks again", key, cur.State, AskAgainTool)}
|
||||||
|
}
|
||||||
|
// Compare-and-set at the revision read: a build written meanwhile is not overwritten.
|
||||||
|
nrev, err := b.Claimer.Store.Update(ctx, key, value, rev)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrConflict) {
|
||||||
|
return BuildAgainResult{Refused: fmt.Sprintf("%s changed while it was announced: read it again", key)}
|
||||||
|
}
|
||||||
|
return BuildAgainResult{Refused: fmt.Sprintf("%s could not be written: %v", key, err)}
|
||||||
|
}
|
||||||
|
b.logf("%s: done, announced again by %s (revision %d): %s", key, by, nrev, why)
|
||||||
|
return BuildAgainResult{Revision: nrev}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package build
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAnnounceAgainRewritesOnlyADoneKeyUnchanged(t *testing.T) { // recovery of photos and link2pay, 2026-10-29
|
||||||
|
for _, c := range []struct {
|
||||||
|
name string
|
||||||
|
key *delivery.BuilderBuild
|
||||||
|
fp string
|
||||||
|
refuse string
|
||||||
|
}{
|
||||||
|
{name: "a done key is written again, unchanged, as a new revision", key: &delivery.BuilderBuild{State: delivery.BuildDone, Step: "built"}},
|
||||||
|
{name: "a failed key is refused, builder_build_again is for it", refuse: "builder_build_again", key: &delivery.BuilderBuild{State: delivery.BuildFailed, Error: "x"}},
|
||||||
|
{name: "a building key is refused, builder_ask_again is for it", refuse: "builder_ask_again", key: &delivery.BuilderBuild{State: delivery.BuildBuilding, By: "novox.builder", Step: "asked"}},
|
||||||
|
{name: "a key with no build is refused", refuse: "has no build key"},
|
||||||
|
{name: "a fingerprint that is not one is refused", refuse: "fingerprint", fp: "nope"},
|
||||||
|
} {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
r := newRig(t, goBundle)
|
||||||
|
var rev uint64
|
||||||
|
if c.key != nil {
|
||||||
|
c.key.Manifest = manifestD
|
||||||
|
rev = putBuild(t, r.st, r.key, *c.key)
|
||||||
|
}
|
||||||
|
before := bytes.Clone(r.st.vals[r.key])
|
||||||
|
fp := fp1
|
||||||
|
if c.fp != "" {
|
||||||
|
fp = c.fp
|
||||||
|
}
|
||||||
|
res := r.b.AnnounceAgain(context.Background(), "m", fp, "grant lost", "shanks.lead")
|
||||||
|
if c.refuse != "" {
|
||||||
|
if !strings.Contains(res.Refused, c.refuse) || res.Revision != 0 || r.st.revs[r.key] != rev || !bytes.Equal(r.st.vals[r.key], before) {
|
||||||
|
t.Fatalf("answer %+v, key at revision %d (was %d); want refused with %q and nothing written", res, r.st.revs[r.key], rev, c.refuse)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if res.Refused != "" || res.Revision <= rev || r.st.revs[r.key] != res.Revision || !bytes.Equal(r.st.vals[r.key], before) {
|
||||||
|
t.Fatalf("answer %+v, key at revision %d (was %d); want a new revision and the same bytes", res, r.st.revs[r.key], rev)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,9 @@ const (
|
|||||||
AskAgainTool = "builder_ask_again"
|
AskAgainTool = "builder_ask_again"
|
||||||
// BuildAgainTool builds a failed key again by hand (issue 1126 note 72): it writes a reasks value of Mode ModeBuild.
|
// BuildAgainTool builds a failed key again by hand (issue 1126 note 72): it writes a reasks value of Mode ModeBuild.
|
||||||
BuildAgainTool = "builder_build_again"
|
BuildAgainTool = "builder_build_again"
|
||||||
|
// AnnounceAgainTool writes a done key again unchanged, so the controller takes it in and composes again: a reasks value
|
||||||
|
// of Mode ModeAnnounce.
|
||||||
|
AnnounceAgainTool = "builder_announce_again"
|
||||||
// ReAsksState is the builder's state of asks to ask again: the bucket mesh-delivery-builder_reasks, keyed as the build key.
|
// ReAsksState is the builder's state of asks to ask again: the bucket mesh-delivery-builder_reasks, keyed as the build key.
|
||||||
ReAsksState = "reasks"
|
ReAsksState = "reasks"
|
||||||
ReAsksBucket = delivery.BuilderModule + "_" + ReAsksState
|
ReAsksBucket = delivery.BuilderModule + "_" + ReAsksState
|
||||||
@@ -56,6 +59,9 @@ type ReAsk struct {
|
|||||||
// ModeBuild is the Mode of a reasks value that asks the key to be built again, not its waiting asks to be asked again.
|
// ModeBuild is the Mode of a reasks value that asks the key to be built again, not its waiting asks to be asked again.
|
||||||
const ModeBuild = "build"
|
const ModeBuild = "build"
|
||||||
|
|
||||||
|
// ModeAnnounce is the Mode of a reasks value that asks a done key to be announced again.
|
||||||
|
const ModeAnnounce = "announce"
|
||||||
|
|
||||||
// DecodeReAsk reads a reasks value.
|
// DecodeReAsk reads a reasks value.
|
||||||
func DecodeReAsk(data []byte) (ReAsk, error) {
|
func DecodeReAsk(data []byte) (ReAsk, error) {
|
||||||
var r ReAsk
|
var r ReAsk
|
||||||
@@ -94,6 +100,19 @@ func NewBuildAgain(module, fp, why, by string, at time.Time) (string, []byte, er
|
|||||||
return key, data, err
|
return key, data, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewAnnounceAgain is the value builder_announce_again writes for a module's fingerprint: its key and the value.
|
||||||
|
func NewAnnounceAgain(module, fp, why, by string, at time.Time) (string, []byte, error) {
|
||||||
|
key, err := delivery.BuilderBuildKey(module, fp)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(why) == "" {
|
||||||
|
return "", nil, errors.New("say why the done build is announced again (why)")
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(ReAsk{V: 1, Why: strings.TrimSpace(why), By: by, At: at.UTC(), Mode: ModeAnnounce, Module: module, FP: fp})
|
||||||
|
return key, data, err
|
||||||
|
}
|
||||||
|
|
||||||
// StateRead is a state the tools read: its keys, and a key's value with when it was written (zero when not known).
|
// StateRead is a state the tools read: its keys, and a key's value with when it was written (zero when not known).
|
||||||
type StateRead struct {
|
type StateRead struct {
|
||||||
Keys func() ([]string, error)
|
Keys func() ([]string, error)
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
|
|||||||
t.Errorf("secrets-owner = %q", m.SecretsOwner)
|
t.Errorf("secrets-owner = %q", m.SecretsOwner)
|
||||||
}
|
}
|
||||||
// 1945: the builder's tools (builder_seat_queue: note 62), served by its tools bundle, and the event it says when it asks an ask again.
|
// 1945: the builder's tools (builder_seat_queue: note 62), served by its tools bundle, and the event it says when it asks an ask again.
|
||||||
if want := []string{"builder_ask_again", "builder_build_again", "builder_seat_queue", "builder_stuck"}; !reflect.DeepEqual(sorted(m.Tools), want) {
|
if want := []string{"builder_announce_again", "builder_ask_again", "builder_build_again", "builder_seat_queue", "builder_stuck"}; !reflect.DeepEqual(sorted(m.Tools), want) {
|
||||||
t.Errorf("tools = %v, want %v", m.Tools, want)
|
t.Errorf("tools = %v, want %v", m.Tools, want)
|
||||||
}
|
}
|
||||||
if want := []string{"build.asked-again"}; !reflect.DeepEqual(m.Emits, want) {
|
if want := []string{"build.asked-again"}; !reflect.DeepEqual(m.Emits, want) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"version": "1",
|
"version": "1",
|
||||||
"slug": "builder",
|
"slug": "builder",
|
||||||
"tools": [
|
"tools": [
|
||||||
|
"builder_announce_again",
|
||||||
"builder_ask_again",
|
"builder_ask_again",
|
||||||
"builder_build_again",
|
"builder_build_again",
|
||||||
"builder_seat_queue",
|
"builder_seat_queue",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module mesh-delivery-checker
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028193502-f91340fe82ef
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771
|
||||||
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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028193502-f91340fe82ef h1:ggwHXCvAOhBCKAY+ErehKEhu/IQTS4poJBRDokGYnDc=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771 h1:Tt16HJk8JZRce1i3JzzDSNdCWOrv7dw8FQ/bckNQ5Ig=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028193502-f91340fe82ef/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771/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=
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module mesh-delivery-indexer
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771
|
||||||
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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388 h1:ycu/sNieTdLpD3bzhZOCXdzcoqXUOxPRmqWV019REpo=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771 h1:Tt16HJk8JZRce1i3JzzDSNdCWOrv7dw8FQ/bckNQ5Ig=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771/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=
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module mesh-grants
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028191251-5228a5782d5c
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028191251-5228a5782d5c h1:ngcItoLuh2uP24toZvkCXGA/3+TBrLTwTbE28XpYDWY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771 h1:Tt16HJk8JZRce1i3JzzDSNdCWOrv7dw8FQ/bckNQ5Ig=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028191251-5228a5782d5c/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771/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=
|
||||||
|
|||||||
Reference in New Issue
Block a user