C2b: a seat verb's no responders is a transport fault when the assigner names a holder of the seat; a seat nothing holds fails at once (ADR 0664 §1, issue 1748)
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -3,6 +3,7 @@ module git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/gopackageregistryclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/npmpackageregistryclient v0.0.0-00010101000000-000000000000
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
assigner "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
imageclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient"
|
||||
goclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/gopackageregistryclient"
|
||||
npmclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/npmpackageregistryclient"
|
||||
@@ -23,6 +24,7 @@ import (
|
||||
goseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry"
|
||||
npmseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry"
|
||||
ociseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry"
|
||||
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/parts"
|
||||
)
|
||||
|
||||
@@ -76,6 +78,41 @@ func SentHow(n int) func(parts.Sent) string {
|
||||
// PublishParts publishes in with ps as its parts on seat, through that seat's client: in's fields are the seat's own
|
||||
// (the four registry seats share the publish verb's input, design 51), the parts are written by the client.
|
||||
func PublishParts(ctx context.Context, call SeatCall, seat string, in reg.PublishInput, ps []parts.Part) (parts.Sent, error) {
|
||||
sent, err := publishParts(ctx, call, seat, in, ps)
|
||||
return sent, HolderSilentOr(call, seat, err)
|
||||
}
|
||||
|
||||
// AssignerHolders is the verb that tells who holds a seat: `mesh-module-assigner.assignments` with seats
|
||||
// (the state seat-holders). The build modules' manifests `invokes` it.
|
||||
const AssignerHolders = "seat:" + assigner.Seat + "." + assigner.VerbAssignments
|
||||
|
||||
// HolderSilentOr is err, or when it is a seat verb's "no responders" and the assigner names a holder of seat, a
|
||||
// sdkstep.HolderSilent (a Passing: the holder's tool runner is being replaced and answers a moment later, ADR 0664 §1). The
|
||||
// bus says "no responders" for a seat nothing holds too, so the words alone are no transport fault: with no holder named, or
|
||||
// when the holders cannot be asked, the error stays as it is, said with why, and fails at once.
|
||||
func HolderSilentOr(call SeatCall, seat string, err error) error {
|
||||
if err == nil || !strings.Contains(strings.ToLower(err.Error()), "no responders") {
|
||||
return err
|
||||
}
|
||||
raw, aerr := call(AssignerHolders, map[string]any{"seats": assigner.AssignmentsInputSeatsTrue})
|
||||
if aerr != nil {
|
||||
return fmt.Errorf("%w (who holds %s could not be asked: %v)", err, seat, aerr)
|
||||
}
|
||||
var answer struct {
|
||||
Seats []assigner.SeatHolders `json:"seats"`
|
||||
}
|
||||
if jerr := json.Unmarshal(raw, &answer); jerr != nil {
|
||||
return fmt.Errorf("%w (who holds %s was answered in what is not read: %v)", err, seat, jerr)
|
||||
}
|
||||
for _, sh := range answer.Seats {
|
||||
if sh.Seat == seat && len(sh.Holders) > 0 {
|
||||
return fmt.Errorf("%w: %w", err, &sdkstep.HolderSilent{Seat: seat, Holder: sh.Holders[0].Module, Node: sh.Holders[0].Node})
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%w (nothing holds %s)", err, seat)
|
||||
}
|
||||
|
||||
func publishParts(ctx context.Context, call SeatCall, seat string, in reg.PublishInput, ps []parts.Part) (parts.Sent, error) {
|
||||
switch seat {
|
||||
case ImageSeat:
|
||||
s, err := imageclient.Publish(ctx, imageclient.Call(call), in, ps)
|
||||
|
||||
@@ -178,7 +178,7 @@ func (m *Module) packInto(ctx context.Context, workspace, module, name, artifact
|
||||
return BuildResult{}, fmt.Errorf("packing %s: %w", artifact, err)
|
||||
}
|
||||
if err := m.publishArchive(ctx, name, artifact, kind, digest, archive); err != nil {
|
||||
return BuildResult{}, fmt.Errorf("%s%s: %v", builds.NotStored, artifact, err)
|
||||
return BuildResult{}, fmt.Errorf("%s%s: %w", builds.NotStored, artifact, err)
|
||||
}
|
||||
return BuildResult{Artifact: artifact, Digest: digest, Size: len(archive), Reference: artifactref.SeatReference(ArtifactSeat, name, artifact, digest)}, nil
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ func (m *Module) buildGoPackage(ctx context.Context, workspace, dir string, art
|
||||
return res, nil
|
||||
}
|
||||
if err := m.publishOn(ctx, seat, path, "package", version, digest, transferName(path, version, digest), body); err != nil {
|
||||
return BuildResult{}, fmt.Errorf("%s%s: %v", builds.NotStored, art.Name, err)
|
||||
return BuildResult{}, fmt.Errorf("%s%s: %w", builds.NotStored, art.Name, err)
|
||||
}
|
||||
res.Reference = path + "@" + version
|
||||
return res, nil
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
|
||||
registry "git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry"
|
||||
builds "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
@@ -139,6 +140,27 @@ func TestARefusedPushFailsTheStepNotStored(t *testing.T) {
|
||||
!strings.Contains(st.Reason, "oci-artifact-registry") {
|
||||
t.Errorf("no holder: the step ended %s: %q", st.State, st.Reason)
|
||||
}
|
||||
// ADR 0664 §1 (issue 1748): "no responders" is the same words for a seat nothing holds and for a holder whose tool runner is
|
||||
// being replaced; the assigner says which. A named holder makes it a transport fault (the bus gives the ask again), none
|
||||
// fails at once naming it.
|
||||
for _, c := range []struct {
|
||||
name, holders string
|
||||
passing bool
|
||||
words string
|
||||
}{
|
||||
{"a holder named", `{"seats":[{"seat":"oci-artifact-registry","holders":[{"module":"mesh-registry","node":"novox"}]}],"holds":1}`, true, "the holder is silent: seat oci-artifact-registry, held by mesh-registry on novox"},
|
||||
{"nothing holds it", `{"seats":[{"seat":"go-package-registry","holders":[{"module":"mesh-registry","node":"novox"}]}],"holds":1}`, false, "nothing holds oci-artifact-registry"},
|
||||
} {
|
||||
m.Call = func(key string, _ any) (json.RawMessage, error) {
|
||||
if key == buildstep.AssignerHolders {
|
||||
return json.RawMessage(c.holders), nil
|
||||
}
|
||||
return nil, errors.New("nats: no responders available for request")
|
||||
}
|
||||
if _, st := runBuild(t, m, states, "tools"); st.State != sdkstep.StepFailed || st.Passing != c.passing || !strings.Contains(st.Reason, c.words) {
|
||||
t.Errorf("%s: the step ended %s: %q (passing %v)", c.name, st.State, st.Reason, st.Passing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1587-S4 (the lead's decision L9): the packed file is not left on the node once its push answered, a candidate's
|
||||
|
||||
@@ -102,6 +102,7 @@ replace git.novox.be/novox/mesh-catalog/modules/ssh-client/sshkeys => ../ssh-cli
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/gopackageregistryclient v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/npmpackageregistryclient v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/ociartifactregistryclient v0.0.0-00010101000000-000000000000 // indirect
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
"seat:oci-artifact-registry.publish-part",
|
||||
"seat:go-package-registry.publish",
|
||||
"seat:go-package-registry.publish-part",
|
||||
"seat:mesh-delivery-checker.input"
|
||||
"seat:mesh-delivery-checker.input",
|
||||
"seat:mesh-module-assigner.assignments"
|
||||
],
|
||||
"config": {
|
||||
"capacity": {
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestTheModuleInvokesTheImageSeatAndHoldsNoRegistry(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := []string{"seat:container-image-registry.publish", "seat:container-image-registry.publish-part",
|
||||
"seat:container-image-registry.mirror"}; !slices.Equal(m.Invokes, want) {
|
||||
"seat:container-image-registry.mirror", "seat:mesh-module-assigner.assignments"}; !slices.Equal(m.Invokes, want) {
|
||||
t.Errorf("invokes = %v, want %v", m.Invokes, want)
|
||||
}
|
||||
// The builder's version.numbered for the late publish (ADR 0617 §1), and the seat's outcomes a copy waits on.
|
||||
|
||||
@@ -136,7 +136,7 @@ func (m *Module) publish(ctx context.Context, workspace, module, local string, r
|
||||
Transfer: buildstep.TransferName(name, q, img.Digest)}
|
||||
sent, err := buildstep.PublishFiles(ctx, m.call(), buildstep.ImageSeat, in, img.Files)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s%s: %v", builds.NotStored, res.Artifact, err)
|
||||
return nil, fmt.Errorf("%s%s: %w", builds.NotStored, res.Artifact, err)
|
||||
}
|
||||
m.say("published %s on %s: %d of its %d parts sent, the rest held", name, buildstep.ImageSeat, len(sent.Parts), len(img.Files))
|
||||
res.Digest, res.Pushed = img.Digest, true
|
||||
|
||||
@@ -34,6 +34,7 @@ require git.novox.be/novox/mesh-catalog/modules/ssh-client/sshkeys v0.0.0-000101
|
||||
replace git.novox.be/novox/mesh-catalog/modules/ssh-client/sshkeys => ../ssh-client/sshkeys
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/gopackageregistryclient v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/npmpackageregistryclient v0.0.0-00010101000000-000000000000 // indirect
|
||||
|
||||
@@ -63,7 +63,8 @@
|
||||
"invokes": [
|
||||
"seat:container-image-registry.publish",
|
||||
"seat:container-image-registry.publish-part",
|
||||
"seat:container-image-registry.mirror"
|
||||
"seat:container-image-registry.mirror",
|
||||
"seat:mesh-module-assigner.assignments"
|
||||
],
|
||||
"consumes": [
|
||||
"mesh-delivery-builder.version.numbered",
|
||||
|
||||
@@ -168,7 +168,7 @@ func (m *Module) build(ctx context.Context, args map[string]any) (any, error) {
|
||||
// Published from this process once the build ended, a candidate's as a trunk build's (ADR 0498 §1 §2; CAT-BN3): the
|
||||
// step answers the seat's reference, or fails not stored. The packed bytes are never written to the node (L9).
|
||||
if err := m.publishOn(ctx, buildstep.ArtifactSeat, a.module+"/"+a.Name, "bundle", "", digest, archive); err != nil {
|
||||
return nil, fmt.Errorf("%s%s: %v", builds.NotStored, a.Name, err)
|
||||
return nil, fmt.Errorf("%s%s: %w", builds.NotStored, a.Name, err)
|
||||
}
|
||||
files := []string{}
|
||||
_ = filepath.WalkDir(bundleDir, func(p string, d os.DirEntry, err error) error {
|
||||
|
||||
@@ -551,7 +551,7 @@ func (m *Module) buildPackage(ctx context.Context, workspace, module, root, dir,
|
||||
return res, nil
|
||||
}
|
||||
if err := m.publishOn(ctx, seat, p.Name, "package", p.Version, p.Digest, p.tgz); err != nil {
|
||||
return nil, fmt.Errorf("%s%s: %v", builds.NotStored, a.Name, err)
|
||||
return nil, fmt.Errorf("%s%s: %w", builds.NotStored, a.Name, err)
|
||||
}
|
||||
res.Reference = p.Name + "@" + p.Version
|
||||
return res, nil
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
|
||||
builds "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
||||
)
|
||||
|
||||
// pushed are the blobs the fake holder took, by digest, for the tests that read an archive back.
|
||||
@@ -130,6 +131,18 @@ func TestARefusedPushFailsNotStoredAndNoPackedFileStays(t *testing.T) {
|
||||
if c.words != "" && (err == nil || !strings.HasPrefix(err.Error(), builds.NotStored+"tools: ") || !strings.Contains(err.Error(), c.words)) {
|
||||
t.Errorf("a refused publish answered %v", err)
|
||||
}
|
||||
if c.noSeat {
|
||||
// ADR 0664 §1: with a holder named by the assigner the same words are a transport fault, the bus gives the ask again.
|
||||
m.Call = func(key string, _ any) (json.RawMessage, error) {
|
||||
if key == buildstep.AssignerHolders {
|
||||
return json.RawMessage(`{"seats":[{"seat":"oci-artifact-registry","holders":[{"module":"mesh-registry","node":"novox"}]}],"holds":1}`), nil
|
||||
}
|
||||
return nil, errors.New("nats: no responders available for request")
|
||||
}
|
||||
if _, err := m.build(context.Background(), buildArgs()); err == nil || !sdkstep.IsPassing(err) || !strings.Contains(err.Error(), "the holder is silent") {
|
||||
t.Errorf("no responders with a holder named answered %v, want a transport fault", err)
|
||||
}
|
||||
}
|
||||
var archives []string
|
||||
filepath.WalkDir(work, func(p string, d os.DirEntry, err error) error {
|
||||
if err == nil && strings.HasSuffix(p, ".tar.gz") {
|
||||
|
||||
@@ -94,6 +94,7 @@ require git.novox.be/novox/mesh-catalog/modules/ssh-client/sshkeys v0.0.0-000101
|
||||
replace git.novox.be/novox/mesh-catalog/modules/ssh-client/sshkeys => ../ssh-client/sshkeys
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/gopackageregistryclient v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000 // indirect
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/npmpackageregistryclient v0.0.0-00010101000000-000000000000 // indirect
|
||||
|
||||
@@ -76,7 +76,8 @@
|
||||
"seat:oci-artifact-registry.publish",
|
||||
"seat:oci-artifact-registry.publish-part",
|
||||
"seat:npm-package-registry.publish",
|
||||
"seat:npm-package-registry.publish-part"
|
||||
"seat:npm-package-registry.publish-part",
|
||||
"seat:mesh-module-assigner.assignments"
|
||||
],
|
||||
"own-secrets": {
|
||||
"broker": "${dir:state}/broker"
|
||||
|
||||
Reference in New Issue
Block a user