diff --git a/modules/mesh-delivery-builder/internal/build/claim.go b/modules/mesh-delivery-builder/internal/build/claim.go index cb5f74c..d598bc7 100644 --- a/modules/mesh-delivery-builder/internal/build/claim.go +++ b/modules/mesh-delivery-builder/internal/build/claim.go @@ -85,9 +85,26 @@ func Unpublished(b delivery.BuilderBuild) bool { return false } +// Unreferenced is whether a done build holds a bundle, an archive or an upstream copy that names no reference: a key done +// before R4e, whose artifacts stayed on their build nodes (ADR 0498 clause 8). An artifact of no kind (a key written before +// kinds) is one too. Claim takes such a key again, once: its new revision names them. +func Unreferenced(b delivery.BuilderBuild) bool { + if b.State != delivery.BuildDone { + return false + } + for _, a := range b.Artifacts { + if a.Reference == "" && (a.Kind == "" || stored(a.Kind)) { + 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: // - no key: it is created building, by this builder; a create that finds the key made meanwhile gets nothing; -// - a done key is not taken again (the same source is never built twice), except one built on one node only (OneNodePrefix) +// - a done key is not taken again (the same source is never built twice), except one whose bundle, archive or upstream +// copy names no reference (Unreferenced, a key done before R4e: ADR 0498 clause 8), which is claimed building as a new +// revision and built whole, and one built on one node only (OneNodePrefix) // when a second node qualifies now (ClaimOptions.MoreNodes): that is taken as it stands, with the done value, and its // builder writes the new revision when the second build ends (ADR 0478 clause 4); // - a failed key is taken again as a new revision of the same key (ADR 0429 clause 18), except one failed for a kind no @@ -115,6 +132,9 @@ func (c Claimer) Claim(ctx context.Context, module, fingerprint, manifest string } switch { case cur.State == delivery.BuildDone: + if Unreferenced(cur) { + return c.write(ctx, key, building, func(data []byte) (uint64, error) { return c.Store.Update(ctx, key, data, rev) }) + } if modules := OneNodeModules(cur); len(modules) > 0 && opt.MoreNodes != nil && opt.MoreNodes(modules) { return &Claim{Key: key, Rev: rev, Build: cur}, nil } diff --git a/modules/mesh-delivery-builder/internal/build/claim_test.go b/modules/mesh-delivery-builder/internal/build/claim_test.go index cab71e3..fdf6d93 100644 --- a/modules/mesh-delivery-builder/internal/build/claim_test.go +++ b/modules/mesh-delivery-builder/internal/build/claim_test.go @@ -125,9 +125,9 @@ func TestClaimLosingTheCreateRaceGetsNothing(t *testing.T) { // 1607/AC3 } } -func TestADoneKeyIsNeverTakenAgain(t *testing.T) { // 1607/AC3 +func TestADoneKeyIsNeverTakenAgain(t *testing.T) { // 1607/AC3; a key whose artifacts name references (ADR 0498 clause 8) st := newMem() - seed(t, st, delivery.BuilderBuild{State: delivery.BuildDone, By: "novox.a", Manifest: hex64('a'), Artifacts: []delivery.BuiltArtifact{{Name: "x", Digest: digest}}}) + seed(t, st, delivery.BuilderBuild{State: delivery.BuildDone, By: "novox.a", Manifest: hex64('a'), Artifacts: []delivery.BuiltArtifact{{Name: "x", Kind: "bundle", Digest: digest, Reference: "artifact-store://m/x/blobs/" + digest}}}) writes := st.writes got, err := Claimer{Store: st, By: "novox.a"}.Claim(context.Background(), "m", fp1, hex64('a'), ClaimOptions{}) if err != nil || got != nil || st.writes != writes { diff --git a/modules/mesh-delivery-builder/internal/build/r4e_test.go b/modules/mesh-delivery-builder/internal/build/r4e_test.go index 6d77b9a..ad5b584 100644 --- a/modules/mesh-delivery-builder/internal/build/r4e_test.go +++ b/modules/mesh-delivery-builder/internal/build/r4e_test.go @@ -74,3 +74,34 @@ func TestOnlyATrunkKeysBuildsAreAskedToPublish(t *testing.T) { // B1, the lead's } } } + +// putBuild writes a builder key's value as the store would hold it. +func putBuild(t *testing.T, st *memStore, key string, b delivery.BuilderBuild) uint64 { + t.Helper() + data, err := delivery.EncodeBuilderBuild(b) + if err != nil { + t.Fatal(err) + } + st.mu.Lock() + defer st.mu.Unlock() + return st.put(key, data) +} + +func TestADoneKeyWrittenBeforeR4eIsBuiltAgainOnceAsANewRevision(t *testing.T) { // B2, ADR 0498 clause 8 + r := newRig(t, goBundle, archiveA) + rev := putBuild(t, r.st, r.key, delivery.BuilderBuild{State: delivery.BuildDone, By: "novox.builder", Manifest: manifestD, Step: "built mesh-build-go on ace and novox", + Artifacts: []delivery.BuiltArtifact{{Name: "files", Kind: "archive", Digest: digest}, {Name: "tool", Kind: "bundle", Digest: digest}}}) + v := r.consider(t, "modules/m") + if v.State != delivery.BuildDone || r.st.revs[r.key] <= rev { + t.Fatalf("build = %+v at revision %d (was %d), want built again as a new revision", v, r.st.revs[r.key], rev) + } + if ok, missing := v.Referenced(); !ok { + t.Fatalf("the new revision names no reference for %v", missing) + } + rev = r.st.revs[r.key] + r.b.tried = map[string]bool{} // a builder started anew looks again + r.consider(t, "modules/m") + if r.st.revs[r.key] != rev { + t.Errorf("a key whose artifacts all name references was built again (revision %d, was %d): once only", r.st.revs[r.key], rev) + } +}