Compare commits

..
Author SHA1 Message Date
jschoubben 0d97c09c49 Fold the retake test into the prefix test (issue 1748 review 1)
mesh/delivery-group group fix/1748-retake-and-resolver ready: every member ready, and composed together they pass
mesh/delivery closed: the pull request closed unmerged
mesh/check pass 0d97c09c49b4 on 5a38a1221659 | unit 6409 run, 187 kept, 14 skipped | integration 6544 run, 220 kept, 23 skipped | over bound: unit, in…
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 22:41:49 +01:00
jschoubben 9c4b2c5e56 A build's log entry says how many times its key was retaken (issue 1748)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 22:27:52 +01:00
jschoubben 0261881feb Add the retake class beside FailedUntilChanged and the retake count on a build key (issue 1748)
A network failure passes by itself; the build contract names it so every build module is held to it and the builder acts on it.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 22:27:00 +01:00
5 changed files with 54 additions and 32 deletions
+15
View File
@@ -150,6 +150,11 @@ const (
// WaitsFor: a fetch found the module's lock naming a package version the registry does not hold yet; the builder
// writes the key queued with Waits (novox/hq ADR 0500 clause 6), and it is built once the package is published.
WaitsFor = "waits for: "
// Retake: a failure that passes by itself (a name lookup that timed out, a refused connection): the builder takes
// the key again after a backoff, at most MaxRetakes times (issue 1748). A build module opens every such failure with it.
Retake = "retakeable: "
// MaxRetakes is how many times the builder takes a key again by itself for a retakeable failure.
MaxRetakes = 3
)
// FailedUntilChanged says whether a failed key's error is one that is not built again until the module's fingerprint
@@ -159,6 +164,12 @@ func FailedUntilChanged(err string) bool {
return strings.HasPrefix(err, NotPinned) || strings.HasPrefix(err, VersionTaken) || strings.HasPrefix(err, RefusedKind)
}
// Retakeable says whether a failed key's error opens with Retake: the builder takes it again by itself after a backoff,
// until BuilderBuild.Retakes reaches MaxRetakes. An error that is FailedUntilChanged, or opens with no prefix, is not.
func Retakeable(err string) bool {
return strings.HasPrefix(err, Retake) && !FailedUntilChanged(err)
}
// ToolchainUsed is what a build used for one language (ADR 0405 clauses 2 and 7): the installed toolchain's version and
// the build module's recipe version.
type ToolchainUsed struct {
@@ -180,6 +191,10 @@ type BuilderBuild struct {
Artifacts []BuiltArtifact `json:"artifacts,omitempty"`
Toolchains map[string]ToolchainUsed `json:"toolchains,omitempty"`
Error string `json:"error,omitempty"`
// Retakes is how many times the builder took this key again by itself after a retakeable failure (Retake); it is kept
// on the key, so a builder that starts again reads it and the count survives a restart (issue 1748). A reader that
// does not know the field reads 0.
Retakes int `json:"retakes,omitempty"`
// Waits is, on a queued key only, the package version `<name>@<version>` the module's lock names and the registry
// does not yet hold: the key is not claimed until that package's trunk key is done and published (novox/hq ADR 0500
// clause 6). A reader that does not know the field reads the key as queued.
+27
View File
@@ -86,11 +86,38 @@ func TestOnlyAPushFailureIsBuiltAgainBeforeTheFingerprintChanges(t *testing.T) {
{VersionTaken + "@novox/mesh-sdk@0.1.15 holds another file", false},
{RefusedKind + "a package in go", false},
{"go test: 2 failed", true},
{Retake + "lookup git.novox.be: i/o timeout", true},
} {
if got := FailedUntilChanged(c.err); got == c.again {
t.Errorf("FailedUntilChanged(%q) = %v", c.err, got)
}
}
// A retakeable error opens with Retake and is not one that waits for the fingerprint; the count is on the key's value (issue 1748).
for _, c := range []struct {
err string
want bool
}{
{Retake + "lookup git.novox.be: i/o timeout", true},
{NotPinned + "x", false},
{Retake[:4] + " not the prefix", false},
{"go test: 2 failed", false},
{"", false},
{VersionTaken + Retake, false},
} {
if got := Retakeable(c.err); got != c.want {
t.Errorf("Retakeable(%q) = %v", c.err, got)
}
}
data, err := EncodeBuilderBuild(BuilderBuild{State: BuildFailed, Error: Retake + "x", Retakes: 2})
if err != nil {
t.Fatal(err)
}
if back, err := DecodeBuilderBuild(data); err != nil || back.Retakes != 2 {
t.Fatalf("decoded %+v, %v", back, err)
}
if Retake != "retakeable: " || MaxRetakes != 3 {
t.Fatal("the retake prefix or its bound changed")
}
if NotStored != "not stored: " || NotPinned != "not pinned: " || VersionTaken != "version taken: " || RefusedKind != "refused kind: " {
t.Fatal("a prefix changed its words")
}
+3
View File
@@ -175,6 +175,9 @@ type BuildLogEntry struct {
// sorted and each once, as the build module answered them (BuildAnswer.Imports; novox/hq ADR 0593 §2): the indexer
// keeps them on the module (KeptInputs2.Imports) when the build's fingerprint is the module's.
Imports []string `json:"imports,omitempty"`
// Retakes is, at a build's end, how many times the builder took the key again by itself after a retakeable failure
// (BuilderBuild.Retakes; issue 1748): builder_builds_since reads it.
Retakes int `json:"retakes,omitempty"`
}
// BuildsSince is the answer of BuildsSinceTool: every ended build since a time, oldest first, and whether the log held
+3 -13
View File
@@ -310,19 +310,9 @@ type ComposeAnswer struct {
// ComposerKey is the key a build module keeps a built composer under: the repository, commit, directory and package it
// was built from and the toolchain it was built with, hashed (`composer-` and 16 hex digits), so each head is built once
// on a node (ADR 0482 clause 4). The mesh package versions the ask names are an input of the build, so they are in the
// key, sorted, when there are any; an ask that names none gets the key it always had (issue 1921).
func ComposerKey(repository, commit, dir, pkg string, toolchain ToolchainUsed, versions PackageVersions) string {
parts := []string{repository, commit, dir, pkg, toolchain.Version, toolchain.Recipe}
pkgs := make([]string, 0, len(versions))
for p := range versions {
pkgs = append(pkgs, p)
}
sort.Strings(pkgs)
for _, p := range pkgs {
parts = append(parts, p+"="+versions[p])
}
sum := sha256.Sum256([]byte(strings.Join(parts, "\n")))
// on a node (ADR 0482 clause 4).
func ComposerKey(repository, commit, dir, pkg string, toolchain ToolchainUsed) string {
sum := sha256.Sum256([]byte(strings.Join([]string{repository, commit, dir, pkg, toolchain.Version, toolchain.Recipe}, "\n")))
return "composer-" + hex.EncodeToString(sum[:8])
}
+6 -19
View File
@@ -56,34 +56,21 @@ func TestAComposeAskIsRefusedWhereItDoesNotBelong(t *testing.T) {
}
}
// 1622/AC3, 1921: a composer is kept under one key per repository, commit, directory, package, toolchain and asked versions, so two asks of
// 1622/AC3: a composer is kept under one key per repository, commit, directory, package and toolchain, so two asks of
// one head build it once on a node, and another head or toolchain builds its own.
func TestAComposerIsKeptByWhatItWasBuiltFrom(t *testing.T) {
tc := ToolchainUsed{Version: "go1.26.0", Recipe: "3"}
k := ComposerKey("novox/mesh-sdk", askCommit, "go", "p", tc, nil)
if k != ComposerKey("novox/mesh-sdk", askCommit, "go", "p", tc, nil) || !strings.HasPrefix(k, "composer-") || len(k) != len("composer-")+16 {
k := ComposerKey("novox/mesh-sdk", askCommit, "go", "p", tc)
if k != ComposerKey("novox/mesh-sdk", askCommit, "go", "p", tc) || !strings.HasPrefix(k, "composer-") || len(k) != len("composer-")+16 {
t.Fatalf("key = %q", k)
}
for _, other := range []string{
ComposerKey("novox/mesh-sdk", strings.Repeat("1", 40), "go", "p", tc, nil),
ComposerKey("novox/mesh-controller", askCommit, ".", "p", tc, nil),
ComposerKey("novox/mesh-sdk", askCommit, "go", "p", ToolchainUsed{Version: "go1.26.1", Recipe: "3"}, nil),
ComposerKey("novox/mesh-sdk", askCommit, "go", "p", tc, PackageVersions{"git.novox.be/novox/mesh-sdk/go": strings.Repeat("2", 40)}),
ComposerKey("novox/mesh-sdk", strings.Repeat("1", 40), "go", "p", tc),
ComposerKey("novox/mesh-controller", askCommit, ".", "p", tc),
ComposerKey("novox/mesh-sdk", askCommit, "go", "p", ToolchainUsed{Version: "go1.26.1", Recipe: "3"}),
} {
if other == k {
t.Errorf("two composers built from different inputs share the key %s", k)
}
}
// 1921: the asked versions are in the key, in any order of naming; none, or an empty ask, gives the key without them.
a, b := strings.Repeat("a", 40), strings.Repeat("b", 40)
k1 := ComposerKey("r", askCommit, "go", "p", tc, PackageVersions{"m/x": a, "m/y": b})
if k1 != ComposerKey("r", askCommit, "go", "p", tc, PackageVersions{"m/y": b, "m/x": a}) {
t.Error("the key depends on the order of the versions")
}
if k1 == ComposerKey("r", askCommit, "go", "p", tc, PackageVersions{"m/x": b, "m/y": a}) || k1 == ComposerKey("r", askCommit, "go", "p", tc, nil) {
t.Error("other versions, or none, share the key")
}
if ComposerKey("r", askCommit, "go", "p", tc, nil) != ComposerKey("r", askCommit, "go", "p", tc, PackageVersions{}) {
t.Error("an empty ask changed the key")
}
}