Take done keys in from a durable pull consumer, a batch at a time (issue 1945, ADR 0544)
The take-in watched the builder's and the indexer's buckets, started a follower per module that retried on a 30 s timer, and at start took every catalogued module in alphabetically: on 2026-10-25 a restart took in about 2 modules a minute, about 190 followers queued behind 16 slots, and every take-in composed two or three times even with nothing new, so a new build waited 30 minutes and more for its credential. Now two durable consumers named mesh-controller-take-in read the indexer's modules bucket (every module's last trunk value when the consumer is made, then each write) and the builder's builds bucket (each write), at most 16 keys unacknowledged each. The bus keeps what is not taken in: the start's look is the consumers' own catch-up, and a restart costs only what was left unacknowledged. A batch reads the catalogue once, takes each module in once, and asks the composer twice: once for its modules' nodes, once for the bus's node with the grants. A module registered at its trunk whose compositions and grant exist, or that this process took in at the same trunk, composes nothing; a key delivered again is taken in whole. A wait for the builder's or indexer's next write is acknowledged, a wait on a read or a failure refused with a 30 s delay. The backlog is in status, and a backlog standing still for 5 minutes raises take-in-stalled. The controller's account is granted the two consumers' acks. The follower tests go with the followers: twelve tests become three. Research 072: the take-ins on a bounded pull consumer (milestone 4, done keys alone) and the start's look composing only what changed (milestone 2).
This commit is contained in:
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -12,210 +14,246 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
"github.com/novox/mesh-controller/internal/builderkeys"
|
||||
"github.com/novox/mesh-controller/internal/conditions"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
)
|
||||
|
||||
// A new fingerprint of a registered module, from its done key to its compositions (novox/hq ADR 0539 clauses 5 and 7,
|
||||
// ADR 0501 §2). With no delivery plan left to take a module's done key in, a write of the builder's or the indexer's
|
||||
// keys does it: the controller takes in the done key of the fingerprint the indexer's trunk value names (registers the
|
||||
// module at it, as a plan's take-in did), composes every node the module is assigned to (which makes what a module's
|
||||
// new manifest needs, a credential among it, as a send made it), composes the node holding the bus (its user list now
|
||||
// carries the module's permissions at the new fingerprint: a new revision of the bus module's composition alone,
|
||||
// written into its want at once by the assigner), and writes `grants.<module>.<fp>`. The assigner's sixth and seventh
|
||||
// rules then let a want name the fingerprint. **No timer**: woken by the keys, and a starting controller looks at every
|
||||
// module once (doneKeysAtStart).
|
||||
// ADR 0501 §2, ADR 0544). With no delivery plan left to take a module's done key in, a write of the builder's or the
|
||||
// indexer's keys does it: the controller takes in the done key of the fingerprint the indexer's trunk value names
|
||||
// (registers the module at it, as a plan's take-in did), composes every node the module is assigned to (which makes what
|
||||
// a module's new manifest needs, a credential among it), composes the node holding the bus (its user list now carries
|
||||
// the module's permissions at the new fingerprint), and writes `grants.<module>.<fp>`. The assigner's sixth and seventh
|
||||
// rules then let a want name the fingerprint.
|
||||
//
|
||||
// **A durable pull consumer, not a watch** (ADR 0544; research 072, milestones 2 and 4). The writes are read from two
|
||||
// durable consumers (take_in_consumer.go): the bus keeps what is not taken in yet, so a restart takes in only what the
|
||||
// controller before it left unacknowledged, and the start's look is the consumers' own catch-up. A batch of keys asks the
|
||||
// composer twice whatever its size, and a key with nothing new composes nothing.
|
||||
|
||||
// doneKeySteps are what taking a module's done key in does, each a step a test may stand in for.
|
||||
type doneKeySteps struct {
|
||||
// held says the catalogue holds the module (a new one is the new-module take-in's, new_modules.go).
|
||||
held func(ctx context.Context, module string) (bool, error)
|
||||
// takeInSteps are what taking a batch of done keys in does, each a step a test may stand in for.
|
||||
type takeInSteps struct {
|
||||
// catalogue is every module the catalogue holds, with the nodes it is assigned to: read once a batch. A module it
|
||||
// does not hold is the new-module take-in's (new_modules.go).
|
||||
catalogue func(ctx context.Context) (map[string][]string, error)
|
||||
// trunk is the indexer's trunk value of the module.
|
||||
trunk func(ctx context.Context, module string) (sdkdelivery.Module, error)
|
||||
// registeredAt says the module is registered at the commit.
|
||||
registeredAt func(ctx context.Context, module, commit string) (bool, error)
|
||||
// takeIn registers the module from the builder's done key of the trunk's fingerprint (takeInKey); errKeyWaits when
|
||||
// the key is not done yet.
|
||||
// takeIn registers the module from the builder's done key of the trunk's fingerprint (takeInKey).
|
||||
takeIn func(ctx context.Context, module string, trunk sdkdelivery.Module) error
|
||||
// nodesOf are the nodes the module is assigned to; busHolder the node holding the bus, "" for none.
|
||||
nodesOf func(ctx context.Context, module string) ([]string, error)
|
||||
// taken says the module's compositions at fp exist for every node and its grant is written: nothing to compose.
|
||||
taken func(ctx context.Context, module, fp string, nodes []string) (bool, error)
|
||||
// busHolder is the node holding the bus, "" for none.
|
||||
busHolder func(ctx context.Context) (string, error)
|
||||
// compose writes the nodes' compositions; grants writes the grants issued.
|
||||
compose func(ctx context.Context, nodes []string) error
|
||||
grants func(ctx context.Context) error
|
||||
// compose composes the nodes, and writes the grants after when grants: each node's error, and the grants'.
|
||||
compose func(ctx context.Context, nodes []string, grants bool) (map[string]error, error, error)
|
||||
}
|
||||
|
||||
// take takes module's done key in and composes what it changed. It answers what it did in a line, "" when there was
|
||||
// nothing to do.
|
||||
func (s doneKeySteps) take(ctx context.Context, module string) (string, error) {
|
||||
held, err := s.held(ctx, module)
|
||||
if err != nil || !held {
|
||||
return "", err
|
||||
}
|
||||
trunk, err := s.trunk(ctx, module)
|
||||
if errors.Is(err, builderkeys.ErrNoValue) {
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !strings.HasPrefix(trunk.FP2, sdkdelivery.SourcePrefix2) {
|
||||
return "", nil
|
||||
}
|
||||
at, err := s.registeredAt(ctx, module, trunk.Commit)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
said := ""
|
||||
if !at {
|
||||
if err := s.takeIn(ctx, module, trunk); err != nil {
|
||||
return "", err
|
||||
}
|
||||
said = fmt.Sprintf("%s registered from the builder's done key %s (%s)", module, short(trunk.FP2), short(trunk.Commit))
|
||||
}
|
||||
nodes, err := s.nodesOf(ctx, module)
|
||||
if err != nil {
|
||||
return said, err
|
||||
}
|
||||
if len(nodes) == 0 {
|
||||
return said, nil
|
||||
}
|
||||
// The module's nodes first: composing them makes what the new manifest needs, its credentials among it.
|
||||
if err := s.compose(ctx, nodes); err != nil {
|
||||
return said, err
|
||||
}
|
||||
// Then the bus's user list, which carries the module's permissions at its new fingerprint.
|
||||
holder, err := s.busHolder(ctx)
|
||||
if err != nil {
|
||||
return said, err
|
||||
}
|
||||
if holder != "" && !inList(nodes, holder) {
|
||||
if err := s.compose(ctx, []string{holder}); err != nil {
|
||||
return said, err
|
||||
}
|
||||
}
|
||||
// Then the grant, which with the composition lets a want name the fingerprint (rules 6 and 7).
|
||||
if err := s.grants(ctx); err != nil {
|
||||
return said, err
|
||||
}
|
||||
if said == "" {
|
||||
said = fmt.Sprintf("%s at %s", module, short(trunk.FP2))
|
||||
}
|
||||
return said + "; composed for " + strings.Join(nodes, ", ") + ", its grant written", nil
|
||||
// delivered is one key a consumer delivered: its module, and whether it was delivered before (a take-in that may have
|
||||
// stopped half-way, so it is taken in whole).
|
||||
type delivered struct {
|
||||
module string
|
||||
again bool
|
||||
}
|
||||
|
||||
func inList(list []string, s string) bool {
|
||||
for _, v := range list {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
// verdict is what becomes of a module's delivered keys.
|
||||
type verdict int
|
||||
|
||||
// doneKeyTakeOnce is one take-in; a variable so a test may stand in for it. doneKeysSay says a line of the take-ins.
|
||||
var (
|
||||
doneKeyTakeOnce = takeDoneKeyOnce
|
||||
doneKeysSay = func(s string) { fmt.Println(s) }
|
||||
const (
|
||||
// acknowledged: taken in, nothing new, a wait a later write ends, or a build key that failed.
|
||||
acknowledged verdict = iota
|
||||
// refused: delivered again by the bus after doneKeyRetry (a wait on a read, or a failure).
|
||||
refused
|
||||
)
|
||||
|
||||
// doneKeysBeforeLetGo is called just before a follower lets go of its module; a variable so a test can wake the module
|
||||
// at that moment. Nil does nothing.
|
||||
var doneKeysBeforeLetGo func(module string)
|
||||
|
||||
// doneKeysLookAgain marks a module woken while a take-in of it ran: that take-in goes round once more.
|
||||
var doneKeysLookAgain sync.Map
|
||||
|
||||
// doneKeysMu keeps one take-in of a module's done key at a time in this process, per module.
|
||||
var doneKeysMu sync.Map
|
||||
|
||||
// takeDoneKey is a woken module's take-in on the live stores and buckets, said. A take-in that fails (its compositions
|
||||
// or its grant not written) is tried again every doneKeyRetry while it fails, loudly: printed each time, and an urgent
|
||||
// condition raised by the first until it has worked, as the chain's own moves are (layBuildsOutSoon). A key not done
|
||||
// yet is no failure: it is said once for each reason it waits for and looked at again every doneKeyRetry.
|
||||
func takeDoneKey(ctx context.Context, open *stores, module string) {
|
||||
mu, _ := doneKeysMu.LoadOrStore(module, &sync.Mutex{})
|
||||
for {
|
||||
if !mu.(*sync.Mutex).TryLock() {
|
||||
// A take-in of the module already follows it: it is told to look again once it is done, so a newer key
|
||||
// written meanwhile is never lost (the review of issue 1945).
|
||||
doneKeysLookAgain.Store(module, true)
|
||||
return
|
||||
}
|
||||
followDoneKey(ctx, open, module)
|
||||
if doneKeysBeforeLetGo != nil {
|
||||
doneKeysBeforeLetGo(module)
|
||||
}
|
||||
mu.(*sync.Mutex).Unlock()
|
||||
// A wake between this follower's last look and its letting go failed its TryLock and only marked the module:
|
||||
// looked at again now that it is let go, and taken once more (the review of issue 1945).
|
||||
if _, again := doneKeysLookAgain.LoadAndDelete(module); !again || ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
// settledKey is what a batch did with one module.
|
||||
type settledKey struct {
|
||||
module string
|
||||
verdict verdict
|
||||
// taken: registered, composed and granted now (not nothing new); its condition is cleared.
|
||||
taken bool
|
||||
// nothingNew: registered and composed before, so nothing was done.
|
||||
nothingNew bool
|
||||
// waits is why it waits, "" when it does not.
|
||||
waits string
|
||||
// err is a failure: raised as the module's condition.
|
||||
err error
|
||||
// said is the line said of it, "" for none.
|
||||
said string
|
||||
}
|
||||
|
||||
// followDoneKey takes a module's done key in, under the module's lock, until it is taken in or ctx ends: a failure or a
|
||||
// wait is tried again every doneKeyRetry.
|
||||
func followDoneKey(ctx context.Context, open *stores, module string) {
|
||||
failed := false
|
||||
saidWait := ""
|
||||
for {
|
||||
doneKeysLookAgain.Delete(module) // what this look reads covers every wake before it
|
||||
err := takeOnceInSlot(ctx, open, module)
|
||||
o := doneKeyObservation(module)
|
||||
if err == nil {
|
||||
// Cleared on every success, whoever raised it: a controller before a restart or another follower (issue
|
||||
// 1945). A condition that is not open costs one read.
|
||||
if keeper := conditionsFrom; keeper != nil {
|
||||
_, _ = keeper.Clear(ctx, o.Key(), "composed and granted")
|
||||
}
|
||||
failed = false
|
||||
if _, again := doneKeysLookAgain.LoadAndDelete(module); again {
|
||||
continue // a wake came while this one ran: once more, for the key written meanwhile
|
||||
}
|
||||
return
|
||||
// batch takes the delivered keys in, each module once: it answers one settledKey per module in the order first
|
||||
// delivered, and the number of composer passes it asked. memo is, per module, the trunk (fingerprint and commit) this
|
||||
// process took in last; a module at the same trunk delivered for the first time has nothing new.
|
||||
func (s takeInSteps) batch(ctx context.Context, keys []delivered, memo map[string]string) ([]settledKey, int) {
|
||||
var order []string
|
||||
again := map[string]bool{}
|
||||
for _, k := range keys {
|
||||
if _, seen := again[k.module]; !seen {
|
||||
order = append(order, k.module)
|
||||
}
|
||||
if errors.Is(err, errKeyWaits) {
|
||||
// A take-in that waits is said once for each reason, and tried again every doneKeyRetry: the key write that
|
||||
// would wake it may never come (issue 1945, live on 2026-10-25: mesh-host's take-in waited unsaid).
|
||||
if why := err.Error(); why != saidWait {
|
||||
doneKeysSay(fmt.Sprintf("done keys: %s waits: %s; looked at again every %s", module,
|
||||
strings.TrimPrefix(why, errKeyWaits.Error()+": "), doneKeyRetry))
|
||||
saidWait = why
|
||||
}
|
||||
} else {
|
||||
doneKeysSay(fmt.Sprintf("done keys: %s: %v; tried again in %s", module, err, doneKeyRetry))
|
||||
again[k.module] = again[k.module] || k.again
|
||||
}
|
||||
out := make([]settledKey, 0, len(order))
|
||||
all := func(v verdict, err error) ([]settledKey, int) {
|
||||
for _, m := range order {
|
||||
out = append(out, settledKey{module: m, verdict: v, err: err})
|
||||
}
|
||||
if errors.Is(err, errKeyWaits) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(doneKeyRetry):
|
||||
}
|
||||
return out, 0
|
||||
}
|
||||
held, err := s.catalogue(ctx)
|
||||
if err != nil {
|
||||
return all(refused, fmt.Errorf("the catalogue cannot be read: %w", err))
|
||||
}
|
||||
type toCompose struct {
|
||||
at int
|
||||
nodes []string
|
||||
mark string
|
||||
said string
|
||||
}
|
||||
var need []toCompose
|
||||
for _, m := range order {
|
||||
r := settledKey{module: m}
|
||||
nodes, isHeld := held[m]
|
||||
if !isHeld {
|
||||
out = append(out, r)
|
||||
continue
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return // stopped: a take-in cut by the stop is no fault of the build, and the next controller takes it in
|
||||
trunk, err := s.trunk(ctx, m)
|
||||
if errors.Is(err, builderkeys.ErrNoValue) || (err == nil && !strings.HasPrefix(trunk.FP2, sdkdelivery.SourcePrefix2)) {
|
||||
out = append(out, r)
|
||||
continue
|
||||
}
|
||||
if keeper := conditionsFrom; keeper != nil && !failed && !errors.Is(err, context.Canceled) {
|
||||
// A take-in whose own step was cancelled is tried again, and raises nothing: a cancellation is no fault
|
||||
// of the build (issue 1945: 30 urgent conditions live, each a take-in cut by a restart).
|
||||
o.Summary += err.Error()
|
||||
if _, oerr := keeper.Observe(ctx, o); oerr == nil {
|
||||
failed = true
|
||||
if err != nil {
|
||||
r.verdict, r.err = refused, err
|
||||
out = append(out, r)
|
||||
continue
|
||||
}
|
||||
mark := trunk.FP2 + "@" + trunk.Commit
|
||||
if !again[m] && memo[m] == mark {
|
||||
r.nothingNew = true
|
||||
out = append(out, r)
|
||||
continue
|
||||
}
|
||||
at, err := s.registeredAt(ctx, m, trunk.Commit)
|
||||
if err != nil {
|
||||
r.verdict, r.err = refused, err
|
||||
out = append(out, r)
|
||||
continue
|
||||
}
|
||||
said := ""
|
||||
if !at {
|
||||
if err := s.takeIn(ctx, m, trunk); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, errWaitsForAWrite):
|
||||
r.waits = err.Error()
|
||||
case errors.Is(err, errKeyWaits):
|
||||
r.verdict, r.waits = refused, err.Error()
|
||||
case errors.Is(err, errKeyFailed):
|
||||
r.err = err // acknowledged: only a new build ends it, and its key is delivered then
|
||||
default:
|
||||
r.verdict, r.err = refused, err
|
||||
}
|
||||
out = append(out, r)
|
||||
continue
|
||||
}
|
||||
said = fmt.Sprintf("%s registered from the builder's done key %s (%s)", m, short(trunk.FP2), short(trunk.Commit))
|
||||
} else if !again[m] {
|
||||
// Registered at its trunk already: composed and granted by a take-in before this one, or at this
|
||||
// controller's start, unless that take-in stopped half-way, which leaves a composition or the grant missing.
|
||||
if done, err := s.taken(ctx, m, trunk.FP2, nodes); err == nil && done {
|
||||
memo[m] = mark
|
||||
r.nothingNew = true
|
||||
out = append(out, r)
|
||||
continue
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(doneKeyRetry):
|
||||
if len(nodes) == 0 {
|
||||
memo[m] = mark
|
||||
r.taken, r.said = true, said
|
||||
out = append(out, r)
|
||||
continue
|
||||
}
|
||||
if said == "" {
|
||||
said = fmt.Sprintf("%s at %s", m, short(trunk.FP2))
|
||||
}
|
||||
need = append(need, toCompose{at: len(out), nodes: nodes, mark: mark, said: said})
|
||||
out = append(out, r)
|
||||
}
|
||||
if len(need) == 0 {
|
||||
return out, 0
|
||||
}
|
||||
refuseNeed := func(err error) ([]settledKey, int) {
|
||||
for _, n := range need {
|
||||
out[n.at].verdict, out[n.at].err = refused, err
|
||||
}
|
||||
return out, 0
|
||||
}
|
||||
holder, err := s.busHolder(ctx)
|
||||
if err != nil {
|
||||
return refuseNeed(fmt.Errorf("the node holding the bus cannot be read: %w", err))
|
||||
}
|
||||
// The modules' nodes first, in one pass: composing them makes what their new manifests need, credentials among it.
|
||||
var union []string
|
||||
for _, n := range need {
|
||||
for _, node := range n.nodes {
|
||||
if node != holder && !slices.Contains(union, node) {
|
||||
union = append(union, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(union)
|
||||
passes := 0
|
||||
failed := map[string]error{}
|
||||
if len(union) > 0 {
|
||||
f, _, err := s.compose(ctx, union, false)
|
||||
passes++
|
||||
if err != nil {
|
||||
return refuseNeed(err)
|
||||
}
|
||||
maps.Copy(failed, f)
|
||||
}
|
||||
// Then the bus's user list, which carries the modules' permissions at their new fingerprints, and the grants, which
|
||||
// with the compositions let a want name each fingerprint (rules 6 and 7).
|
||||
var bus []string
|
||||
if holder != "" {
|
||||
bus = []string{holder}
|
||||
}
|
||||
f, grantsErr, err := s.compose(ctx, bus, true)
|
||||
passes++
|
||||
if err != nil {
|
||||
return refuseNeed(err)
|
||||
}
|
||||
maps.Copy(failed, f)
|
||||
for _, n := range need {
|
||||
var errs []error
|
||||
for _, node := range append(slices.Clone(n.nodes), bus...) {
|
||||
if e := failed[node]; e != nil && !slices.ContainsFunc(errs, func(x error) bool { return x == e }) {
|
||||
errs = append(errs, e)
|
||||
}
|
||||
}
|
||||
if grantsErr != nil {
|
||||
errs = append(errs, grantsErr)
|
||||
}
|
||||
r := &out[n.at]
|
||||
if len(errs) > 0 {
|
||||
r.verdict, r.err = refused, errors.Join(errs...)
|
||||
continue
|
||||
}
|
||||
memo[r.module] = n.mark
|
||||
r.taken = true
|
||||
r.said = n.said + "; composed for " + strings.Join(n.nodes, ", ") + ", its grant written"
|
||||
}
|
||||
return out, passes
|
||||
}
|
||||
|
||||
// doneKeyRetry is how long a failed take-in of a done key waits before it tries again.
|
||||
// doneKeysSay says a line of the take-ins; a variable so a test may hear it.
|
||||
var doneKeysSay = func(s string) { fmt.Println(s) }
|
||||
|
||||
// doneKeyRetry is how long the bus waits before it delivers again a key whose take-in was refused.
|
||||
var doneKeyRetry = 30 * time.Second
|
||||
|
||||
// kindNotComposed is the condition of a module's new fingerprint whose compositions or grant are not written: until
|
||||
@@ -228,30 +266,20 @@ func doneKeyObservation(module string) conditions.Observation {
|
||||
Summary: module + "'s new fingerprint is not composed or granted, so no want can name it: "}
|
||||
}
|
||||
|
||||
// takeDoneKeyOnce is one take-in of a module's done key.
|
||||
func takeDoneKeyOnce(ctx context.Context, open *stores, module string) error {
|
||||
keys, closeKeys, err := openBuilderKeys()
|
||||
if err != nil {
|
||||
return fmt.Errorf("the builder's keys cannot be read: %w", err)
|
||||
}
|
||||
defer closeKeys()
|
||||
said, err := liveDoneKeySteps(open, keys).take(ctx, module)
|
||||
if err == nil && said != "" {
|
||||
fmt.Printf("done keys: %s\n", said)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// liveDoneKeySteps are the steps on the controller's own stores and the bus.
|
||||
func liveDoneKeySteps(open *stores, keys registerKeys) doneKeySteps {
|
||||
// liveTakeInSteps are the steps on the controller's own stores and the bus.
|
||||
func liveTakeInSteps(open *stores, keys registerKeys, js jetstream.JetStream) takeInSteps {
|
||||
inv := open.inventory
|
||||
return doneKeySteps{
|
||||
held: func(ctx context.Context, module string) (bool, error) {
|
||||
_, err := inv.SourceOf(ctx, module)
|
||||
if errors.Is(err, inventory.ErrNoSuchModule) {
|
||||
return false, nil
|
||||
return takeInSteps{
|
||||
catalogue: func(ctx context.Context) (map[string][]string, error) {
|
||||
entries, err := inv.Catalogued(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return err == nil, err
|
||||
out := make(map[string][]string, len(entries))
|
||||
for _, e := range entries {
|
||||
out[e.Manifest.Module] = e.On
|
||||
}
|
||||
return out, nil
|
||||
},
|
||||
trunk: func(ctx context.Context, module string) (sdkdelivery.Module, error) {
|
||||
return keys.ModuleAt(ctx, module, "")
|
||||
@@ -270,17 +298,8 @@ func liveDoneKeySteps(open *stores, keys registerKeys) doneKeySteps {
|
||||
}
|
||||
return err
|
||||
},
|
||||
nodesOf: func(ctx context.Context, module string) ([]string, error) {
|
||||
entries, err := inv.Catalogued(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.Manifest.Module == module {
|
||||
return e.On, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
taken: func(ctx context.Context, module, fp string, nodes []string) (bool, error) {
|
||||
return composedAndGranted(ctx, js, module, fp, nodes)
|
||||
},
|
||||
busHolder: func(ctx context.Context) (string, error) {
|
||||
holders, err := seatHolders(ctx, inv)
|
||||
@@ -291,123 +310,46 @@ func liveDoneKeySteps(open *stores, keys registerKeys) doneKeySteps {
|
||||
},
|
||||
// The composer's, never the take-in's own: one pass at a time composes each node asked for once, and writes the
|
||||
// grants once (issue 1945, composer.go).
|
||||
compose: func(ctx context.Context, nodes []string) error {
|
||||
return composerFor(open).compose(ctx, nodes, false)
|
||||
},
|
||||
grants: func(ctx context.Context) error {
|
||||
return composerFor(open).compose(ctx, nil, true)
|
||||
compose: func(ctx context.Context, nodes []string, grants bool) (map[string]error, error, error) {
|
||||
return composerFor(open).composeEach(ctx, nodes, grants)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// watchDoneKeys wakes the take-in of a module's done key for each module the builder or the indexer writes, until ctx
|
||||
// ends: never on the watch's own goroutine.
|
||||
func watchDoneKeys(ctx context.Context, open *stores) {
|
||||
keys, closeKeys, err := openBuilderKeys()
|
||||
// composedAndGranted says the bus holds module's grant at fp and its composition at fp for every node: a few reads of
|
||||
// keys, never a composition.
|
||||
func composedAndGranted(ctx context.Context, js jetstream.JetStream, module, fp string, nodes []string) (bool, error) {
|
||||
grants, err := js.KeyValue(ctx, broker.GrantsBucket)
|
||||
if err != nil {
|
||||
fmt.Printf("done keys: the builder's keys are not watched: %v\n", err)
|
||||
return
|
||||
return false, err
|
||||
}
|
||||
if err := builderkeys.Watch(ctx, keys.JS, func(module string) {
|
||||
inBackground(ctx, func() { takeDoneKey(ctx, open, module) })
|
||||
}); err != nil {
|
||||
closeKeys()
|
||||
fmt.Printf("done keys: the builder's keys are not watched: %v\n", err)
|
||||
return
|
||||
}
|
||||
goServing(ctx, func() {
|
||||
<-ctx.Done()
|
||||
closeKeys()
|
||||
})
|
||||
// And every module once now, after the watch is open so no write falls between the two.
|
||||
inBackground(ctx, func() {
|
||||
doneKeysAtStart(ctx, catalogedModules(open), func(module string) { takeDoneKeyAtStart(ctx, open, module) })
|
||||
})
|
||||
}
|
||||
|
||||
// doneKeysAtStart takes in the done key of every catalogued module once, one after another: the start's look the watch of
|
||||
// new writes cannot give (it hears updates only). A done key written while no controller served, or whose take-in the
|
||||
// controller this one replaced was stopped in, is taken here; without it the module stays in the node's own composition
|
||||
// with no credential until its keys are written again (issue 1945, live on 2026-10-25 at 02:34). A take-in with nothing
|
||||
// to do does nothing; a catalogue that cannot be read is said.
|
||||
func doneKeysAtStart(ctx context.Context, modules func(context.Context) ([]string, error), take func(module string)) {
|
||||
names, err := modules(ctx)
|
||||
key, err := sdkdelivery.ControllerGrantKey(module, fp)
|
||||
if err != nil {
|
||||
fmt.Printf("done keys: the modules are not looked at once at the start: %v\n", err)
|
||||
return
|
||||
return false, err
|
||||
}
|
||||
sort.Strings(names)
|
||||
// Side by side, a few at a time: their compositions meet in the composer's passes, each node composed once per pass
|
||||
// (issue 1945), so the start's look is not one module's composition after another.
|
||||
sem := make(chan struct{}, startTakeIns)
|
||||
var wg sync.WaitGroup
|
||||
for _, m := range names {
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
if _, err := grants.Get(ctx, key); err != nil {
|
||||
if errors.Is(err, jetstream.ErrKeyNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
sem <- struct{}{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer func() { <-sem; wg.Done() }()
|
||||
take(m)
|
||||
}()
|
||||
return false, err
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// startTakeIns is how many take-ins the start's look runs at once, and how many run at once in all.
|
||||
const startTakeIns = 16
|
||||
|
||||
// takeInSlots are the take-ins that may run at once in this process, the watch's and the start's look's together: a
|
||||
// burst of done keys (about 120 at 16:07:36 on 2026-10-25) once started a take-in each, all at once, and their reads
|
||||
// filled the store until it refused every connection (issues 1945 and 1277).
|
||||
var takeInSlots = make(chan struct{}, startTakeIns)
|
||||
|
||||
// takeOnceInSlot is one take-in once a slot is free; a take-in that waits for its next try holds none.
|
||||
func takeOnceInSlot(ctx context.Context, open *stores, module string) error {
|
||||
select {
|
||||
case takeInSlots <- struct{}{}:
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
digests, err := js.KeyValue(ctx, sdkdelivery.ControllerComposedBucket)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer func() { <-takeInSlots }()
|
||||
return doneKeyTakeOnce(ctx, open, module)
|
||||
}
|
||||
|
||||
// takeDoneKeyAtStart is one take-in of the start's look, under the module's lock; one that fails is handed to takeDoneKey,
|
||||
// which tries again every doneKeyRetry, loudly, so a failing module never holds up the look at the others.
|
||||
func takeDoneKeyAtStart(ctx context.Context, open *stores, module string) {
|
||||
mu, _ := doneKeysMu.LoadOrStore(module, &sync.Mutex{})
|
||||
if !mu.(*sync.Mutex).TryLock() {
|
||||
// A take-in already follows the module: told to look again, it takes what the start would have (issue 1945).
|
||||
doneKeysLookAgain.Store(module, true)
|
||||
return
|
||||
}
|
||||
doneKeysLookAgain.Delete(module) // what this look reads covers every wake before it
|
||||
err := takeOnceInSlot(ctx, open, module)
|
||||
mu.(*sync.Mutex).Unlock()
|
||||
// Read after letting go, as every holder of a module's lock does: a wake during this look only marked the module
|
||||
// (the review of issue 1945).
|
||||
_, again := doneKeysLookAgain.LoadAndDelete(module)
|
||||
if err != nil || again {
|
||||
// A failure, a wait or a wake meanwhile is followed by takeDoneKey, which says it and looks again.
|
||||
inBackground(ctx, func() { takeDoneKey(ctx, open, module) })
|
||||
}
|
||||
}
|
||||
|
||||
// catalogedModules are the names of every module the catalogue holds.
|
||||
func catalogedModules(open *stores) func(context.Context) ([]string, error) {
|
||||
return func(ctx context.Context) ([]string, error) {
|
||||
entries, err := open.inventory.Catalogued(ctx)
|
||||
for _, node := range nodes {
|
||||
key, err := sdkdelivery.ComposedKey(node, module, fp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return false, err
|
||||
}
|
||||
out := make([]string, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
out = append(out, e.Manifest.Module)
|
||||
if _, err := digests.Get(ctx, key); err != nil {
|
||||
if errors.Is(err, jetstream.ErrKeyNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// doneKeysComposers are the composers of the done keys' take-ins in this process, one for each set of stores (a test
|
||||
|
||||
Reference in New Issue
Block a user