CAT-D3, D2: one mirror copy, said once; a refusal at the cap stays loud until the holder takes a publish again

A second tools process following mirrors would copy again and say published twice. The ask is claimed by
compare-and-set before it is copied, and its outcome carries the ask's id as the event id, so a redelivery is one
message. The cap's publish-refused finding stands on incoming, not on one file, and clears with the next publish
taken.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 13:24:40 +01:00
parent 9f01b9e79a
commit d683a78d9c
4 changed files with 103 additions and 7 deletions
@@ -6,6 +6,8 @@
package main
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
@@ -87,7 +89,7 @@ func (builderVersions) Recorded(name, version, digest string) (bool, string, err
}
func say(seat, event string, body holder.Event) error {
return stdio.SeatPublish("mesh.seat."+seat+".event."+event, eventBody(event, body), "")
return stdio.SeatPublish("mesh.seat."+seat+".event."+event, eventBody(event, body), body.ID)
}
func finding(kind, subject, words string, raised bool) {
@@ -219,11 +221,22 @@ func (mirrors) Ask(key string, r holder.MirrorRequest) error {
}
return nil
}
func (mirrors) Claim(key string, r holder.MirrorRequest, revision uint64) (bool, error) {
_, err := stdio.State(holder.MirrorsState).Update(key, r, revision)
if errors.Is(err, stdio.ErrChanged) {
return false, nil
}
return err == nil, err
}
func (mirrors) Delete(key string) error { return stdio.State(holder.MirrorsState).Delete(key) }
// followMirrors runs every copy `mirror` took on, on the state's watch: the asks kept when the bundle starts (a copy a
// restart cut) and each new one, one at a time.
// restart cut) and each new one, one at a time. Each is claimed first (Verbs.Take), so a second follower copies none
// a first one took on.
func followMirrors(v *holder.Verbs) {
id := make([]byte, 8)
_, _ = rand.Read(id)
self := "distribution-tools-" + hex.EncodeToString(id)
<-stdio.Served()
stdio.State(holder.MirrorsState).Follow("", func(ch stdio.StateChange) error {
if ch.Op != stdio.OpPut {
@@ -234,7 +247,7 @@ func followMirrors(v *holder.Verbs) {
logf("the mirror ask %s is no ask (%v): dropped", ch.Key, err)
return stdio.State(holder.MirrorsState).Delete(ch.Key)
}
return v.Mirrored(r)
return v.Take(ch.Key, r, ch.Revision, ch.Current, self)
})
}
@@ -129,8 +129,23 @@ func (m memMirrors) Ask(k string, r MirrorRequest) error {
}
return nil
}
func (m memMirrors) Claim(k string, r MirrorRequest, rev uint64) (bool, error) {
if cur, ok := m[k]; !ok || mirrorRev(cur) != rev {
return false, nil
}
m[k] = r
return true, nil
}
func (m memMirrors) Delete(k string) error { delete(m, k); return nil }
// mirrorRev is a kept ask's revision in memMirrors: 1 unclaimed, 2 once claimed.
func mirrorRev(r MirrorRequest) uint64 {
if r.Claim == "" {
return 1
}
return 2
}
type memHolds map[string]Hold
func (m memHolds) Get(k string) (Hold, bool, error) { h, ok := m[k]; return h, ok, nil }
@@ -301,6 +316,17 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
if b.findings["incoming-full incoming"] {
t.Error("the finding stayed after the transfer was dropped")
}
// A refusal for want of room stays loud on incoming until the holder takes a publish again.
b.h.Cap = 100
_, _ = b.h.Publish(ArtifactSeat, builder, req("t4"))
if !b.findings["publish-refused incoming"] {
t.Errorf("a refusal at the cap raised %v, want publish-refused on incoming", b.findings)
}
b.h.Cap = 1 << 20
b.store.has[part.Digest] = true
if _, err := b.h.Publish(ArtifactSeat, builder, req("t5")); err != nil || b.findings["publish-refused incoming"] {
t.Errorf("the next publish taken = %v, findings %v: want the refusal at the cap cleared", err, b.findings)
}
})
t.Run("the sweep's removal, hold and release", func(t *testing.T) {
b := newBed(true)
@@ -447,6 +473,20 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
b.said[0].body.Reference != want || b.said[0].body.Kind != KindUpstream || len(asked) != 0 {
t.Fatalf("the copy's end = %v, said %+v, asks %v: want published with the reference and the ask gone", err, b.said, asked)
}
// One copy, said once: two followers on the same ask; the second sees it claimed and passes, and the outcome
// carries the ask's id as its event id.
_, _ = b.v.Mirror(ImageSeat, "novox.mesh-build-image", MirrorRequest{Source: "nats", Digest: d})
ask := asked[key]
copies := len(b.said)
for _, follower := range []string{"one", "two"} {
if err := b.v.Take(key, ask, 1, false, follower); err != nil {
t.Fatalf("Take by %s: %v", follower, err)
}
}
if len(b.said) != copies+1 || ask.ID == "" || b.said[copies].body.ID != ask.ID || len(asked) != 0 {
t.Fatalf("two followers on one ask said %+v (ask id %q), asks %v: want one copy, said once under the ask's id", b.said[copies:], ask.ID, asked)
}
b.said = b.said[:copies]
b.cat.mirrorErr = errors.New("the upstream said 404")
_, _ = b.v.Mirror(ImageSeat, "novox.mesh-build-image", MirrorRequest{Source: "nats", Digest: d})
if err := b.v.Mirrored(asked[key]); err != nil || b.said[1].event != EventRefused || !strings.Contains(b.said[1].body.Reason, "404") || len(asked) != 0 {
@@ -99,6 +99,9 @@ type Event struct {
Reason string `json:"reason,omitempty"`
// By is who asked a removal, hold or release: the caller the tool runner proved.
By string `json:"by,omitempty"`
// ID is the event's id on the bus when the outcome has one of its own (a mirror ask's): the same outcome said
// twice, by a redelivery or a second follower, is one message. Empty: the runtime makes one.
ID string `json:"-"`
}
// The events of the seats.
@@ -197,6 +200,8 @@ func (h *Holder) receiver() *parts.Receiver[Opening] {
return errors.Join(errors.New("refused: "+reason), h.refused(t, reason))
}
h.Finding("publish-refused", t.Shape.Name+"@"+t.Shape.Version, "", false)
// Room was found again: a refusal for want of it clears with the first publish the holder takes.
h.Finding("publish-refused", IncomingObjects, "", false)
return h.Say(t.Shape.Seat, EventPublished, h.eventOf(t.Shape.Seat, t.Shape.PublishRequest, ref, ""))
},
Refuse: h.refused,
@@ -205,9 +210,14 @@ func (h *Holder) receiver() *parts.Receiver[Opening] {
}}
}
// refused writes the finding and says refused on the transfer's seat.
// refused writes the finding and says refused on the transfer's seat. A refusal for want of room is the holder's,
// not the file's: its finding stands on incoming and clears when the holder takes a publish again.
func (h *Holder) refused(t Transfer, reason string) error {
h.Finding("publish-refused", t.Shape.Name+"@"+t.Shape.Version, reason, true)
subject := t.Shape.Name + "@" + t.Shape.Version
if parts.RefusedAtCap(reason) {
subject = IncomingObjects
}
h.Finding("publish-refused", subject, reason, true)
return h.Say(t.Shape.Seat, EventRefused, h.eventOf(t.Shape.Seat, t.Shape.PublishRequest, "", reason))
}
+35 -2
View File
@@ -1,6 +1,8 @@
package holder
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"slices"
@@ -32,6 +34,9 @@ const MirrorsState = "mirrors"
type Mirrors interface {
// Ask keeps r under its key; an ask already kept is no fault (the copy is taken on once).
Ask(key string, r MirrorRequest) error
// Claim writes r (its Claim set) under key at revision, compare-and-set: false, and no error, when another
// writer wrote the key first.
Claim(key string, r MirrorRequest, revision uint64) (bool, error)
Delete(key string) error
}
@@ -225,6 +230,10 @@ type MirrorRequest struct {
Source string `json:"source"`
Digest string `json:"digest"`
By string `json:"by,omitempty"`
// ID is the ask's own id, made when it is asked: the id of the event that says its end.
ID string `json:"id,omitempty"`
// Claim is the follower that took the copy on: none until one claims it.
Claim string `json:"claim,omitempty"`
}
// MirrorsKey is an ask's key in the state mirrors: the copy's name and digest, in a key's characters.
@@ -250,7 +259,12 @@ func (v *Verbs) Mirror(seat, caller string, r MirrorRequest) (string, error) {
if err != nil {
return "", err
}
r.By = caller
r.By, r.Claim = caller, ""
if r.ID == "" {
id := make([]byte, 8)
_, _ = rand.Read(id)
r.ID = "mirror-" + hex.EncodeToString(id)
}
if err := v.Mirrors.Ask(MirrorsKey(src.Name(), r.Digest), r); err != nil {
return "", fmt.Errorf("mirror: the copy of %s@%s cannot be taken on: %w", r.Source, r.Digest, err)
}
@@ -265,7 +279,7 @@ func (v *Verbs) Mirrored(r MirrorRequest) error {
if err != nil {
return err
}
e := Event{Name: src.Name(), Kind: KindUpstream, Digest: r.Digest, Holder: Self, By: r.By}
e := Event{Name: src.Name(), Kind: KindUpstream, Digest: r.Digest, Holder: Self, By: r.By, ID: r.ID}
event := EventPublished
if e.Reference, err = v.Catalog.Mirror(r.Source, r.Digest); err != nil {
event, e.Reference, e.Reason = EventRefused, "", fmt.Sprintf("%s@%s was not copied: %v", r.Source, r.Digest, err)
@@ -276,6 +290,25 @@ func (v *Verbs) Mirrored(r MirrorRequest) error {
return v.Mirrors.Delete(MirrorsKey(src.Name(), r.Digest))
}
// Take decides one change of the state mirrors for the follower self: an ask no follower claimed, or one claimed
// before this follower's watch began (its claimant may have been cut by a restart), is claimed by compare-and-set at
// revision and copied by the follower that won; an ask claimed since, by self or by another, is passed. Two followers
// that both start on the same cut ask can both win in turn; the outcome then carries the ask's id, so it is said once.
func (v *Verbs) Take(key string, r MirrorRequest, revision uint64, current bool, self string) error {
if r.Claim != "" && (r.Claim == self || !current) {
return nil
}
r.Claim = self
won, err := v.Mirrors.Claim(key, r, revision)
if err != nil {
return fmt.Errorf("mirror: the ask %s cannot be claimed: %w", key, err)
}
if !won {
return nil
}
return v.Mirrored(r)
}
// HeldNow lists the holds, for registry_holds.
func (v *Verbs) HeldNow() ([]Hold, error) {
all, err := v.Holds.List()