A done key the controller never took in holds no manifest, and the store's nightly collector deleted its bare blob: node-tools', mesh-host's and the forge bridge's keys name blobs that answer 404. Since issue 1933 the take-in refuses such a key, and nothing built it again. held (in the serving process, HEADs only) lets the builder ask before it reuses a key; a refused take-in now names every gone archive and says artifacts-gone with the module, key, fingerprint and references.
129 lines
5.0 KiB
Go
129 lines
5.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery"
|
|
|
|
"github.com/novox/mesh-controller/internal/artifacts"
|
|
"github.com/novox/mesh-controller/internal/builderkeys"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// The verb `held` and the event `artifacts-gone` (novox/hq issue 1937).
|
|
//
|
|
// **A done key can name a blob the store no longer holds.** A build module pushes a bundle or an archive as a bare blob
|
|
// (ADR 0498 §1); the store's nightly collector deletes every blob no manifest names; and only a take-in or a sweep holds
|
|
// one by its manifest. A key the controller has not taken in by the next collection names a blob that is gone. On
|
|
// 2026-10-24 that was node-tools', mesh-host's and the forge bridge's. Since issue 1933 the take-in refuses such a key;
|
|
// these two let the builder build it again: `held` before it reuses a done key, `artifacts-gone` when a take-in found it.
|
|
|
|
// heldMost is the most references one ask of held judges: a builder asks for one key's artifacts at a time.
|
|
const heldMost = 64
|
|
|
|
// heldAsk asks the store whether it holds one reference: nil, artifacts.Gone or another error. A variable so a test
|
|
// needs no store.
|
|
var heldAsk = func(ctx context.Context, address, reference string) error {
|
|
return artifacts.Store{Address: address}.Holds(ctx, reference)
|
|
}
|
|
|
|
// heldAddress is the artifact store's address the serving controller asks. A variable so a test needs no records.
|
|
var heldAddress = func(ctx context.Context) (string, error) {
|
|
d := doctorFrom
|
|
if d == nil || d.open == nil || d.open.inventory == nil {
|
|
return "", errors.New("this controller is not serving, so it asks the artifact store nothing: ask again, and " +
|
|
"the serving controller answers")
|
|
}
|
|
shelf, err := d.open.inventory.Catalogue(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
address, err := artifactStoreAddress(ctx, d.open.inventory, shelf, "")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if address == "" {
|
|
return "", errors.New("the artifact store is on no network the controller reaches")
|
|
}
|
|
return address, nil
|
|
}
|
|
|
|
// heldAnswer is the verb `held`: each reference given, held, gone (the store answered 404) or unknown with why. It
|
|
// holds and writes nothing: a reference the store holds and no record names is left for the take-in to hold.
|
|
func heldAnswer(ctx context.Context, given string) (any, error) {
|
|
var refs []string
|
|
for _, r := range strings.Split(given, ",") {
|
|
if r = strings.TrimSpace(r); r != "" && !slices.Contains(refs, r) {
|
|
refs = append(refs, r)
|
|
}
|
|
}
|
|
if len(refs) == 0 {
|
|
return nil, errors.New("held judges the references given, and none was")
|
|
}
|
|
if len(refs) > heldMost {
|
|
return nil, fmt.Errorf("held judges at most %d references at once, and %d were given", heldMost, len(refs))
|
|
}
|
|
for _, r := range refs {
|
|
if _, ours := catalogue.InArtifactStore(r); !ours {
|
|
return nil, fmt.Errorf("%s is not a reference into the mesh's artifact store (artifact-store://...): nothing was asked", r)
|
|
}
|
|
}
|
|
address, err := heldAddress(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
asking, stop := context.WithTimeout(ctx, 20*time.Second)
|
|
defer stop()
|
|
a := sdkdelivery.HeldAnswer{Store: address, Held: []string{}, Gone: []string{}}
|
|
for _, r := range refs {
|
|
switch err := heldAsk(asking, address, r); {
|
|
case err == nil:
|
|
a.Held = append(a.Held, r)
|
|
case errors.Is(err, artifacts.Gone):
|
|
a.Gone = append(a.Gone, r)
|
|
default:
|
|
if a.Unknown == nil {
|
|
a.Unknown = map[string]string{}
|
|
}
|
|
a.Unknown[r] = err.Error()
|
|
}
|
|
}
|
|
out := fmt.Sprintf("the artifact store at %s holds %d of the %d reference(s) asked", address, len(a.Held), len(refs))
|
|
if len(a.Gone) > 0 {
|
|
out += "; gone: " + strings.Join(a.Gone, ", ")
|
|
}
|
|
if len(a.Unknown) > 0 {
|
|
out += fmt.Sprintf("; %d could not be asked", len(a.Unknown))
|
|
}
|
|
return verbAnswer{Output: out + "\n", OK: true, Answer: a}, nil
|
|
}
|
|
|
|
// sayArtifactsGone says `artifacts-gone` for a builder's key the take-in refused because the store does not hold what
|
|
// it names (issue 1937): the builder hears it and builds the key again. Said from the serving controller only, and never
|
|
// in the take-in's way: a bus that cannot carry it is said in the journal.
|
|
var sayArtifactsGone = func(ctx context.Context, k builderkeys.Key, module string, gone []string) {
|
|
bus := checkEvents
|
|
if bus == nil || len(gone) == 0 {
|
|
return
|
|
}
|
|
key, _ := sdkdelivery.BuilderBuildKey(module, k.Fingerprint)
|
|
raw, err := sdkdelivery.EncodeArtifactsGone(sdkdelivery.ArtifactsGone{Module: module, Key: key,
|
|
Fingerprint: k.Fingerprint, Gone: gone, At: time.Now().UTC()})
|
|
if err != nil {
|
|
fmt.Printf("%s: its key names archives the store does not hold, and that could not be said: %v\n", module, err)
|
|
return
|
|
}
|
|
saying, stop := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
|
|
defer stop()
|
|
if err := bus.PublishSeatEvent(saying, link.MeshControllerSeat, link.KeyArtifactsGone, raw); err != nil {
|
|
fmt.Printf("%s: its key names archives the store does not hold, and %s could not be said: %v\n",
|
|
module, link.KeyArtifactsGone, err)
|
|
}
|
|
}
|