CAT-BI1: a copy keeps its wait for the mirror's outcome in the state mirror-waits
The image seat's published and refused reach one instance of the module, not necessarily the one whose copy waits, so an outcome held only in memory was dropped. The instance that hears it now writes it into the copy's key, and the copy reads it there, woken by its watch of the state; build_image_mirror_waits reads the state. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -44,7 +44,10 @@ The Dockerfile's steps may use the network (ADR 0487 §6); nothing a step can re
|
||||
digest the manifest pins (ADR 0498 §4): the holder copies it and says `published` or `refused` on the seat's events,
|
||||
which the manifest `consumes`; the step ends on that outcome (or fails `not stored:` after 30 minutes without one) and
|
||||
answers the digest and the seat's reference. A `from` without a digest fails `not pinned:` before the seat is asked. It
|
||||
needs no workspace, no buildah, and reads no outside registry itself.
|
||||
needs no workspace, no buildah, and reads no outside registry itself. The copy's wait is kept in the state
|
||||
`mirror-waits` under its digest before the seat is asked: the seat's events reach one instance of the module, which
|
||||
writes the outcome into that key, and the waiting copy reads it there, woken by its watch of the state. A copy removes
|
||||
its wait when it ends; one a stopped process left goes after two days. `build_image_mirror_waits` reads that state.
|
||||
- It refuses, naming why, an image whose build context is a whole repository (`context`) or whose `build.on` names
|
||||
another module's artifact (`unsupportedImage` in `build.go`): those forms belong to the images build-agent builds
|
||||
until it is removed at R4g.
|
||||
|
||||
@@ -14,6 +14,7 @@ var Kept = []announce.State{
|
||||
{Name: "keys", Readable: true},
|
||||
{Name: "steps", Readable: true},
|
||||
{Name: "pauses", Readable: true},
|
||||
{Name: MirrorWaitsState},
|
||||
}
|
||||
|
||||
// keepStates announces Kept as this module's (stdio.Keeps): main calls it before it serves.
|
||||
|
||||
@@ -4,7 +4,8 @@ package main
|
||||
// CAT-BI1): an upstream artifact is copied into the image seat by the seat's verb mirror, which the manifest
|
||||
// `invokes`, by the digest its manifest pins. The holder copies it, every platform and only what it lacks, and says
|
||||
// published or refused on the seat's events; the step waits on that outcome, heard through the manifest's `consumes`,
|
||||
// not on the call, so a large base never outlasts a call's bound. The copy runs no code of a change and reads no
|
||||
// not on the call, so a large base never outlasts a call's bound; the wait is kept in the state mirror-waits, so an
|
||||
// outcome another instance heard is matched there. The copy runs no code of a change and reads no
|
||||
// outside registry itself. An upstream without a digest is not copied: `not pinned:`. A copy the holder does not end
|
||||
// with the pinned digest stored is `not stored:`.
|
||||
|
||||
@@ -13,11 +14,14 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
reg "git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
|
||||
@@ -84,58 +88,131 @@ func (m *Module) copy(ctx context.Context, args map[string]any) (any, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("not pinned: %s: %v; an upstream artifact is copied by the digest its manifest pins", name, err)
|
||||
}
|
||||
if m.Outcomes == nil {
|
||||
if m.Outcomes == nil || m.Outcomes.Waits == nil {
|
||||
return nil, fmt.Errorf("%s%s: this process hears no outcome of the image seat", delivery.NotStored, name)
|
||||
}
|
||||
// Waited for before the ask, so an outcome said at once is not missed.
|
||||
key := MirrorWaitKey(digest)
|
||||
now := time.Now
|
||||
if m.Now != nil {
|
||||
now = m.Now
|
||||
}
|
||||
asked := CopyWait{Digest: digest, Module: module, Artifact: name, From: from, Node: m.Node,
|
||||
Asked: now().UTC(), Token: strconv.FormatInt(now().UnixNano(), 36) + "-" + m.Node}
|
||||
// Waited for before the ask, in memory and in the state mirror-waits, so an outcome said at once, or heard by another
|
||||
// instance of the module (the seat's events reach one of them), is not missed.
|
||||
heard, stop := m.Outcomes.Expect(digest)
|
||||
defer stop()
|
||||
if _, err := m.Outcomes.Waits.Put(key, asked); err != nil {
|
||||
return nil, fmt.Errorf("%s%s: keeping the copy's wait in %s: %v", delivery.NotStored, name, MirrorWaitsState, err)
|
||||
}
|
||||
defer m.Outcomes.drop(key, asked.Token)
|
||||
raw, err := m.call()("seat:"+buildstep.ImageSeat+"."+reg.VerbMirror, reg.MirrorInput{Name: module + "/" + name, Source: from, Digest: digest})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s%s: %s's mirror: %v", delivery.NotStored, name, buildstep.ImageSeat, err)
|
||||
}
|
||||
var asked reg.MirrorAnswer
|
||||
_ = json.Unmarshal(raw, &asked)
|
||||
m.say("asked %s to copy %s (%s); waiting for its outcome", buildstep.ImageSeat, from, asked.Reference)
|
||||
var answer reg.MirrorAnswer
|
||||
_ = json.Unmarshal(raw, &answer)
|
||||
m.say("asked %s to copy %s (%s); waiting for its outcome", buildstep.ImageSeat, from, answer.Reference)
|
||||
wait := m.MirrorWait
|
||||
if wait == 0 {
|
||||
wait = MirrorWait
|
||||
}
|
||||
select {
|
||||
case o := <-heard:
|
||||
if o.event == reg.EventRefused {
|
||||
return nil, fmt.Errorf("%s%s: %s refused the copy: %s", delivery.NotStored, name, buildstep.ImageSeat, o.Reason)
|
||||
bound := time.After(wait)
|
||||
for {
|
||||
// The state first: an outcome written there while this copy did not hear it is this copy's.
|
||||
if o, ok := m.Outcomes.kept(key); ok {
|
||||
return copyEnd(name, o, answer.Reference)
|
||||
}
|
||||
ref := o.Reference
|
||||
if ref == "" {
|
||||
ref = asked.Reference
|
||||
select {
|
||||
case <-heard:
|
||||
case <-bound:
|
||||
return nil, fmt.Errorf("%s%s: %s said no outcome of the copy of %s in %s", delivery.NotStored, name, buildstep.ImageSeat, from, wait)
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("%s%s: stopped while waiting for the copy of %s: %w", delivery.NotStored, name, from, ctx.Err())
|
||||
}
|
||||
return CopyResult{Artifact: name, Digest: digest, Reference: ref, Pushed: true}, nil
|
||||
case <-time.After(wait):
|
||||
return nil, fmt.Errorf("%s%s: %s said no outcome of the copy of %s in %s", delivery.NotStored, name, buildstep.ImageSeat, from, wait)
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("%s%s: stopped while waiting for the copy of %s: %w", delivery.NotStored, name, from, ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
// outcome is one of the image seat's published or refused, as heard.
|
||||
type outcome struct {
|
||||
// copyEnd is a copy's end on the outcome kept for it.
|
||||
func copyEnd(name string, o MirrorOutcome, asked string) (any, error) {
|
||||
if o.Event == reg.EventRefused {
|
||||
return nil, fmt.Errorf("%s%s: %s refused the copy: %s", delivery.NotStored, name, buildstep.ImageSeat, o.Reason)
|
||||
}
|
||||
ref := o.Reference
|
||||
if ref == "" {
|
||||
ref = asked
|
||||
}
|
||||
return CopyResult{Artifact: name, Digest: o.Digest, Reference: ref, Pushed: true}, nil
|
||||
}
|
||||
|
||||
// The state mirror-waits (CAT-BI1, the lead's ruling on A3r): each upstream copy waiting on the image seat's outcome,
|
||||
// keyed by its digest, with the outcome once an instance of the module heard it. The seat's published and refused reach
|
||||
// one instance of the module, not necessarily the one whose copy waits, and none while no instance listens; the
|
||||
// instance that hears it writes it to the copy's key, and the waiting copy reads it there, woken by its watch of the
|
||||
// state. A copy removes its key when it ends; one a stopped process left goes with the state's time to live.
|
||||
const MirrorWaitsState = "mirror-waits"
|
||||
|
||||
// MirrorWaitKey is a copy's key in mirror-waits: its digest, with the colon a key cannot hold as a dash.
|
||||
func MirrorWaitKey(digest string) string { return strings.Replace(digest, ":", "-", 1) }
|
||||
|
||||
// CopyWait is one value of mirror-waits: the copy asked, and its outcome once heard.
|
||||
type CopyWait struct {
|
||||
Digest string `json:"digest"`
|
||||
Module string `json:"module"`
|
||||
Artifact string `json:"artifact"`
|
||||
From string `json:"from"`
|
||||
Node string `json:"node"`
|
||||
Asked time.Time `json:"asked"`
|
||||
Token string `json:"token"`
|
||||
Outcome *MirrorOutcome `json:"outcome,omitempty"`
|
||||
}
|
||||
|
||||
// MirrorOutcome is the image seat's published or refused of a copy, as kept.
|
||||
type MirrorOutcome struct {
|
||||
reg.Outcome
|
||||
event string
|
||||
Event string `json:"event"`
|
||||
Heard time.Time `json:"heard"`
|
||||
By string `json:"by,omitempty"`
|
||||
}
|
||||
|
||||
// Outcomes hands the image seat's published and refused of an upstream copy to the copies waiting on its digest.
|
||||
// WaitState is the state mirror-waits: a key's value (nil for none), a write, a removal.
|
||||
type WaitState interface {
|
||||
Get(key string) (json.RawMessage, error)
|
||||
Put(key string, value any) (uint64, error)
|
||||
Delete(key string) error
|
||||
}
|
||||
|
||||
// stateWaits is mirror-waits as the process keeps it on the bus.
|
||||
type stateWaits struct{ s stdio.KeptState }
|
||||
|
||||
func (w stateWaits) Get(key string) (json.RawMessage, error) {
|
||||
e, err := w.s.Get(key)
|
||||
if err != nil || e == nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.Value, nil
|
||||
}
|
||||
func (w stateWaits) Put(key string, value any) (uint64, error) { return w.s.Put(key, value) }
|
||||
func (w stateWaits) Delete(key string) error { return w.s.Delete(key) }
|
||||
|
||||
// Outcomes hands the image seat's published and refused of an upstream copy to the copy waiting on its digest: written
|
||||
// into the copy's key in Waits, and a wake for the copies of this process waiting on it.
|
||||
type Outcomes struct {
|
||||
// Waits is the state mirror-waits; Node names this instance in an outcome it writes; Now is the clock.
|
||||
Waits WaitState
|
||||
Node string
|
||||
Now func() time.Time
|
||||
|
||||
mu sync.Mutex
|
||||
waiting map[string][]chan outcome
|
||||
waiting map[string][]chan struct{}
|
||||
}
|
||||
|
||||
// Expect waits for the outcome of digest's copy; stop ends the wait.
|
||||
func (o *Outcomes) Expect(digest string) (<-chan outcome, func()) {
|
||||
ch := make(chan outcome, 1)
|
||||
// Expect wakes on every change of digest's wait; stop ends it.
|
||||
func (o *Outcomes) Expect(digest string) (<-chan struct{}, func()) {
|
||||
ch := make(chan struct{}, 1)
|
||||
o.mu.Lock()
|
||||
if o.waiting == nil {
|
||||
o.waiting = map[string][]chan outcome{}
|
||||
o.waiting = map[string][]chan struct{}{}
|
||||
}
|
||||
o.waiting[digest] = append(o.waiting[digest], ch)
|
||||
o.mu.Unlock()
|
||||
@@ -156,27 +233,129 @@ func (o *Outcomes) Expect(digest string) (<-chan outcome, func()) {
|
||||
}
|
||||
}
|
||||
|
||||
// Heard takes one event of the seat (published or refused) in: an upstream copy's outcome reaches every copy waiting
|
||||
// on its digest; any other is passed.
|
||||
func (o *Outcomes) wake(digest string) {
|
||||
o.mu.Lock()
|
||||
defer o.mu.Unlock()
|
||||
for _, ch := range o.waiting[digest] {
|
||||
select {
|
||||
case ch <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// read is the wait kept at key, nil for none or one this module cannot read.
|
||||
func (o *Outcomes) read(key string) (*CopyWait, error) {
|
||||
raw, err := o.Waits.Get(key)
|
||||
if err != nil || len(raw) == 0 || string(raw) == "null" {
|
||||
return nil, err
|
||||
}
|
||||
var w CopyWait
|
||||
if json.Unmarshal(raw, &w) != nil || w.Digest == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return &w, nil
|
||||
}
|
||||
|
||||
// kept is the outcome kept for key's copy, when one was heard.
|
||||
func (o *Outcomes) kept(key string) (MirrorOutcome, bool) {
|
||||
w, err := o.read(key)
|
||||
if err != nil || w == nil || w.Outcome == nil {
|
||||
return MirrorOutcome{}, false
|
||||
}
|
||||
return *w.Outcome, true
|
||||
}
|
||||
|
||||
// drop removes key's wait when it is still the copy's own (token): a later copy of the same digest keeps its own.
|
||||
func (o *Outcomes) drop(key, token string) {
|
||||
if w, err := o.read(key); err == nil && w != nil && w.Token == token {
|
||||
_ = o.Waits.Delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
// Heard takes one event of the seat (published or refused) in: an upstream copy's outcome is written into its wait in
|
||||
// mirror-waits and wakes the copies of this process waiting on its digest; any other, and one no copy waits on, is
|
||||
// passed. A wait the state cannot take is an error, so the event is offered again.
|
||||
func (o *Outcomes) Heard(event string, body json.RawMessage) error {
|
||||
var got reg.Outcome
|
||||
if err := json.Unmarshal(body, &got); err != nil {
|
||||
return fmt.Errorf("the image seat's %s is not an outcome: %w", event, err)
|
||||
}
|
||||
if got.Kind != "upstream" || got.Digest == "" {
|
||||
if got.Kind != "upstream" || got.Digest == "" || o.Waits == nil {
|
||||
return nil
|
||||
}
|
||||
o.mu.Lock()
|
||||
defer o.mu.Unlock()
|
||||
for _, ch := range o.waiting[got.Digest] {
|
||||
select {
|
||||
case ch <- outcome{Outcome: got, event: event}:
|
||||
default:
|
||||
key := MirrorWaitKey(got.Digest)
|
||||
w, err := o.read(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading the copy's wait %s: %w", key, err)
|
||||
}
|
||||
if w == nil {
|
||||
return nil
|
||||
}
|
||||
if w.Outcome == nil {
|
||||
now := time.Now
|
||||
if o.Now != nil {
|
||||
now = o.Now
|
||||
}
|
||||
w.Outcome = &MirrorOutcome{Outcome: got, Event: event, Heard: now().UTC(), By: o.Node}
|
||||
if _, err := o.Waits.Put(key, w); err != nil {
|
||||
return fmt.Errorf("keeping the outcome of %s in %s: %w", key, MirrorWaitsState, err)
|
||||
}
|
||||
}
|
||||
o.wake(got.Digest)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Changed takes one change of mirror-waits in, as the module's watch of the state hands it: a wait whose outcome
|
||||
// another instance wrote wakes this process's copies of its digest.
|
||||
func (o *Outcomes) Changed(change stdio.StateChange) error {
|
||||
var w CopyWait
|
||||
if len(change.Value) == 0 || json.Unmarshal(change.Value, &w) != nil || w.Outcome == nil {
|
||||
return nil
|
||||
}
|
||||
o.wake(w.Digest)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MirrorWaitsTool reads mirror-waits.
|
||||
const MirrorWaitsTool = "build_image_mirror_waits"
|
||||
|
||||
// Waiting reads every copy waiting in mirror-waits, oldest first, with its outcome once heard.
|
||||
func (m *Module) Waiting() ([]CopyWait, error) {
|
||||
if m.ReadWaits.Keys == nil || m.ReadWaits.Get == nil {
|
||||
return nil, fmt.Errorf("the state %s cannot be read here", MirrorWaitsState)
|
||||
}
|
||||
keys, err := m.ReadWaits.Keys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raws, err := readValues(keys, m.ReadWaits.Get)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := []CopyWait{}
|
||||
for _, raw := range raws {
|
||||
var w CopyWait
|
||||
if len(raw) == 0 || json.Unmarshal(raw, &w) != nil || w.Digest == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, w)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Asked.Before(out[j].Asked) })
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *Module) mirrorWaitsTool() stdio.Tool {
|
||||
return stdio.Tool{Name: MirrorWaitsTool,
|
||||
Description: "The upstream copies waiting on the image seat's outcome of their mirror (the module's state " + MirrorWaitsState +
|
||||
"), oldest first: the digest, the module and artifact, the reference, the node whose copy asked and when, and the " +
|
||||
"seat's published or refused once an instance heard it. A copy removes its wait when it ends. Read-only.",
|
||||
Run: func(map[string]any) (any, error) {
|
||||
w, err := m.Waiting()
|
||||
return map[string]any{"waits": w}, err
|
||||
}}
|
||||
}
|
||||
|
||||
// Plain is a long step that needs nothing of the machine: no guard. The copy runs no buildah.
|
||||
func (m *Module) Plain(tool string, run func(ctx context.Context, args map[string]any) (any, error)) func(map[string]any) (any, error) {
|
||||
return func(args map[string]any) (any, error) {
|
||||
|
||||
@@ -35,8 +35,15 @@ func TestTheCopyIsTheImageSeatsMirrorAndEndsOnItsOutcome(t *testing.T) {
|
||||
from string
|
||||
mirrored func(reg.MirrorInput) (string, reg.Outcome)
|
||||
reason string
|
||||
// elsewhere, watched, atOnce: who hears the outcome and when (the fake's fields of the same names).
|
||||
elsewhere, watched, atOnce bool
|
||||
}{
|
||||
"published": {from: from},
|
||||
"heard by another instance, handed over by the watch of mirror-waits": {from: from, elsewhere: true, watched: true},
|
||||
"heard by another instance before this copy read its wait": {from: from, elsewhere: true, atOnce: true},
|
||||
"heard by another instance that refused it": {from: from, elsewhere: true, atOnce: true, mirrored: func(in reg.MirrorInput) (string, reg.Outcome) {
|
||||
return reg.EventRefused, reg.Outcome{Kind: "upstream", Digest: in.Digest, Reason: "the upstream answered 404"}
|
||||
}, reason: "refused the copy: the upstream answered 404"},
|
||||
"refused": {from: from, mirrored: func(in reg.MirrorInput) (string, reg.Outcome) {
|
||||
return reg.EventRefused, reg.Outcome{Kind: "upstream", Digest: in.Digest, Reason: "the upstream answered 404"}
|
||||
}, reason: "not stored: server: container-image-registry refused the copy: the upstream answered 404"},
|
||||
@@ -50,10 +57,14 @@ func TestTheCopyIsTheImageSeatsMirrorAndEndsOnItsOutcome(t *testing.T) {
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
f := newFake(t)
|
||||
f.mirrored = c.mirrored
|
||||
f.mirrored, f.elsewhere, f.watched, f.atOnce = c.mirrored, c.elsewhere, c.watched, c.atOnce
|
||||
states := newStates()
|
||||
m := moduleWith(t, f, states)
|
||||
m.MirrorWait = 200 * time.Millisecond
|
||||
waits := m.Outcomes.Waits.(*memWaits)
|
||||
m.ReadWaits = StateRead{Keys: waits.Keys, Get: waits.Get}
|
||||
var asked []CopyWait
|
||||
f.atAsk = func() { asked, _ = m.Waiting() }
|
||||
res, st := runCopy(t, m, states, map[string]any{"module": "nats", "artifact": "server", "from": c.from})
|
||||
if c.reason != "" {
|
||||
if st.State != delivery.StepFailed || !strings.Contains(st.Reason, c.reason) {
|
||||
@@ -74,6 +85,13 @@ func TestTheCopyIsTheImageSeatsMirrorAndEndsOnItsOutcome(t *testing.T) {
|
||||
if len(f.lines()) != 0 {
|
||||
t.Errorf("the copy ran commands: %v", f.lines())
|
||||
}
|
||||
// The read tool of mirror-waits answers the copy's wait while it waits.
|
||||
if len(asked) != 1 || asked[0].Digest != d || asked[0].From != from || asked[0].Node != "ace" || asked[0].Outcome != nil {
|
||||
t.Errorf("%s at the ask = %+v", MirrorWaitsTool, asked)
|
||||
}
|
||||
if keys, _ := waits.Keys(); len(keys) != 0 {
|
||||
t.Errorf("an ended copy left its wait in mirror-waits: %v", keys)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
|
||||
reg "git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
@@ -87,6 +89,60 @@ type fake struct {
|
||||
mirrors []reg.MirrorInput
|
||||
mirrored func(reg.MirrorInput) (string, reg.Outcome)
|
||||
outcomes *Outcomes
|
||||
// elsewhere: the outcome is heard by another instance of the module over the same mirror-waits, not by this
|
||||
// process; watched: this process's watch of mirror-waits hands that instance's write over; atOnce: the outcome is
|
||||
// said before the mirror answers.
|
||||
elsewhere, watched, atOnce bool
|
||||
// atAsk runs when the mirror is asked.
|
||||
atAsk func()
|
||||
}
|
||||
|
||||
// memWaits is the state mirror-waits in memory; watch, when set, is handed every write as the state's watch hands it.
|
||||
type memWaits struct {
|
||||
mu sync.Mutex
|
||||
m map[string]json.RawMessage
|
||||
watch func(stdio.StateChange) error
|
||||
}
|
||||
|
||||
func (w *memWaits) Get(key string) (json.RawMessage, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
return w.m[key], nil
|
||||
}
|
||||
|
||||
func (w *memWaits) Put(key string, value any) (uint64, error) {
|
||||
b, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
w.mu.Lock()
|
||||
if w.m == nil {
|
||||
w.m = map[string]json.RawMessage{}
|
||||
}
|
||||
w.m[key] = b
|
||||
watch := w.watch
|
||||
w.mu.Unlock()
|
||||
if watch != nil {
|
||||
_ = watch(stdio.StateChange{State: MirrorWaitsState, Key: key, Op: "put", Value: b})
|
||||
}
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func (w *memWaits) Delete(key string) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
delete(w.m, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *memWaits) Keys() ([]string, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
keys := []string{}
|
||||
for k := range w.m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
const imageDigest = "sha256:1111111111111111111111111111111111111111111111111111111111111111"
|
||||
@@ -163,15 +219,30 @@ func (f *fake) call(key string, body any) (json.RawMessage, error) {
|
||||
var in reg.MirrorInput
|
||||
_ = json.Unmarshal(raw, &in)
|
||||
f.mirrors = append(f.mirrors, in)
|
||||
if f.atAsk != nil {
|
||||
f.atAsk()
|
||||
}
|
||||
event, o := reg.EventPublished, reg.Outcome{Kind: "upstream", Name: "upstream/" + in.Name, Digest: in.Digest,
|
||||
Holder: "distribution", Reference: reg.Seat + "/upstream/" + in.Name + "@" + in.Digest}
|
||||
if f.mirrored != nil {
|
||||
event, o = f.mirrored(in)
|
||||
}
|
||||
if event != "" && f.outcomes != nil {
|
||||
// Said after the answer, on the seat's events, as the holder's watch says it.
|
||||
hearer := f.outcomes
|
||||
if f.elsewhere {
|
||||
waits := f.outcomes.Waits.(*memWaits)
|
||||
if f.watched {
|
||||
waits.watch = f.outcomes.Changed
|
||||
}
|
||||
hearer = &Outcomes{Waits: waits, Node: "g14"}
|
||||
}
|
||||
b, _ := json.Marshal(o)
|
||||
go func() { time.Sleep(time.Millisecond); _ = f.outcomes.Heard(event, b) }()
|
||||
if f.atOnce {
|
||||
_ = hearer.Heard(event, b)
|
||||
} else {
|
||||
// Said after the answer, on the seat's events, as the holder's watch says it.
|
||||
go func() { _ = hearer.Heard(event, b) }()
|
||||
}
|
||||
}
|
||||
return json.Marshal(reg.MirrorAnswer{Digest: in.Digest, Reference: reg.Seat + "/upstream/" + in.Name + "@" + in.Digest})
|
||||
}
|
||||
@@ -248,7 +319,7 @@ func moduleWith(t *testing.T, f *fake, states *keptStates) *Module {
|
||||
}
|
||||
m := &Module{
|
||||
Node: "ace", Account: "mesh-build-image", UID: "1234", Home: t.TempDir(), Work: t.TempDir(), Store: t.TempDir(),
|
||||
Steps: states.putStep, Ctx: context.Background(), Run: f.run, Call: f.call, Outcomes: &Outcomes{},
|
||||
Steps: states.putStep, Ctx: context.Background(), Run: f.run, Call: f.call, Outcomes: &Outcomes{Waits: &memWaits{}},
|
||||
SubUIDFile: write("subuid", "someone:100000:65536\nmesh-build-image:624288:65536\n"),
|
||||
SubGIDFile: write("subgid", "mesh-build-image:624288:65536\n"),
|
||||
Tun: write("tun", ""),
|
||||
|
||||
@@ -45,7 +45,9 @@ func main() {
|
||||
if u, err := user.Current(); err == nil {
|
||||
m.Home, m.Account, m.UID = u.HomeDir, u.Username, u.Uid
|
||||
}
|
||||
m.Outcomes = &Outcomes{}
|
||||
waits := stdio.State(MirrorWaitsState)
|
||||
m.Outcomes = &Outcomes{Waits: stateWaits{waits}, Node: m.Node}
|
||||
m.ReadWaits = readOf(waits)
|
||||
m.Settings = m.Home + "/settings.json"
|
||||
m.Run = StepRunner(m.Home)
|
||||
// Outside a launch by the runtime — a build, a check — there is no node and no state to write.
|
||||
@@ -68,8 +70,10 @@ func main() {
|
||||
go m.endLeftRunning(ctx, m.stepScan(), booted, logf)
|
||||
}
|
||||
if m.Node != "" {
|
||||
// The image seat's outcomes, which a copy waits on (the manifest's consumes).
|
||||
// The image seat's outcomes, which a copy waits on (the manifest's consumes), and the copies' waits in
|
||||
// mirror-waits, where another instance writes an outcome it heard.
|
||||
go hearOutcomes(ctx, m.Outcomes)
|
||||
go waits.Follow("", m.Outcomes.Changed)
|
||||
}
|
||||
if m.Node != "" {
|
||||
// The seat's asks, pulled once Serve has the runtime's channel; a pull before it is said and tried again.
|
||||
|
||||
@@ -100,20 +100,21 @@ func TestWorkAndStoreDirectoriesLieOutsideTheHome(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The states are the two the module keeps, the one seat it claims is its build seat (ADR 0525 §2), the health is its own tool, which the manifest also lists,
|
||||
// The states are the four the module keeps (mirror-waits a copy's wait for its outcome, CAT-BI1), the one seat it claims is its build seat (ADR 0525 §2), the health is its own tool, which the manifest also lists,
|
||||
// and the registry's address is bound into a file of the home that the program reads.
|
||||
func TestManifestKeepsItsStatesClaimsItsBuildSeatAndDeclaresItsHealthTool(t *testing.T) {
|
||||
m := readManifest(t)
|
||||
states := []string{}
|
||||
for _, s := range m.State {
|
||||
states = append(states, s.Name)
|
||||
// A step's key is per step and dropped after its age, so the bucket cannot fill; a pause has no age.
|
||||
if (s.Name == "steps") != (s.TTLSeconds > 0) {
|
||||
t.Errorf("state %s has ttl-seconds %d: only steps has an age", s.Name, s.TTLSeconds)
|
||||
// A step's key is per step and dropped after its age, so the bucket cannot fill, and so is a copy's wait, which a
|
||||
// stopped process leaves; a pause has no age.
|
||||
if (s.Name == "steps" || s.Name == MirrorWaitsState) != (s.TTLSeconds > 0) {
|
||||
t.Errorf("state %s has ttl-seconds %d: only steps and %s have an age", s.Name, s.TTLSeconds, MirrorWaitsState)
|
||||
}
|
||||
}
|
||||
slices.Sort(states)
|
||||
if want := []string{"keys", "pauses", "steps"}; !slices.Equal(states, want) {
|
||||
if want := []string{"keys", MirrorWaitsState, "pauses", "steps"}; !slices.Equal(states, want) {
|
||||
t.Errorf("state = %v, want %v", states, want)
|
||||
}
|
||||
if len(m.Claims) != 1 || m.Claims[0].Name != Seat || !slices.Equal(m.Claims[0].Serves, []string{"current", "pause", "resume"}) {
|
||||
|
||||
@@ -43,8 +43,10 @@ type Module struct {
|
||||
Tun string
|
||||
// Call asks a seat's verb (the image seat's publish, publish-part and mirror); the runtime's stdio.Ask when nil.
|
||||
Call buildstep.SeatCall
|
||||
// Outcomes are the image seat's published and refused as this process hears them, which a copy waits on.
|
||||
Outcomes *Outcomes
|
||||
// Outcomes are the image seat's published and refused as this process hears them, which a copy waits on, kept in the
|
||||
// state mirror-waits; ReadWaits reads that state for its read tool.
|
||||
Outcomes *Outcomes
|
||||
ReadWaits StateRead
|
||||
// RemoteBase is where repositories are fetched from, "ssh://mesh-forge/" when empty; FetchOptions are the options of
|
||||
// the SDK's fetch and merge. A test points them at a local repository (buildstep.AllowFileRemote).
|
||||
RemoteBase string
|
||||
|
||||
@@ -45,6 +45,7 @@ func (m *Module) Tools(steps []StepTool) []stdio.Tool {
|
||||
tools = append(tools, t)
|
||||
}
|
||||
tools = append(tools, m.ReadTools()...)
|
||||
tools = append(tools, m.mirrorWaitsTool())
|
||||
tools = append(tools, buildstep.StepEndTool(StepEndTool, m.Node, m.stepScan(), m.Held))
|
||||
tools = append(tools, m.releaseTool())
|
||||
return append(tools,
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
],
|
||||
"state": [
|
||||
"keys",
|
||||
{"name": "steps", "ttl-seconds": 172800}, "pauses"
|
||||
{"name": "steps", "ttl-seconds": 172800}, "pauses",
|
||||
{"name": "mirror-waits", "ttl-seconds": 172800}
|
||||
],
|
||||
"claims": [
|
||||
{
|
||||
@@ -42,6 +43,7 @@
|
||||
"build_image_fetch",
|
||||
"build_image_build",
|
||||
"build_image_copy",
|
||||
"build_image_mirror_waits",
|
||||
"build_image_release",
|
||||
"build_image_step",
|
||||
"build_image_steps",
|
||||
|
||||
Reference in New Issue
Block a user