AS21: the assigner follows the watched branches the gate's source and the registrations name, reads no gate bucket and names no branch
The assigner follows the catalog half through meshcatalogclient's branch follower (the registrations' branches, before the first registrations those of the gate's verb `source` and its event `source-moved`, §507, §532): its trunk is the first watched branch, never a fixed "main" (§7). It reads neither the gate's `source` nor `merges` (§431); the Approve of a release is no yes to a move (the release lookup goes). The follower tells a part of each branch it adopts (Branches.Adopted). Builds: design 47 G §507, §532, §431, §7 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -19,3 +19,6 @@ func endDone(t testing.TB, w *world, module, fp, manifest string) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// trunkBranch is the first watched branch the tests give the world: the code names none (design 47 G §507).
|
||||
const trunkBranch = "main"
|
||||
|
||||
@@ -44,7 +44,8 @@ func (p *passer) keepGraph(ctx context.Context) {
|
||||
}
|
||||
in := assigngraph.Inputs{Halves: g.halves(), Placements: p.w.assignments(), Capabilities: p.capabilityDigests()}
|
||||
// With no pipeline ends kept, the wants are the trunk-only form (pass.go): a placement naming no branch runs the trunk's.
|
||||
in.FirstBranch = func(string) string { return trunkBranch }
|
||||
first := p.w.trunkName()
|
||||
in.FirstBranch = func(string) string { return first }
|
||||
if p.ends != nil {
|
||||
in.FirstBranch = func(module string) string { return p.ends.branchOf(module, "") }
|
||||
}
|
||||
@@ -66,7 +67,7 @@ func (p *passer) keepGraph(ctx context.Context) {
|
||||
g.last, g.has, g.kept, g.ok = c.Revision, true, rev, true
|
||||
g.source = 0
|
||||
for _, cat := range rev.Catalogs {
|
||||
if cat.Branch == trunkBranch {
|
||||
if cat.Branch == p.w.trunkName() {
|
||||
g.source = cat.Source
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,12 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
|
||||
// halves follows the catalog half of each watched branch through the seat mesh-catalog (design 47 G §14, §184, §195): one
|
||||
// meshcatalogclient.Catalog per branch, adopted through the module's views reader (A-26), which asks the first view of the verb
|
||||
// `revision`, hears each `catalog-revision` of that branch and raises and clears the follower's findings; each entry is taken
|
||||
// into the world as the value and the graph node the rules read. Of the view the assigner reads only the catalog half, never its
|
||||
// own output back (§195).
|
||||
// halves follows the catalog half of each watched branch through the seat mesh-catalog (design 47 G §14, §184, §195, §507, §532): the
|
||||
// branch follower of meshcatalogclient, adopted on the module's views reader (A-26), follows the branches the registrations name
|
||||
// and, before the first registrations, the branches the gate's verb `source` answers and its event `source-moved` brings; each
|
||||
// branch's view asks the verb `revision`, hears each `catalog-revision` of that branch and raises and clears the follower's findings;
|
||||
// each entry is taken into the world as the value and the graph node the rules read. Of the view the assigner reads only the catalog
|
||||
// half, never its own output back (§195).
|
||||
type halves struct {
|
||||
w *world
|
||||
call mcc.Call
|
||||
@@ -29,42 +30,70 @@ type halves struct {
|
||||
logf func(string, ...any)
|
||||
ctx context.Context
|
||||
|
||||
mu sync.Mutex
|
||||
cats map[string]*mcc.Catalog
|
||||
// settled is told once when a branch's first view is answered, for the first pass to wait on (the trunk's alone).
|
||||
branches *mcc.Branches
|
||||
mu sync.Mutex
|
||||
// settled is told once when the first watched branch's first view is answered, for the first pass to wait on.
|
||||
settled chan<- struct{}
|
||||
told bool
|
||||
}
|
||||
|
||||
// follow adopts the catalog of branch through the reader, once; true when it was made now.
|
||||
func (h *halves) follow(branch string) bool {
|
||||
h.mu.Lock()
|
||||
if h.cats[branch] != nil {
|
||||
h.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
c, _, err := mcc.WireCatalogProcess(h.reader, h.call, branch, func(module string, entry json.RawMessage, deleted bool, _ time.Time) {
|
||||
h.changed(branch, module, entry, deleted)
|
||||
})
|
||||
// follow makes the branch follower on the reader; the reader's start asks the first views.
|
||||
func (h *halves) follow() error {
|
||||
b, err := mcc.FollowBranchesProcess(h.reader, h.call)
|
||||
if err != nil {
|
||||
h.mu.Unlock()
|
||||
h.logf("the catalog half of branch %q is not followed: %v", branch, err)
|
||||
return false
|
||||
return err
|
||||
}
|
||||
if h.cats == nil {
|
||||
h.cats = map[string]*mcc.Catalog{}
|
||||
b.Logf = h.logf
|
||||
b.Changed = func(branch, module string, entry json.RawMessage, deleted bool, _ time.Time) {
|
||||
h.changed(branch, module, entry, deleted)
|
||||
}
|
||||
h.cats[branch] = c
|
||||
b.Adopted = h.adopted
|
||||
h.mu.Lock()
|
||||
h.branches = b
|
||||
h.mu.Unlock()
|
||||
if branch == trunkBranch && h.settled != nil {
|
||||
go func() {
|
||||
select {
|
||||
case <-c.Ready():
|
||||
h.settled <- struct{}{}
|
||||
case <-h.ctx.Done():
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// first is the first watched branch followed, the trunk's: "" while none is.
|
||||
func (h *halves) first() string {
|
||||
h.mu.Lock()
|
||||
b := h.branches
|
||||
h.mu.Unlock()
|
||||
if b == nil {
|
||||
return ""
|
||||
}
|
||||
return true
|
||||
if f := b.Followed(); len(f) > 0 {
|
||||
return f[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// adopted is told of each branch whose view the follower adopted: the first branch is the trunk's, and the first pass waits for its
|
||||
// first view.
|
||||
func (h *halves) adopted(branch string, c *mcc.Catalog) {
|
||||
first := h.first()
|
||||
if first == "" {
|
||||
return
|
||||
}
|
||||
h.w.setTrunk(first)
|
||||
h.wake()
|
||||
if branch != first || h.settled == nil {
|
||||
return
|
||||
}
|
||||
h.mu.Lock()
|
||||
told := h.told
|
||||
h.told = true
|
||||
h.mu.Unlock()
|
||||
if told {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
select {
|
||||
case <-c.Ready():
|
||||
h.settled <- struct{}{}
|
||||
case <-h.ctx.Done():
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// changed takes one entry of branch's half into the world, or a module taken out of it.
|
||||
@@ -114,11 +143,17 @@ func fromEntry(e meshcatalog.CatalogEntry) (meshcatalog.ModulesByModuleValue, gr
|
||||
// views is the catalog graph revision held now of each followed branch whose first view is answered.
|
||||
func (h *halves) views() map[string]mcc.Half {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
out := make(map[string]mcc.Half, len(h.cats))
|
||||
for branch, c := range h.cats {
|
||||
if half, ok := c.View(); ok {
|
||||
out[branch] = half
|
||||
b := h.branches
|
||||
h.mu.Unlock()
|
||||
out := map[string]mcc.Half{}
|
||||
if b == nil {
|
||||
return out
|
||||
}
|
||||
for _, branch := range b.Followed() {
|
||||
if c, ok := b.Catalog(branch); ok {
|
||||
if half, ok := c.View(); ok {
|
||||
out[branch] = half
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
|
||||
@@ -44,6 +44,7 @@ import (
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
|
||||
mdseat "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/announce"
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
@@ -1954,6 +1955,29 @@ func serveCatalog(t *testing.T, b *bed, nc *nats.Conn) {
|
||||
raw, _ := json.Marshal(map[string]any{"result": meshcatalog.RevisionAnswer{Revision: &rev, Entries: entries}, "node": "n0"})
|
||||
_ = m.Respond(raw)
|
||||
}
|
||||
// The gate's verb `source` given no branch (design 47 G §507, §690): the branches it keeps a source revision of, which the assigner
|
||||
// follows before the first registrations, and the trunk's `main` first.
|
||||
source := func(m *nats.Msg) {
|
||||
b.cat.mu.Lock()
|
||||
defer b.cat.mu.Unlock()
|
||||
names := map[string]bool{"main": true}
|
||||
for branch := range b.cat.rev {
|
||||
names[branch] = true
|
||||
}
|
||||
var escaped []string
|
||||
for _, n := range slices.Sorted(maps.Keys(names)) {
|
||||
escaped = append(escaped, keys.EscapeName(n))
|
||||
}
|
||||
raw, _ := json.Marshal(map[string]any{"result": mdseat.SourceAnswer{Branches: escaped}, "node": "n0"})
|
||||
_ = m.Respond(raw)
|
||||
}
|
||||
for _, subject := range []string{"mesh.seat.mesh-delivery.tool.source", "mesh.seat.mesh-delivery.call.source.*", "mesh.seat.mesh-delivery.tool.source.*"} {
|
||||
sub, err := nc.Subscribe(subject, source)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = sub.Unsubscribe() })
|
||||
}
|
||||
for _, subject := range []string{"mesh.seat.mesh-catalog.tool.revision", "mesh.seat.mesh-catalog.call.revision.*", "mesh.seat.mesh-catalog.tool.revision.*"} {
|
||||
sub, err := nc.Subscribe(subject, answer)
|
||||
if err != nil {
|
||||
|
||||
@@ -66,8 +66,6 @@ var readBuckets = []string{
|
||||
buckets.Presence,
|
||||
buckets.Have,
|
||||
buckets.Provisions,
|
||||
buckets.Source,
|
||||
buckets.Merges,
|
||||
}
|
||||
|
||||
// readFamilies are the states kept one bucket per node the assigner reads, each named in its manifest's reads: every
|
||||
@@ -167,12 +165,6 @@ func open(ctx context.Context, js process.BucketOpener, logf func(string, ...any
|
||||
logf("the bucket %s cannot be opened (%v): the assigner's conditions are said straight to the router until it starts again", name, err)
|
||||
continue
|
||||
}
|
||||
if (name == buckets.Source || name == buckets.Merges) && err != nil {
|
||||
// The gate's source and merges (ADR 0601 §1): without them no release is known to carry a build, so every move of
|
||||
// a record module is asked of a person, as before.
|
||||
logf("the bucket %s cannot be opened (%v): no release's Approve answers a record ask until it is bound", name, err)
|
||||
continue
|
||||
}
|
||||
if name == engineReadsBucket && err != nil {
|
||||
// New with ADR 0618 §3: without it the statements the verb answers are kept in memory only, and
|
||||
// assigner_engine_reads lists nothing.
|
||||
@@ -214,7 +206,7 @@ func open(ctx context.Context, js process.BucketOpener, logf func(string, ...any
|
||||
func startsWithout(name string) bool {
|
||||
switch name {
|
||||
case plc.SeatHoldersBucket, buckets.Provisions, assign.PoliciesBucket, assign.FindingsBucket,
|
||||
buckets.Puts, buckets.Source, buckets.Merges, engineReadsBucket, retiredBucket, writtenBucket, buckets.Waits, buckets.Inputs:
|
||||
buckets.Puts, engineReadsBucket, retiredBucket, writtenBucket, buckets.Waits, buckets.Inputs:
|
||||
return true
|
||||
}
|
||||
return slices.Contains(seatBuckets, name) || name == buckets.Grants
|
||||
@@ -338,8 +330,6 @@ var watched = []string{
|
||||
assign.PoliciesBucket,
|
||||
buckets.Provisions,
|
||||
plc.SeatHoldersBucket,
|
||||
buckets.Source,
|
||||
buckets.Merges,
|
||||
engineReadsBucket,
|
||||
writtenBucket,
|
||||
buckets.Waits,
|
||||
@@ -453,8 +443,8 @@ func follow(ctx context.Context, js jetstream.JetStream, nc process.Subscriber,
|
||||
}
|
||||
started++
|
||||
var opts []jetstream.WatchOpt
|
||||
if name == buckets.Have || name == buckets.Source {
|
||||
// what each node ran healthy before, for a set-back; and the release behind each source revision (ADR 0601 §1)
|
||||
if name == buckets.Have {
|
||||
// what each node ran healthy before, for a set-back
|
||||
opts = append(opts, jetstream.IncludeHistory())
|
||||
}
|
||||
if err := watch(name, opts...); err != nil {
|
||||
@@ -562,13 +552,7 @@ func follow(ctx context.Context, js jetstream.JetStream, nc process.Subscriber,
|
||||
// The runner's pipeline-ended and group-ended (design 47 G §29): kept in the state written, which the wants follow
|
||||
// once the first registrations are written (§95, §353).
|
||||
p.ends = newPipelineEnds(kvEnds{ctx, kv}, wake, logger.Printf)
|
||||
if iv != nil && iv.h != nil {
|
||||
p.ends.onBranches = func(branches []string) {
|
||||
for _, b := range branches {
|
||||
iv.h.follow(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
p.ends.trunk = w.trunkName
|
||||
err := hearEnds(hub, p.ends)
|
||||
if err == nil && bus != nil {
|
||||
err = followRegistrations(ctx, hub, bus.Call, p.ends)
|
||||
|
||||
@@ -247,7 +247,7 @@ func (p *passer) wants(ctx context.Context) {
|
||||
views[branch] = v
|
||||
return v
|
||||
}
|
||||
nodes := viewOf(trunkBranch).nodes
|
||||
nodes := viewOf(p.w.trunkName()).nodes
|
||||
p.engineReads(nodes)
|
||||
nodesOf := map[string][]string{}
|
||||
for node, modules := range assigned {
|
||||
@@ -319,8 +319,7 @@ func (p *passer) wants(ctx context.Context) {
|
||||
Allowed: func(node, digest string) bool {
|
||||
key, err := asks.RecordAskKey(node, name, digest)
|
||||
return err == nil && p.answeredWith(key, assign.OptionYes)
|
||||
},
|
||||
Release: func(fp string) (string, bool) { return p.w.release(name, fp) }}
|
||||
}}
|
||||
if fp != "" {
|
||||
held, _ := mwc.HeldKey(name, fp)
|
||||
grant, _ := grantsstate.PartGrantKey(name, fp)
|
||||
@@ -1061,9 +1060,8 @@ func (p *passer) backupAsk(ctx context.Context, key, node, module string, want m
|
||||
// yeses writes a placed node's wants naming the build alone, and sees to the moves a node that composes its own judged a
|
||||
// person must allow (ADR 0547 §3): for a have of phase
|
||||
// waiting-yes it raises the record ask once, naming the composition waiting and the node's judgement, and on a yes writes
|
||||
// the want again naming it; a yes once written is kept while the want stays at its build, so the node is not asked again. A
|
||||
// move of the build alone (the have says so) to a build an approved release carries is given its yes by that Approve, the
|
||||
// want naming the release, with no ask (ADR 0601 §1).
|
||||
// the want again naming it; a yes once written is kept while the want stays at its build, so the node is not asked again. No
|
||||
// release's Approve is a yes: the Approve per merge goes and the assigner reads no `merges` (design 47 G §431).
|
||||
func (p *passer) yeses(ctx context.Context, module string, m assign.Module, before map[string]mwc.Want, out *assign.Outcome) {
|
||||
for _, node := range m.Nodes {
|
||||
if m.Self == nil || !m.Self(node) {
|
||||
@@ -1092,14 +1090,6 @@ func (p *passer) yeses(ctx context.Context, module string, m assign.Module, befo
|
||||
switch {
|
||||
case m.Allowed != nil && m.Allowed(node, h.Waits):
|
||||
yes, release = h.Waits, ""
|
||||
case h.BuildAlone && h.Fp != w.Fp && m.Release != nil:
|
||||
// A move of the build alone, to the build an approved release carries: that Approve is the yes (ADR 0601 §1),
|
||||
// named in the want. A move that changes a setting or a secret too, or one the node could not tell, is asked.
|
||||
if r, ok := m.Release(w.Fp); ok {
|
||||
yes, release = h.Waits, r
|
||||
break
|
||||
}
|
||||
p.recordAsk(ctx, module, w.Fp, assign.RecordNeed{Node: node, Digest: h.Waits, Judgement: h.Judgement, Words: h.Detail})
|
||||
default:
|
||||
p.recordAsk(ctx, module, w.Fp, assign.RecordNeed{Node: node, Digest: h.Waits, Judgement: h.Judgement, Words: h.Detail})
|
||||
}
|
||||
@@ -1409,14 +1399,14 @@ type group struct {
|
||||
func (p *passer) groups(module string, nodes []string, placed map[string]string) []group {
|
||||
by := map[string][]string{}
|
||||
for _, node := range nodes {
|
||||
b := trunkBranch
|
||||
b := p.w.trunkName()
|
||||
if p.ends.following() {
|
||||
b = p.ends.branchOf(module, placed[node])
|
||||
}
|
||||
by[b] = append(by[b], node)
|
||||
}
|
||||
if len(by) == 0 {
|
||||
b := trunkBranch
|
||||
b := p.w.trunkName()
|
||||
if p.ends.following() {
|
||||
b = p.ends.branchOf(module, "")
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
mdc "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshdeliveryclient"
|
||||
mwc "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
|
||||
mdseat "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-teller/tellerevents"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
|
||||
|
||||
@@ -69,7 +68,9 @@ func (m *memKV) Delete(_ context.Context, key string, _ ...jetstream.KVDeleteOpt
|
||||
|
||||
// newPasser is a pass on a fresh world, as after a start, writing to the buckets given, at the clock given.
|
||||
func newPasser(kvs kvSet, at time.Time) *passer {
|
||||
return &passer{kvs: kvs, w: newWorld(), logf: func(string, ...any) {}, said: map[string]bool{}, answeredAt: map[string]uint64{},
|
||||
w := newWorld()
|
||||
w.setTrunk(trunkBranch)
|
||||
return &passer{kvs: kvs, w: w, logf: func(string, ...any) {}, said: map[string]bool{}, answeredAt: map[string]uint64{},
|
||||
now: func() time.Time { return at }}
|
||||
}
|
||||
|
||||
@@ -1163,70 +1164,29 @@ func TestARecordAskOfAHeldBackNodeSurvivesPassesAndItsYesMovesTheWant(t *testing
|
||||
aReleasesApproveAnswersTheMoveOfItsBuildAlone(t)
|
||||
}
|
||||
|
||||
// ADR 0601 §1: a placed node's have waiting for a yes to a move of the build alone, to a build an approved release carries, is
|
||||
// given its yes by that Approve, the want naming the release, and no record ask is raised; a move that changes a setting or a
|
||||
// secret too, one at the build it runs, and one no approved release carries are asked of a person as before.
|
||||
// Design 47 G §431: no release's Approve is a yes (the assigner reads no `merges`): a placed node's have waiting for a yes to a move
|
||||
// of the build alone is asked of a person, the want naming no release and no yes.
|
||||
func aReleasesApproveAnswersTheMoveOfItsBuildAlone(t *testing.T) {
|
||||
fpOld := meshcatalogclient.SourcePrefix2 + strings.Repeat("a", 64)
|
||||
waiting, was := "sha256:"+strings.Repeat("e", 64), "sha256:"+strings.Repeat("d", 64)
|
||||
commit := strings.Repeat("1", 40)
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
alone bool
|
||||
at string // the fingerprint the have runs
|
||||
mergedAt string // the commit the release merged web at; "" for no merge of the release
|
||||
fpSrc int64 // the source revision at which web's fingerprint last changed
|
||||
yes, asked bool
|
||||
}{
|
||||
{"the build alone, its release approved", true, fpOld, commit, 7, true, false},
|
||||
{"a setting changed too", false, fpOld, commit, 7, false, true},
|
||||
{"a setting changed at the build it runs", true, fpTest, commit, 7, false, true},
|
||||
{"no approved release carries it", true, fpOld, "", 7, false, true},
|
||||
{"the release merged another commit", true, fpOld, strings.Repeat("2", 40), 7, false, true},
|
||||
// Its fingerprint last moved at another release's revision, which merged nothing of web: that Approve answers nothing.
|
||||
{"a fingerprint another release moved", true, fpOld, commit, 8, false, true},
|
||||
// Review rounds 1 and 2: moved by a recompute after the release (a toolchain, a package, a base) with no merge, the
|
||||
// indexer writes fp-src 0, not known, though Src is still the release's: asked of a person.
|
||||
{"a fingerprint moved by a recompute after the release", true, fpOld, commit, 0, false, true},
|
||||
} {
|
||||
asksKV := &memKV{values: map[string][]byte{}}
|
||||
p := newPasser(kvSet{buckets.Asks: asksKV}, time.Now())
|
||||
p.logf = t.Logf
|
||||
mod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fpTest, Repository: "novox/web", Commit: commit, Src: 7, FpSrc: c.fpSrc, Path: meshcatalogclient.RootPath})
|
||||
putModuleRaw(p.w, trunkBranch, "web", mod)
|
||||
src, _ := mdc.EncodeSource(mdseat.SourceByBranchValue{Group: "feat/web", Heads: map[string]string{"novox/web": commit}})
|
||||
p.w.putEntry(buckets.Source, mdc.SourceKey(trunkBranch), src, 7, time.Now())
|
||||
later, _ := mdc.EncodeSource(mdseat.SourceByBranchValue{Group: "feat/other", Heads: map[string]string{"novox/web": commit, "novox/x": commit}})
|
||||
p.w.putEntry(buckets.Source, mdc.SourceKey(trunkBranch), later, 8, time.Now()) // a later merge leaves revision 7's release
|
||||
if c.mergedAt != "" {
|
||||
merge, err := mdc.EncodeMerge(mdseat.MergesByGroupValue{Group: "feat/web", Ask: "merge.feat_web.0123456789ab", Pulls: []mdseat.MergesByGroupValuePullsItem{
|
||||
{Repository: "novox/web", Number: 3, Head: strings.Repeat("3", 40), State: string(mdc.MergeMerged), Commit: c.mergedAt}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.w.put(buckets.Merges, "feat/web", merge)
|
||||
other, err := mdc.EncodeMerge(mdseat.MergesByGroupValue{Group: "feat/other", Ask: "merge.feat_other.0123456789ab", Pulls: []mdseat.MergesByGroupValuePullsItem{
|
||||
{Repository: "novox/x", Number: 4, Head: strings.Repeat("4", 40), State: string(mdc.MergeMerged), Commit: commit}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.w.put(buckets.Merges, "feat/other", other)
|
||||
}
|
||||
have, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: c.at, Composed: was, Phase: nodeengineclient.PhaseWaitingYes, Health: nodeengineclient.HealthOK,
|
||||
Judgement: assign.JudgedRecreates, Waits: waiting, Detail: "it recreates web.server", BuildAlone: c.alone})
|
||||
p.w.put(buckets.Have, "g14.web", have)
|
||||
m := assign.Module{Name: "web", FP: fpTest, Nodes: []string{"g14"}, Self: func(string) bool { return true },
|
||||
Release: func(fp string) (string, bool) { return p.w.release("web", fp) }}
|
||||
out := assign.Outcome{Wants: map[string]mwc.Want{"g14": {Fp: fpTest, Self: true}}}
|
||||
p.yeses(context.Background(), "web", m, map[string]mwc.Want{"g14": {Fp: fpOld, Self: true}}, &out)
|
||||
w := out.Wants["g14"]
|
||||
if got := w.Yes == waiting && w.Release == "feat/web"; got != c.yes || (!c.yes && (w.Yes != "" || w.Release != "")) {
|
||||
t.Errorf("%s: the want is %+v", c.name, w)
|
||||
}
|
||||
akey, _ := asks.RecordAskKey("g14", "web", waiting)
|
||||
if _, there := asksKV.values[akey]; there != c.asked {
|
||||
t.Errorf("%s: a record ask raised %v, want %v", c.name, there, c.asked)
|
||||
}
|
||||
asksKV := &memKV{values: map[string][]byte{}}
|
||||
p := newPasser(kvSet{buckets.Asks: asksKV}, time.Now())
|
||||
p.logf = t.Logf
|
||||
mod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fpTest, Repository: "novox/web", Commit: commit, Src: 7, FpSrc: 7, Path: meshcatalogclient.RootPath})
|
||||
putModuleRaw(p.w, trunkBranch, "web", mod)
|
||||
have, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fpOld, Composed: was, Phase: nodeengineclient.PhaseWaitingYes, Health: nodeengineclient.HealthOK,
|
||||
Judgement: assign.JudgedRecreates, Waits: waiting, Detail: "it recreates web.server", BuildAlone: true})
|
||||
p.w.put(buckets.Have, "g14.web", have)
|
||||
m := assign.Module{Name: "web", FP: fpTest, Nodes: []string{"g14"}, Self: func(string) bool { return true }}
|
||||
out := assign.Outcome{Wants: map[string]mwc.Want{"g14": {Fp: fpTest, Self: true}}}
|
||||
p.yeses(context.Background(), "web", m, map[string]mwc.Want{"g14": {Fp: fpOld, Self: true}}, &out)
|
||||
if w := out.Wants["g14"]; w.Yes != "" || w.Release != "" {
|
||||
t.Errorf("the want is %+v: no release's Approve is a yes", w)
|
||||
}
|
||||
akey, _ := asks.RecordAskKey("g14", "web", waiting)
|
||||
if _, there := asksKV.values[akey]; !there {
|
||||
t.Error("no record ask was raised for the move of the build alone")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -153,6 +153,8 @@ type pipelineEnds struct {
|
||||
firsts map[string]string
|
||||
// onBranches is told the branches a registration names, for the catalog half of each to be followed (design 47 G §14, §195).
|
||||
onBranches func([]string)
|
||||
// trunk names the first watched branch, the trunk's (design 47 G §507), the branch of a module registered with none.
|
||||
trunk func() string
|
||||
}
|
||||
|
||||
func newPipelineEnds(st endStore, wake func(), logf func(string, ...any)) *pipelineEnds {
|
||||
@@ -225,7 +227,10 @@ func (e *pipelineEnds) branchOf(module, placed string) string {
|
||||
if b := e.firsts[module]; b != "" {
|
||||
return b
|
||||
}
|
||||
return trunkBranch
|
||||
if e.trunk != nil {
|
||||
return e.trunk()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// hearPipeline takes a pipeline-ended. A pipeline of a group writes nothing on its own end, and a run on request delivers
|
||||
|
||||
@@ -166,6 +166,7 @@ func TestAPassWritesNoWantWithoutAPassedPipeline(t *testing.T) {
|
||||
p.w.put(buckets.Presence, "g14", on)
|
||||
if c.follow {
|
||||
p.ends = newPipelineEnds(newMemEnds(), func() {}, t.Logf)
|
||||
p.ends.trunk = p.w.trunkName
|
||||
p.ends.on.Store(true)
|
||||
if c.passed {
|
||||
_ = p.ends.take(pipe("web", trunkBranch, 5, 1, fp, mpr.PipelineEndedOutcomePassed), true)
|
||||
@@ -193,6 +194,7 @@ func TestTheWantsSwitchOnTheFirstRegistration(t *testing.T) {
|
||||
st := newMemEnds()
|
||||
woken := 0
|
||||
e := newPipelineEnds(st, func() { woken++ }, t.Logf)
|
||||
e.trunk = func() string { return trunkBranch }
|
||||
_ = e.take(pipe("web", "main", 5, 1, "fp1", mpr.PipelineEndedOutcomeFailed), false)
|
||||
_ = e.take(pipe("web", "beta", 5, 1, "fp1", mpr.PipelineEndedOutcomePassed), true)
|
||||
if e.following() || !e.mayWant("web", "fp1", []string{"main"}, nil) {
|
||||
@@ -222,6 +224,7 @@ func TestACommitOnBetaMovesOnlyTheNodeThatChoseBeta(t *testing.T) {
|
||||
p := newPasser(kvSet{buckets.Wants: wantsKV, buckets.Asks: &memKV{values: map[string][]byte{}}}, time.Now())
|
||||
p.say = func(oc.Condition) error { return nil }
|
||||
p.ends = newPipelineEnds(newMemEnds(), func() {}, t.Logf)
|
||||
p.ends.trunk = p.w.trunkName
|
||||
p.ends.registered(mpcat.Registration{Module: "web", Branches: []string{"main", "beta"}}, false)
|
||||
digest := "sha256:" + strings.Repeat("c", 64)
|
||||
on, _ := presenceValue(true)
|
||||
|
||||
@@ -136,7 +136,9 @@ func (iv *catalogViews) start(ctx context.Context, w *world, wake func(), settle
|
||||
reader.Findings = func() views.WireWriter { return iv.writer }
|
||||
}
|
||||
iv.h = &halves{w: w, call: iv.call, reader: reader, wake: wake, logf: iv.logf, ctx: ctx, settled: settled}
|
||||
iv.h.follow(trunkBranch)
|
||||
if err := iv.h.follow(); err != nil {
|
||||
iv.logf("the catalog half is not followed, so no module is placed until the assigner starts again: %v", err)
|
||||
}
|
||||
reader.Start(ctx, iv.js, iv.account, iv.status)
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
mwc "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/graph"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/trunk"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
|
||||
mmac "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
@@ -48,9 +47,10 @@ type world struct {
|
||||
// assignments is the seat's state assignments assembled node by node from its keys in either form (mesh-sdk
|
||||
// mmac.Keys; research 072 row 37): the one reader of its keys in the assigner.
|
||||
assignmentKeys *mmac.Keys
|
||||
// sourceGroups is, by revision of the gate's source.main, the release (group) that moved it, from every revision its
|
||||
// history keeps (watched with history): a build's source revision names the release that carries it (ADR 0601 §1).
|
||||
sourceGroups map[uint64]string
|
||||
// first is the first watched branch the catalog half is followed of, the trunk's: the branch of the trunk-only want, read from the
|
||||
// branches the registrations name, before the first registrations from those the gate's `source` answers (design 47 G §507, §532),
|
||||
// and never fixed in code (§7).
|
||||
first string
|
||||
// running is, by `<node>.<module>`, the last `running` the node-engine seat said of it, kept from its stream by the assigner's
|
||||
// durable consumer (design 47 G §199, §309); runningHeard says that consumer is bound.
|
||||
running map[string]nodeengine.Running
|
||||
@@ -77,7 +77,7 @@ type parsed struct {
|
||||
func newWorld() *world {
|
||||
return &world{values: map[string]map[string][]byte{}, manifests: map[string]parsed{}, healthy: map[string][]string{}, changes: map[string]uint64{},
|
||||
meta: map[string]entryMeta{}, assignmentKeys: mmac.NewKeys(),
|
||||
sourceGroups: map[uint64]string{}, running: map[string]nodeengine.Running{},
|
||||
running: map[string]nodeengine.Running{},
|
||||
modules: map[string]map[string]meshcatalog.ModulesByModuleValue{}, graphs: map[string]map[string]graph.Node{}}
|
||||
}
|
||||
|
||||
@@ -94,44 +94,6 @@ func (w *world) putEntry(bucket, key string, value []byte, revision uint64, at t
|
||||
if bucket == plc.AssignmentsBucket {
|
||||
_, _ = w.assignmentKeys.Put(key, value, revision, false) // put took it already; this is its revision
|
||||
}
|
||||
if bucket == buckets.Source && key == mdc.SourceKey(trunkBranch) {
|
||||
if s, err := mdc.DecodeSource(value); err == nil && s.Group != "" {
|
||||
w.sourceGroups[revision] = s.Group
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// release is the release whose Approve carries module's build fp (ADR 0601 §1), and whether one does: the trunk's value of
|
||||
// the module names fp, the source revision at which that fingerprint last changed (FPSrc, never the Src of its last merge,
|
||||
// which a recompute with no merge keeps) names the release that moved it, and the gate's merge of that release, which it
|
||||
// writes on the operator's Approve, holds the module's own trunk merge among its merged pull requests. Any of them
|
||||
// unknown: none, and the move is asked of a person.
|
||||
func (w *world) release(module, fp string) (string, bool) {
|
||||
t, ok := w.trunk()[module]
|
||||
if !ok || fp == "" || t.Fp2 != fp || t.FpSrc == 0 || t.Commit == "" {
|
||||
return "", false
|
||||
}
|
||||
w.mu.Lock()
|
||||
group := w.sourceGroups[uint64(t.FpSrc)]
|
||||
w.mu.Unlock()
|
||||
key, err := mdc.MergesKey(group)
|
||||
if group == "" || err != nil {
|
||||
return "", false
|
||||
}
|
||||
raw := w.value(buckets.Merges, key)
|
||||
if raw == nil {
|
||||
return "", false
|
||||
}
|
||||
merge, err := mdc.DecodeMerge(raw)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
for _, pull := range merge.Pulls {
|
||||
if pull.State == string(mdc.MergeMerged) && pull.Commit == t.Commit && trunk.SameRepository(pull.Repository, t.Repository) {
|
||||
return group, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// metaOf is the revision and time of the value a bucket holds at key, as the watch said them.
|
||||
@@ -267,8 +229,8 @@ func (w *world) branches() []string {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
out := slices.Sorted(maps.Keys(w.modules))
|
||||
if i := slices.Index(out, trunkBranch); i > 0 {
|
||||
out = slices.Insert(slices.Delete(out, i, i+1), 0, trunkBranch)
|
||||
if i := slices.Index(out, w.first); i > 0 {
|
||||
out = slices.Insert(slices.Delete(out, i, i+1), 0, w.first)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -576,8 +538,19 @@ func (w *world) unplacedNodes() []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// trunkBranch is the branch of the trunk-only want, kept until the first registrations (design 47 G §95, §353).
|
||||
const trunkBranch = "main"
|
||||
// trunkName is the first watched branch, the trunk's (design 47 G §507, §353): "" until the first view of the catalog half is adopted.
|
||||
func (w *world) trunkName() string {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
return w.first
|
||||
}
|
||||
|
||||
// setTrunk says which watched branch is the first.
|
||||
func (w *world) setTrunk(branch string) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
w.first = branch
|
||||
}
|
||||
|
||||
// placedBranches is, by module and node, the watched branch each placement names ("" when it names none).
|
||||
func (w *world) placedBranches() map[string]map[string]string {
|
||||
|
||||
@@ -3,6 +3,8 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"mesh-delivery-assigner/internal/buckets"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -81,3 +83,32 @@ func putModuleRaw(w *world, branch, module string, raw []byte) {
|
||||
}
|
||||
w.putModule(branch, module, v, graph.Node{FP: v.Fp2, Repository: v.Repository}, nil)
|
||||
}
|
||||
|
||||
// Design 47 G §507, §532, §7: the first watched branch the catalog half is followed of is the trunk's, whatever its name, and no part of the
|
||||
// assigner's code names a branch: the world's trunk comes first, and no non-test source holds the string "main".
|
||||
func TestTheTrunkIsTheFirstWatchedBranchAndNoCodeNamesOne(t *testing.T) {
|
||||
w := newWorld()
|
||||
w.setTrunk("develop")
|
||||
for _, b := range []string{"beta", "develop", "alpha"} {
|
||||
w.putModule(b, "web", meshcatalog.ModulesByModuleValue{Fp2: "fp"}, graph.Node{FP: "fp"}, nil)
|
||||
}
|
||||
if got := w.branches(); strings.Join(got, ",") != "develop,alpha,beta" {
|
||||
t.Errorf("branches = %v, want the first watched first, then by name", got)
|
||||
}
|
||||
files, err := filepath.Glob("*.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, f := range files {
|
||||
if strings.HasSuffix(f, "_test.go") {
|
||||
continue
|
||||
}
|
||||
src, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(src), `"main"`) {
|
||||
t.Errorf("%s names the branch main in code (design 47 G §507)", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,10 +73,6 @@ type Module struct {
|
||||
// Allowed says whether the operator answered yes to the record ask about the move of this module on node to the composition
|
||||
// with this digest (ADR 0539 §14). Nil: none did.
|
||||
Allowed func(node, digest string) bool
|
||||
// Release names the release whose Approve carries the build fp, as mesh-delivery's gate holds it for the release's heads,
|
||||
// read by the build's source revision (ADR 0601 §1): that Approve is the yes to a move of the build alone. False when no
|
||||
// approved release is known to carry it. Nil: none is read.
|
||||
Release func(fp string) (string, bool)
|
||||
// Released is a hold on FP the operator released (ADR 0539 §19): FP is not held again by a health check that failed.
|
||||
Released bool
|
||||
// Restored says whether the operator answered "restore" to the restore ask of this data module on node: it is then set back
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
watchdogs "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs"
|
||||
catalog "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
|
||||
delivery "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery"
|
||||
wants "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
|
||||
|
||||
@@ -57,9 +56,6 @@ const (
|
||||
Provisions = ProvisionerModule + "_provisions"
|
||||
// Presence holds each part's presence and the node's.
|
||||
Presence = "mesh-delivery-watchman_" + watchdogs.StatePresence
|
||||
// Source and Merges are the gate's.
|
||||
Source = "mesh-delivery-gate_" + delivery.StateSource
|
||||
Merges = "mesh-delivery-gate_" + delivery.StateMerges
|
||||
// TellerModule is the module whose hand the operator's answers pass: the one that holds each ask's answer, and the
|
||||
// name the hand-act log records a warrant under. Held equal to the manifest's reads by its test.
|
||||
TellerModule = "mesh-delivery-teller"
|
||||
|
||||
@@ -8,10 +8,9 @@ func TestBucketsAreTheEarlierNames(t *testing.T) {
|
||||
for name, c := range map[string][2]string{
|
||||
"Have": {Have, "node-engine_have"}, "ComposedRead": {ComposedRead, "mesh-host.composed"},
|
||||
"Modules": {Modules, "mesh-delivery-indexer_modules"}, "Manifests": {Manifests, "mesh-delivery-indexer_manifests"},
|
||||
"Presence": {Presence, "mesh-delivery-watchman_presence"}, "Source": {Source, "mesh-delivery-gate_source"},
|
||||
"Merges": {Merges, "mesh-delivery-gate_merges"}, "Answers": {Answers, "mesh-delivery-teller_answers"},
|
||||
"Presence": {Presence, "mesh-delivery-watchman_presence"}, "Answers": {Answers, "mesh-delivery-teller_answers"},
|
||||
"Grants": {Grants, "mesh-grants_grants"},
|
||||
"Wants": {Wants, "mesh-delivery-assigner_wants"}, "Differences": {Differences, "mesh-delivery-assigner_differences"},
|
||||
"Wants": {Wants, "mesh-delivery-assigner_wants"}, "Differences": {Differences, "mesh-delivery-assigner_differences"},
|
||||
"Asks": {Asks, "mesh-delivery-assigner_asks"}, "Puts": {Puts, "mesh-delivery-assigner_puts"},
|
||||
"Policies": {Policies, "mesh-delivery-assigner_policies"}, "Provisions": {Provisions, "mesh-provisioner_provisions"}, "TellerModule": {TellerModule, "mesh-delivery-teller"},
|
||||
} {
|
||||
|
||||
@@ -43,18 +43,18 @@ func TestManifest(t *testing.T) {
|
||||
reads := slices.Clone(m.Reads)
|
||||
slices.Sort(reads)
|
||||
// ADR 0593 §8: it composes nothing, so it reads no candidate, held or resolved value, compose step or build log; it reads
|
||||
// every node's published compositions to compare them, and the gate's source and merges, for the release that carries a build (ADR 0601 §1).
|
||||
// every node's published compositions to compare them,.
|
||||
// DLV-F2r (ADR 0615 §1, ADR 0631 §1): the indexer's manifests, modules and module graph (whose verb edges a want waits
|
||||
// on, ADR 0614 §2, §3) are read through its first views and views family, none of the indexer's states by reads.
|
||||
// A-181: what was built is heard from the builder's stream of ends, so the builder's builds bucket is no read.
|
||||
want := []string{"mesh-delivery-gate.merges", "mesh-delivery-gate.source",
|
||||
"mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-grants.grants", "mesh-host.composed",
|
||||
// §431, §507: the gate's source is read through its verb `source` and its event `source-moved`, and no `merges` is read.
|
||||
want := []string{"mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-grants.grants", "mesh-host.composed",
|
||||
"mesh-provisioner.provisions", "node-engine.have"}
|
||||
if !reflect.DeepEqual(reads, want) {
|
||||
t.Errorf("reads = %v, want exactly %v", reads, want)
|
||||
}
|
||||
// the buckets the program names are the manifest's reads, `<module>.<state>` as `<module>_<state>`
|
||||
named := []string{buckets.Merges, buckets.Source, buckets.Answers, buckets.Presence, buckets.Grants,
|
||||
named := []string{buckets.Answers, buckets.Presence, buckets.Grants,
|
||||
buckets.ComposedRead, "mesh-provisioner_provisions", buckets.Have}
|
||||
slices.Sort(named)
|
||||
for i, r := range want {
|
||||
@@ -131,12 +131,13 @@ func TestManifest(t *testing.T) {
|
||||
}
|
||||
// ADR 0618 §4: and it hears each node's engine say its reads changed; ADR 0636 §4 (issue 1966): and the seat list's two events;
|
||||
// issue 1966 note 151: and the teller's ask-ended, its subject the one the assigner hears.
|
||||
if !slices.Equal(m.Consumes, []string{"mesh-conditions.findings-asked", "mesh-conditions.condition-raised", "mesh-conditions.condition-changed", "mesh-conditions.condition-cleared", "mesh-catalog.catalog-revision", "mesh-catalog.registration-changed", "mesh-catalog.version-released", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", builds.BuilderModule + "." + builds.BuildsStreamEvents, buckets.TellerModule + "." + tellerevents.AskEnded, "mesh-pipeline-runner.pipeline-ended", "mesh-pipeline-runner.group-ended", "node-engine.manifest-reads-changed", "node-engine.running"}) {
|
||||
if !slices.Equal(m.Consumes, []string{"mesh-conditions.findings-asked", "mesh-conditions.condition-raised", "mesh-conditions.condition-changed", "mesh-conditions.condition-cleared", "mesh-catalog.catalog-revision", "mesh-catalog.registration-changed", "mesh-delivery.source-moved", "mesh-catalog.version-released", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", builds.BuilderModule + "." + builds.BuildsStreamEvents, buckets.TellerModule + "." + tellerevents.AskEnded, "mesh-pipeline-runner.pipeline-ended", "mesh-pipeline-runner.group-ended", "node-engine.manifest-reads-changed", "node-engine.running"}) {
|
||||
t.Errorf("consumes = %v, want the catalog half's revisions (design 47 G §195), the teller's ask-ended, the engines' reads changed and what they run", m.Consumes)
|
||||
}
|
||||
if want := []string{"mesh-delivery-indexer.manifests", "mesh-delivery-indexer.modules",
|
||||
"seat:mesh-catalog.registrations", "seat:mesh-catalog.revision", "seat:mesh-catalog.seat-definitions", // ADR 0636 §4: the seat list
|
||||
"seat:mesh-conditions.conditions", // design 47 G §706: the conditions read at each judging of an outside wait
|
||||
"seat:mesh-delivery.source", // §507: the branches the gate keeps a source revision of, before the first registrations
|
||||
"seat:mesh-hand-acts.hand-act", "seat:mesh-hand-acts.hand-acts", "seat:mesh-hand-acts.warranted", // ADR 0565 §9, §11: the seat alone
|
||||
"seat:node-backup.backed-up", "seat:node-backup.now",
|
||||
"seat:node-engine.manifest-reads", // ADR 0618 §3
|
||||
|
||||
@@ -30,8 +30,6 @@
|
||||
}
|
||||
],
|
||||
"reads": [
|
||||
"mesh-delivery-gate.merges",
|
||||
"mesh-delivery-gate.source",
|
||||
"mesh-delivery-teller.answers",
|
||||
"mesh-delivery-watchman.presence",
|
||||
"mesh-grants.grants",
|
||||
@@ -49,6 +47,7 @@
|
||||
"mesh-conditions.condition-cleared",
|
||||
"mesh-catalog.catalog-revision",
|
||||
"mesh-catalog.registration-changed",
|
||||
"mesh-delivery.source-moved",
|
||||
"mesh-catalog.version-released",
|
||||
"mesh-catalog.seat-definition-changed",
|
||||
"mesh-catalog.seat-definition-removed",
|
||||
@@ -66,6 +65,7 @@
|
||||
"seat:mesh-catalog.revision",
|
||||
"seat:mesh-catalog.seat-definitions",
|
||||
"seat:mesh-conditions.conditions",
|
||||
"seat:mesh-delivery.source",
|
||||
"seat:mesh-hand-acts.hand-act",
|
||||
"seat:mesh-hand-acts.hand-acts",
|
||||
"seat:mesh-hand-acts.warranted",
|
||||
|
||||
@@ -68,6 +68,9 @@ type Branches struct {
|
||||
Entries *Entries
|
||||
// Changed, when not nil, is told of each change to a branch's view (module, its entry, deleted), set before Wire.
|
||||
Changed func(branch, module string, entry json.RawMessage, deleted bool, at time.Time)
|
||||
// Adopted, when not nil, is told on a goroutine of its own of each branch whose view was adopted, with that view (a part that waits for
|
||||
// its first branch's first view, design 47 G §507), set before Wire.
|
||||
Adopted func(branch string, c *Catalog)
|
||||
// Logf, when not nil, says each view adopted and dropped.
|
||||
Logf func(format string, args ...any)
|
||||
|
||||
@@ -250,6 +253,9 @@ func (b *Branches) reconcile() {
|
||||
b.catalogs[e] = c
|
||||
b.order = append(b.order, e)
|
||||
b.logf("meshcatalogclient: the branch %q is followed", want[e])
|
||||
if b.Adopted != nil {
|
||||
go b.Adopted(branch, c)
|
||||
}
|
||||
}
|
||||
for _, e := range slices.Clone(b.order) {
|
||||
if _, ok := want[e]; ok {
|
||||
|
||||
@@ -126,6 +126,12 @@ func TestBranchesFollowSourceThenRegistrations(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b.Logf = func(format string, args ...any) { said <- fmt.Sprintf(format, args...) }
|
||||
adopted := make(chan string, 8)
|
||||
b.Adopted = func(branch string, c *Catalog) {
|
||||
if c.Branch == branch {
|
||||
adopted <- branch
|
||||
}
|
||||
}
|
||||
want := func(names ...string) {
|
||||
t.Helper()
|
||||
if got := w.names(); !slices.Equal(got, names) {
|
||||
@@ -148,6 +154,18 @@ func TestBranchesFollowSourceThenRegistrations(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
waitFor(`"feat/x" is followed`)
|
||||
told := []string{} // each adopted branch is told, with its view (a part waits for its first one's first view), in no set order
|
||||
for range 2 {
|
||||
select {
|
||||
case got := <-adopted:
|
||||
told = append(told, got)
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("told of %v, want main and feat/x", told)
|
||||
}
|
||||
}
|
||||
if slices.Sort(told); !slices.Equal(told, []string{"feat/x", "main"}) {
|
||||
t.Fatalf("told of %v", told)
|
||||
}
|
||||
if got := b.Followed(); !slices.Equal(got, []string{"main", "feat/x"}) {
|
||||
t.Fatalf("followed = %v", got)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user