Publish a merged candidate's image once; a retaken key builds only what is missing (review r1)
This commit is contained in:
@@ -66,6 +66,23 @@ type ClaimOptions struct {
|
||||
// ImageBuilder says whether a node runs a healthy image build module now: a key failed for NoImageBuilderPrefix is taken
|
||||
// again only then. Nil means none does.
|
||||
ImageBuilder func() bool
|
||||
// Trunk is whether the trunk's record names this fingerprint. A done key whose image has no reference was built as a
|
||||
// candidate's, which never pushes (ADR 0487 clause 12); on the trunk it is taken again to publish the image, when an image
|
||||
// builder runs (ImageBuilder).
|
||||
Trunk bool
|
||||
}
|
||||
|
||||
// Unpublished is whether a done build holds an image built without a push: it has no reference.
|
||||
func Unpublished(b delivery.BuilderBuild) bool {
|
||||
if b.State != delivery.BuildDone {
|
||||
return false
|
||||
}
|
||||
for _, a := range b.Artifacts {
|
||||
if a.Kind == "image" && a.Reference == "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Claim takes the build of module at fingerprint, or answers nil when it is not this builder's to build now:
|
||||
@@ -101,6 +118,9 @@ func (c Claimer) Claim(ctx context.Context, module, fingerprint, manifest string
|
||||
if modules := OneNodeModules(cur); len(modules) > 0 && opt.MoreNodes != nil && opt.MoreNodes(modules) {
|
||||
return &Claim{Key: key, Rev: rev, Build: cur}, nil
|
||||
}
|
||||
if opt.Trunk && Unpublished(cur) && opt.ImageBuilder != nil && opt.ImageBuilder() {
|
||||
return &Claim{Key: key, Rev: rev, Build: cur}, nil
|
||||
}
|
||||
return nil, nil
|
||||
case cur.State == delivery.BuildFailed && (strings.HasPrefix(cur.Error, RefusedPrefix) || strings.HasPrefix(cur.Error, buildrun.DigestsDifferPrefix)):
|
||||
return nil, nil
|
||||
|
||||
@@ -288,6 +288,7 @@ func (b *Builder) try(ctx context.Context, module string, m delivery.Module) {
|
||||
}
|
||||
key, _ := delivery.BuilderBuildKey(module, m.FP2)
|
||||
b.mu.Lock()
|
||||
trunk := b.trunkFP[key]
|
||||
first := !b.tried[key]
|
||||
b.tried[key] = true
|
||||
epoch := b.epoch
|
||||
@@ -297,7 +298,7 @@ func (b *Builder) try(ctx context.Context, module string, m delivery.Module) {
|
||||
// A second build that failed is not tried again until the nodes change (issue 1664).
|
||||
return (!failedBefore || epoch > failedAt) && b.moreNodes(modules)
|
||||
}
|
||||
cl, err := b.Claimer.Claim(ctx, module, m.FP2, m.Manifest, ClaimOptions{TakeOver: first, MoreNodes: moreNodes, ImageBuilder: b.imageBuilderRuns})
|
||||
cl, err := b.Claimer.Claim(ctx, module, m.FP2, m.Manifest, ClaimOptions{TakeOver: first, MoreNodes: moreNodes, ImageBuilder: b.imageBuilderRuns, Trunk: trunk})
|
||||
if err != nil {
|
||||
b.logf("%s: %v", module, err)
|
||||
return
|
||||
@@ -305,7 +306,20 @@ func (b *Builder) try(ctx context.Context, module string, m delivery.Module) {
|
||||
if cl == nil {
|
||||
return
|
||||
}
|
||||
result := b.build(ctx, module, m, plan, planErr)
|
||||
// A done key taken again builds only what is missing: the build modules that ran on one node, and the image when it was
|
||||
// built as a candidate's; the other artifacts stay as the done key has them (ADR 0487 clauses 5 and 8).
|
||||
var prior *delivery.BuilderBuild
|
||||
var only map[string]bool
|
||||
if cl.Build.State == delivery.BuildDone {
|
||||
prior, only = &cl.Build, map[string]bool{}
|
||||
for _, bm := range OneNodeModules(cl.Build) {
|
||||
only[bm] = true
|
||||
}
|
||||
if trunk && Unpublished(cl.Build) {
|
||||
only[ImageBuildModule] = true
|
||||
}
|
||||
}
|
||||
result := b.build(ctx, module, m, plan, planErr, prior, only)
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
@@ -365,7 +379,7 @@ type lane struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (b *Builder) build(ctx context.Context, module string, m delivery.Module, plan Plan, planErr error) delivery.BuilderBuild {
|
||||
func (b *Builder) build(ctx context.Context, module string, m delivery.Module, plan Plan, planErr error, prior *delivery.BuilderBuild, only map[string]bool) delivery.BuilderBuild {
|
||||
switch {
|
||||
case planErr != nil:
|
||||
return failed("%s", planErr)
|
||||
@@ -388,7 +402,12 @@ func (b *Builder) build(ctx context.Context, module string, m delivery.Module, p
|
||||
b.Places.Cancel(l.node)
|
||||
}
|
||||
}
|
||||
var kept []string // build modules whose artifacts stay as the prior done key has them
|
||||
for _, bm := range plan.Modules() {
|
||||
if prior != nil && len(only) > 0 && !only[bm] {
|
||||
kept = append(kept, bm)
|
||||
continue
|
||||
}
|
||||
language := buildrun.LanguageOf[bm]
|
||||
entries := b.Toolchains.Entries(language)
|
||||
qualifies := func(node string) bool { return b.qualifies(node, bm) }
|
||||
@@ -461,7 +480,23 @@ func (b *Builder) build(ctx context.Context, module string, m delivery.Module, p
|
||||
return failed("%s", strings.Join(errs, "; "))
|
||||
}
|
||||
var artifacts []delivery.BuiltArtifact
|
||||
keptModule := map[string]bool{}
|
||||
for _, bm := range kept {
|
||||
keptModule[bm] = true
|
||||
}
|
||||
for _, t := range plan.Tasks {
|
||||
if keptModule[t.Module] {
|
||||
found := false
|
||||
for _, a := range prior.Artifacts {
|
||||
if a.Name == t.Artifact {
|
||||
artifacts, found = append(artifacts, a), true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return failed("the done build holds no artifact %s to keep", t.Artifact)
|
||||
}
|
||||
continue
|
||||
}
|
||||
byNode := map[string]string{}
|
||||
for _, l := range lanes {
|
||||
if d, ok := l.digests[t.Artifact]; ok && l.module == t.Module {
|
||||
@@ -480,6 +515,14 @@ func (b *Builder) build(ctx context.Context, module string, m delivery.Module, p
|
||||
}
|
||||
artifacts = append(artifacts, built)
|
||||
}
|
||||
for _, bm := range kept {
|
||||
steps = append(steps, bm+" kept")
|
||||
for lang, u := range prior.Toolchains {
|
||||
if _, ok := used[lang]; !ok && buildrun.LanguageOf[bm] == lang {
|
||||
used[lang] = u
|
||||
}
|
||||
}
|
||||
}
|
||||
step := "built " + strings.Join(steps, "; ")
|
||||
if len(oneNode) > 0 {
|
||||
// Marked so that Claim builds the key again once a second node qualifies (ADR 0478 clause 4).
|
||||
|
||||
@@ -530,3 +530,79 @@ func TestANodeThatIsNotOnlineIsNotChosenAndComingBackWakesTheBuild(t *testing.T)
|
||||
t.Errorf("build = %+v, want done once a node is online", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestACandidatesImageIsPublishedOnceWhenTheTrunkNamesItsFingerprint(t *testing.T) { // review r1, mesh-delivery 1; ADR 0487 clause 5
|
||||
r := newRig(t, imageArt)
|
||||
r.b.Health = imageHave(t, "ace")
|
||||
trunkModule(t, r) // the trunk carries another fingerprint; fp1 is the candidate's
|
||||
r.b.Candidate(context.Background(), "feat%2Fx", candidateValue(t, "feat/x", 7, headX))
|
||||
r.b.Wait()
|
||||
v := valueOf(t, r.st, r.key)
|
||||
if v.State != delivery.BuildDone || v.Artifacts[0].Reference != "" {
|
||||
t.Fatalf("candidate build = %+v, want done with no reference", v)
|
||||
}
|
||||
if n := len(r.bm.callsOf("build_image_build")); n != 1 {
|
||||
t.Fatalf("%d image builds after the candidate, want 1", n)
|
||||
}
|
||||
// The pull request is merged: the trunk's record names the candidate's fingerprint.
|
||||
r.b.Module(context.Background(), "m", moduleValue(t, "modules/m"))
|
||||
r.b.Wait()
|
||||
v = valueOf(t, r.st, r.key)
|
||||
if v.State != delivery.BuildDone || len(v.Artifacts) != 1 || v.Artifacts[0].Reference == "" {
|
||||
t.Fatalf("trunk build = %+v, want done with a reference", v)
|
||||
}
|
||||
calls := r.bm.callsOf("build_image_build")
|
||||
published := 0
|
||||
for _, c := range calls {
|
||||
if c.body.(map[string]any)["publish"] == true {
|
||||
published++
|
||||
}
|
||||
}
|
||||
if len(calls) != 2 || published != 1 {
|
||||
t.Errorf("%d image builds, %d with publish, want 2 and exactly 1", len(calls), published)
|
||||
}
|
||||
// Written once more only: looking again builds nothing.
|
||||
rev := r.st.revs[r.key]
|
||||
r.b.Module(context.Background(), "m", moduleValue(t, "modules/m"))
|
||||
r.b.Wait()
|
||||
if r.st.revs[r.key] != rev || len(r.bm.callsOf("build_image_build")) != 2 {
|
||||
t.Error("a published key was built again")
|
||||
}
|
||||
}
|
||||
|
||||
func TestASecondGoNodeDoesNotBuildOrPushTheImageAgain(t *testing.T) { // review r1, mesh-delivery 2; ADR 0487 clause 8
|
||||
r := newRig(t, goBundle, imageArt)
|
||||
r.b.Health = imageHave(t, "ace", "novox")
|
||||
r.pres.Put("ace", presenceValue(t, false), false) // only novox qualifies for Go (and the image)
|
||||
v := r.consider(t, "modules/m")
|
||||
if v.State != delivery.BuildDone || len(OneNodeModules(v)) != 1 {
|
||||
t.Fatalf("build = %+v, want done with the Go bundle on one node", v)
|
||||
}
|
||||
var image delivery.BuiltArtifact
|
||||
for _, a := range v.Artifacts {
|
||||
if a.Kind == "image" {
|
||||
image = a
|
||||
}
|
||||
}
|
||||
if image.Reference == "" {
|
||||
t.Fatalf("artifacts = %+v, want a published image", v.Artifacts)
|
||||
}
|
||||
r.pres.Put("ace", presenceValue(t, true), false)
|
||||
r.b.Reconsider(context.Background())
|
||||
r.b.Wait()
|
||||
v = valueOf(t, r.st, r.key)
|
||||
if len(OneNodeModules(v)) != 0 || len(v.Artifacts) != 2 {
|
||||
t.Fatalf("build = %+v, want done on two Go nodes", v)
|
||||
}
|
||||
if n := len(r.bm.callsOf("build_image_build")); n != 1 {
|
||||
t.Errorf("%d image builds, want the one of the first build only", n)
|
||||
}
|
||||
for _, a := range v.Artifacts {
|
||||
if a.Kind == "image" && a != image {
|
||||
t.Errorf("image = %+v, was %+v: the done key was rewritten with another image", a, image)
|
||||
}
|
||||
}
|
||||
if n := len(r.bm.callsOf("build_go_build")); n != 3 { // one, then two
|
||||
t.Errorf("%d go builds, want 3", n)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user