Give each asking of a merge ask its own id, and add the have phase removed (issues 1370, 1369)
mesh/merge-gate head ba684e54976b merged into trunk 5786ba190c69 — pass: group fix/r3e-before-the-gate composed: every machine composes with the change as …
mesh/repo-check head ba684e54976b merged into trunk 5786ba190c69 beside mesh-host#121@150fadfe — pass: its merge-check.sh passed, each part in its toolchai…
mesh/delivery delivered
mesh/delivery-group group fix/r3e-before-the-gate delivered: every member is delivered
mesh/merge-gate head ba684e54976b merged into trunk 5786ba190c69 — pass: group fix/r3e-before-the-gate composed: every machine composes with the change as …
mesh/repo-check head ba684e54976b merged into trunk 5786ba190c69 beside mesh-host#121@150fadfe — pass: its merge-check.sh passed, each part in its toolchai…
mesh/delivery delivered
mesh/delivery-group group fix/r3e-before-the-gate delivered: every member is delivered
The router drops an ask under an id it keeps as a redelivery, so a gate that writes the same group's ask again at unchanged heads was never heard. Ask gains Asking; AskingID gives the first asking the old id and later ones their own. PhaseRemoved lets the node-engine say a module left its node.
This commit is contained in:
@@ -80,6 +80,10 @@ const (
|
|||||||
PhaseChecking Phase = "checking"
|
PhaseChecking Phase = "checking"
|
||||||
PhaseRunning Phase = "running"
|
PhaseRunning Phase = "running"
|
||||||
PhaseFailed Phase = "failed"
|
PhaseFailed Phase = "failed"
|
||||||
|
// PhaseRemoved is written by the node-engine, once, for a module its applied declaration no longer holds and
|
||||||
|
// for which it had written a have key; the key is never deleted (issue 1369, ADR 0408 §4), so a reader can tell
|
||||||
|
// that the module stopped.
|
||||||
|
PhaseRemoved Phase = "removed"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Health is what the module's own check says.
|
// Health is what the module's own check says.
|
||||||
@@ -338,9 +342,9 @@ func decode(data []byte, into any) error {
|
|||||||
|
|
||||||
func (h Have) check() error {
|
func (h Have) check() error {
|
||||||
switch h.Phase {
|
switch h.Phase {
|
||||||
case PhaseBackingUp, PhaseApplying, PhaseChecking, PhaseRunning, PhaseFailed:
|
case PhaseBackingUp, PhaseApplying, PhaseChecking, PhaseRunning, PhaseFailed, PhaseRemoved:
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("delivery: phase %q is not backing-up, applying, checking, running or failed", h.Phase)
|
return fmt.Errorf("delivery: phase %q is not backing-up, applying, checking, running, failed or removed", h.Phase)
|
||||||
}
|
}
|
||||||
switch h.Health {
|
switch h.Health {
|
||||||
case HealthOK, HealthFailed, HealthWaits, HealthUnknown:
|
case HealthOK, HealthFailed, HealthWaits, HealthUnknown:
|
||||||
|
|||||||
@@ -292,3 +292,19 @@ func TestSame(t *testing.T) {
|
|||||||
t.Error("different pulses same")
|
t.Error("different pulses same")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHaveRemoved(t *testing.T) { // 1370/AC3
|
||||||
|
in := Have{Build: "b1", Phase: PhaseRemoved, Health: HealthUnknown, Detail: "the declaration no longer holds it"}
|
||||||
|
b, err := EncodeHave(in)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
out, err := DecodeHave(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
in.V = Version
|
||||||
|
if out != in || out.Phase != "removed" {
|
||||||
|
t.Errorf("round trip: %+v", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+21
-2
@@ -119,6 +119,18 @@ func AskID(askKey string) string {
|
|||||||
return string(readable) + "-" + hex.EncodeToString(sum[:])[:12]
|
return string(readable) + "-" + hex.EncodeToString(sum[:])[:12]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AskingID is the id of the asking-th writing of the ask at askKey. Asking 0 and 1 give AskID(askKey), so the first
|
||||||
|
// asking's id is unchanged; asking n of 2 or more gives the AskID form of askKey + "." + n, so each asking has an id
|
||||||
|
// of its own (at most 64 bytes, ending in the 12 hex digits of the hash of that whole string) and the router, which
|
||||||
|
// drops an ask under an id it keeps as a redelivery, takes each as new and an answer to an earlier asking is never
|
||||||
|
// taken for a later one.
|
||||||
|
func AskingID(askKey string, asking int) string {
|
||||||
|
if asking < 2 {
|
||||||
|
return AskID(askKey)
|
||||||
|
}
|
||||||
|
return AskID(askKey + "." + strconv.Itoa(asking))
|
||||||
|
}
|
||||||
|
|
||||||
func checkOwedAct(act string) (kind, context string, err error) {
|
func checkOwedAct(act string) (kind, context string, err error) {
|
||||||
kind, context, hasContext := strings.Cut(act, "=")
|
kind, context, hasContext := strings.Cut(act, "=")
|
||||||
switch kind {
|
switch kind {
|
||||||
@@ -274,15 +286,22 @@ type Ask struct {
|
|||||||
Group string `json:"group"`
|
Group string `json:"group"`
|
||||||
Heads []Head `json:"heads"`
|
Heads []Head `json:"heads"`
|
||||||
Ask asks.Ask `json:"ask"`
|
Ask asks.Ask `json:"ask"`
|
||||||
|
// Asking counts the times the gate wrote this group's ask at these heads: 0 or 1 for the first, n for the n-th.
|
||||||
|
// The router drops an ask whose id it has already kept as a redelivery, so each asking has its own id
|
||||||
|
// (AskingID); the first asking's id is unchanged and the field is omitted for it.
|
||||||
|
Asking int `json:"asking,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Ask) check() error {
|
func (a Ask) check() error {
|
||||||
|
if a.Asking < 0 {
|
||||||
|
return fmt.Errorf("delivery: the ask's asking %d is negative", a.Asking)
|
||||||
|
}
|
||||||
key, err := MergeAskKey(a.Group, a.Heads)
|
key, err := MergeAskKey(a.Group, a.Heads)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("delivery: an ask needs its group and at least one pull request's head: %w", err)
|
return fmt.Errorf("delivery: an ask needs its group and at least one pull request's head: %w", err)
|
||||||
}
|
}
|
||||||
if want := AskID(key); a.Ask.ID != want {
|
if want := AskingID(key, a.Asking); a.Ask.ID != want {
|
||||||
return fmt.Errorf("delivery: the ask's id %q is not %q, the id its group and heads give", a.Ask.ID, want)
|
return fmt.Errorf("delivery: the ask's id %q is not %q, the id its group, heads and asking %d give", a.Ask.ID, want, a.Asking)
|
||||||
}
|
}
|
||||||
if len(a.Ask.Options) == 0 {
|
if len(a.Ask.Options) == 0 {
|
||||||
return fmt.Errorf("delivery: the ask has no option")
|
return fmt.Errorf("delivery: the ask has no option")
|
||||||
|
|||||||
@@ -508,3 +508,67 @@ func TestOwedCommentsHaveASequence(t *testing.T) { // 1322/AC3
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAskingID(t *testing.T) { // 1370/AC1
|
||||||
|
k, _ := MergeAskKey("feat/x", gateHeads())
|
||||||
|
long, _ := MergeAskKey("feat/"+strings.Repeat("long-", 20), gateHeads())
|
||||||
|
for _, key := range []string{k, long} {
|
||||||
|
if AskingID(key, 1) != AskID(key) || AskingID(key, 0) != AskID(key) {
|
||||||
|
t.Errorf("the first asking's id is not AskID: %q %q %q", AskingID(key, 0), AskingID(key, 1), AskID(key))
|
||||||
|
}
|
||||||
|
seen := map[string]int{AskID(key): 1}
|
||||||
|
for n := 2; n <= 12; n++ {
|
||||||
|
id := AskingID(key, n)
|
||||||
|
if other, dup := seen[id]; dup {
|
||||||
|
t.Errorf("askings %d and %d give the id %q", n, other, id)
|
||||||
|
}
|
||||||
|
seen[id] = n
|
||||||
|
if len(id) > 64 || !asks.UsableID(id) {
|
||||||
|
t.Errorf("id %q of asking %d is not usable by the router", id, n)
|
||||||
|
}
|
||||||
|
if id != AskID(key+"."+strconv.Itoa(n)) {
|
||||||
|
t.Errorf("id of asking %d is not the AskID form of key.%d: %q", n, n, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAskChecksItsAsking(t *testing.T) { // 1370/AC2
|
||||||
|
key, _ := MergeAskKey("feat/x", gateHeads())
|
||||||
|
a := sampleAsk()
|
||||||
|
if _, err := EncodeAsk(a); err != nil {
|
||||||
|
t.Fatalf("asking 0: %v", err)
|
||||||
|
}
|
||||||
|
a.Asking = 1
|
||||||
|
if _, err := EncodeAsk(a); err != nil {
|
||||||
|
t.Fatalf("asking 1 under the first id: %v", err)
|
||||||
|
}
|
||||||
|
a.Asking = 3
|
||||||
|
a.Ask.ID = AskingID(key, 3)
|
||||||
|
data, err := EncodeAsk(a)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("asking 3 under its id: %v", err)
|
||||||
|
}
|
||||||
|
a.V = Version
|
||||||
|
if got, err := DecodeAsk(data); err != nil || !SameAsk(got, a) || got.Asking != 3 {
|
||||||
|
t.Errorf("asking round trip: %+v %v", got, err)
|
||||||
|
}
|
||||||
|
a.Ask.ID = AskingID(key, 2)
|
||||||
|
if _, err := EncodeAsk(a); err == nil {
|
||||||
|
t.Error("asking 3 under asking 2's id was accepted")
|
||||||
|
}
|
||||||
|
a.Asking = 2
|
||||||
|
a.Ask.ID = AskID(key)
|
||||||
|
if _, err := EncodeAsk(a); err == nil {
|
||||||
|
t.Error("asking 2 under the first asking's id was accepted")
|
||||||
|
}
|
||||||
|
a.Asking = -1
|
||||||
|
if _, err := EncodeAsk(a); err == nil {
|
||||||
|
t.Error("a negative asking was accepted")
|
||||||
|
}
|
||||||
|
// the first asking is written without the field
|
||||||
|
data, _ = EncodeAsk(sampleAsk())
|
||||||
|
if strings.Contains(string(data), `"asking"`) {
|
||||||
|
t.Errorf("asking 0 is written: %s", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user