diff --git a/go/delivery/builder.go b/go/delivery/builder.go index 95468e3..bad5430 100644 --- a/go/delivery/builder.go +++ b/go/delivery/builder.go @@ -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 `@` 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. diff --git a/go/delivery/builder_reference_test.go b/go/delivery/builder_reference_test.go index 4a0dd3c..dd9c68a 100644 --- a/go/delivery/builder_reference_test.go +++ b/go/delivery/builder_reference_test.go @@ -141,3 +141,32 @@ func TestWaitsForIsAPrefixThatIsBuiltAgain(t *testing.T) { t.Fatalf("WaitsFor = %q", WaitsFor) } } + +// 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). +func TestRetakeableIsAPrefixTheBuilderTakesAgain(t *testing.T) { + 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) + } + } + if Retake != "retakeable: " || MaxRetakes != 3 { + t.Fatal("the retake prefix or its bound changed") + } + 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) + } +}