mesh/delivery held for a person: merged without a passing check: only a person decides what follows
mesh/check checking 666d1b682e7f on 045f25bd1353
registeredAt compared commits, so a catalogue merge that moved only the commit (2df05030 at 18:02, a85d9037 before it) took every catalogue module in again and composed it: about 200 take-ins, some 25 minutes, on every mesh-catalog merge, which held nextcloud for an hour. A module whose trunk fingerprint is the one it is registered at now has the new commit recorded (built_from and source_head, as a registration moves them) and composes nothing unless its composition or grant is missing. Readers of the registered commit: the fingerprints read the commit's build or, with none, the indexer's value at it, which names the same fingerprint; status, modules, the board and the facts read it as the source's commit, which it is; the upgrade path counts it as built from that commit, which it is. The stall watch counted only settles the bus accepted, so each refused acknowledgement at a first start left work held for good: it now counts the loop's every attempt. The acknowledgement and refusals at the stop are sent past the stop, and the connection closes only after the readers and the loop have ended. Research 072: the take-ins on a bounded pull consumer.
453 lines
17 KiB
Go
453 lines
17 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
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/conditions"
|
|
)
|
|
|
|
// The take-in's two durable pull consumers (novox/hq ADR 0544; research 072, milestones 2 and 4). On the indexer's
|
|
// modules bucket the consumer delivers each module's last trunk value when it is made, then each write: the start's look
|
|
// of a controller whose consumer is new, and nothing at a restart but what the controller before it left
|
|
// unacknowledged. On the builder's builds bucket it delivers each write from when it is made. A key is acknowledged once
|
|
// its module is taken in, has nothing new, or waits for a write that will be delivered; any other wait or failure is
|
|
// refused with a delay that grows with each refusal, and the bus delivers it again.
|
|
//
|
|
// **The bound on the store is the one take-in loop**, a batch of at most startTakeIns keys at a time (issues 1945 and
|
|
// 1277: a burst of take-ins once filled the store until it refused every connection). The consumers' own limit on
|
|
// unacknowledged keys is far above it, because a refused key stays unacknowledged until its delay ends: with a limit of
|
|
// a batch, a handful of keys refused for long (a manifest word this controller does not read yet) would stop a consumer
|
|
// for good, the build that reads the word among what it holds back (the review of ADR 0544).
|
|
|
|
// startTakeIns is the most a batch takes: the take-ins that may wait on the store at once.
|
|
const startTakeIns = 16
|
|
|
|
// takeInPending is how many keys each consumer may leave unacknowledged: delivered and being taken in, or refused and
|
|
// waiting for their delay. Far above a batch, so refused keys never stop the others.
|
|
const takeInPending = 1000
|
|
|
|
// takeInBuffered is how many delivered keys each consumer's reader holds ahead of the loop.
|
|
const takeInBuffered = startTakeIns / 2
|
|
|
|
// takeInAckWait is how long a delivered key may stay unacknowledged before the bus delivers it again: longer than a batch
|
|
// takes, its two composer passes among it.
|
|
const takeInAckWait = 10 * time.Minute
|
|
|
|
// takeInGather is how long a batch waits for more keys after its first, so a burst is one batch.
|
|
var takeInGather = 200 * time.Millisecond
|
|
|
|
// takeInStall is how long a backlog may stand without a key delivered before it is a condition (ADR 0544 §6).
|
|
var takeInStall = 5 * time.Minute
|
|
|
|
// takeInMsg is one delivered key, as the take-in uses it.
|
|
type takeInMsg struct {
|
|
delivered
|
|
ack func()
|
|
nak func(delay time.Duration)
|
|
}
|
|
|
|
// takeInKeyOf is the module a delivered key names, false for a delete or a purge, which takes nothing in.
|
|
func takeInKeyOf(bucket string, m jetstream.Msg) (delivered, bool) {
|
|
if op := m.Headers().Get("KV-Operation"); op == "DEL" || op == "PURGE" {
|
|
return delivered{}, false
|
|
}
|
|
key := strings.TrimPrefix(m.Subject(), "$KV."+bucket+".")
|
|
module, _, _ := strings.Cut(key, ".")
|
|
if module == "" {
|
|
return delivered{}, false
|
|
}
|
|
var deliveries uint64 = 1
|
|
if meta, err := m.Metadata(); err == nil {
|
|
deliveries = meta.NumDelivered
|
|
}
|
|
return delivered{module: module, again: deliveries > 1, deliveries: deliveries}, true
|
|
}
|
|
|
|
// takeInConsumer is one bucket's consumer of the take-in.
|
|
type takeInConsumer struct {
|
|
bucket string
|
|
consumer jetstream.Consumer
|
|
}
|
|
|
|
// takeInConfig is the take-in's consumer on bucket's stream.
|
|
func takeInConfig(bucket string) jetstream.ConsumerConfig {
|
|
deliver := jetstream.DeliverNewPolicy
|
|
if bucket == sdkdelivery.ModulesBucket {
|
|
deliver = jetstream.DeliverLastPerSubjectPolicy // the start's look: every module's trunk value once
|
|
}
|
|
return jetstream.ConsumerConfig{
|
|
Durable: broker.TakeInConsumer, FilterSubject: "$KV." + bucket + ".>", DeliverPolicy: deliver,
|
|
AckPolicy: jetstream.AckExplicitPolicy, AckWait: takeInAckWait, MaxAckPending: takeInPending,
|
|
Description: "the controller's take-in of done keys (novox/hq ADR 0544)",
|
|
}
|
|
}
|
|
|
|
// takeInConsumers makes or finds the take-in's consumer on each bucket's stream; one that cannot be made is said, and
|
|
// the other is still read.
|
|
func takeInConsumers(ctx context.Context, js jetstream.JetStream) []takeInConsumer {
|
|
var out []takeInConsumer
|
|
for _, bucket := range broker.TakeInBuckets {
|
|
c, err := js.CreateOrUpdateConsumer(ctx, "KV_"+bucket, takeInConfig(bucket))
|
|
o := takeInNotConsumedObservation(bucket, err)
|
|
if err != nil {
|
|
// Loud: a consumer whose deliver policy differs cannot be updated, and nothing of its bucket is taken in.
|
|
doneKeysSay("done keys: " + o.Summary)
|
|
if keeper := conditionsFrom; keeper != nil {
|
|
_, _ = keeper.Observe(ctx, o)
|
|
}
|
|
continue
|
|
}
|
|
// Made: a condition a controller before this one raised for it is cleared (a restart that works clears it).
|
|
if keeper := conditionsFrom; keeper != nil {
|
|
_, _ = keeper.Clear(ctx, o.Key(), "the take-in's consumer on "+bucket+" is made")
|
|
}
|
|
out = append(out, takeInConsumer{bucket: bucket, consumer: c})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// watchDoneKeys takes in the done keys the two consumers deliver, batch after batch, until ctx ends, and watches their
|
|
// backlog. A consumer that cannot be made is said: its bucket is then not taken in until the next start.
|
|
func watchDoneKeys(ctx context.Context, open *stores) {
|
|
keys, closeKeys, err := openBuilderKeys()
|
|
if err != nil {
|
|
fmt.Printf("done keys: the builder's keys are not taken in: %v\n", err)
|
|
return
|
|
}
|
|
consumers := takeInConsumers(ctx, keys.JS)
|
|
if len(consumers) == 0 {
|
|
closeKeys()
|
|
return
|
|
}
|
|
in := make(chan takeInMsg)
|
|
// moving counts the keys handed to the loop and the keys it settled (acknowledged or refused, each as the bus took
|
|
// it): what tells a take-in that moves from one that holds work and does nothing (the stall watch, ADR 0544 §6).
|
|
moving := &takeInMoving{}
|
|
read := make([]jetstream.Consumer, 0, len(consumers))
|
|
// The connection closes only once the readers and the loop have ended, so the refusals they send at the stop reach
|
|
// the bus (the third review of ADR 0544).
|
|
var users sync.WaitGroup
|
|
for _, c := range consumers {
|
|
bucket := c.bucket
|
|
read = append(read, c.consumer)
|
|
it, err := c.consumer.Messages(jetstream.PullMaxMessages(takeInBuffered))
|
|
if err != nil {
|
|
fmt.Printf("done keys: the take-in's consumer on %s is not read: %v\n", bucket, err)
|
|
continue
|
|
}
|
|
goServing(ctx, func() {
|
|
<-ctx.Done()
|
|
it.Drain() // the keys it holds are handed out and refused at once below, not lost to their ack wait
|
|
})
|
|
users.Add(1)
|
|
goServing(ctx, func() {
|
|
defer users.Done()
|
|
for {
|
|
m, err := it.Next()
|
|
if err != nil {
|
|
if ctx.Err() == nil && !errors.Is(err, jetstream.ErrMsgIteratorClosed) {
|
|
fmt.Printf("done keys: the take-in's consumer on %s stopped: %v\n", bucket, err)
|
|
}
|
|
return
|
|
}
|
|
d, ok := takeInKeyOf(bucket, m)
|
|
if !ok {
|
|
_ = m.Ack()
|
|
continue
|
|
}
|
|
// Settled is the loop's attempt, whatever the bus answers: an acknowledgement it refuses (at a first start,
|
|
// until the bus node is composed) is delivered again and settled again, and never counts as work held
|
|
// (the third review of ADR 0544). Acknowledged synchronously and past the stop, so a refusal is said.
|
|
msg := takeInMsg{delivered: d, ack: func() {
|
|
acking, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
|
|
defer cancel()
|
|
if err := m.DoubleAck(acking); err != nil {
|
|
doneKeysSay(fmt.Sprintf("done keys: the bus did not take the acknowledgement of %s: %v", d.module, err))
|
|
}
|
|
moving.settled.Add(1)
|
|
},
|
|
nak: func(delay time.Duration) {
|
|
_ = m.NakWithDelay(delay)
|
|
moving.settled.Add(1)
|
|
}}
|
|
if ctx.Err() != nil {
|
|
_ = m.Nak()
|
|
continue // drained: each key it held, refused at once
|
|
}
|
|
select {
|
|
case in <- msg:
|
|
moving.handed.Add(1)
|
|
case <-ctx.Done():
|
|
_ = m.Nak() // delivered again at once to the controller after this one, not after the ack wait
|
|
continue
|
|
}
|
|
}
|
|
})
|
|
}
|
|
backlog := func(ctx context.Context) (takeInBacklog, error) { return backlogOf(ctx, read) }
|
|
steps := liveTakeInSteps(open, keys, keys.JS)
|
|
users.Add(1)
|
|
goServing(ctx, func() {
|
|
defer users.Done()
|
|
takeInLoop(ctx, in, steps, backlog)
|
|
})
|
|
goServing(ctx, func() { watchTakeInStall(ctx, backlog, moving) })
|
|
goServing(ctx, func() {
|
|
<-ctx.Done()
|
|
users.Wait()
|
|
closeKeys()
|
|
})
|
|
}
|
|
|
|
// takeInLoop gathers delivered keys into batches and takes each batch in, until ctx ends.
|
|
func takeInLoop(ctx context.Context, in <-chan takeInMsg, steps takeInSteps,
|
|
backlog func(context.Context) (takeInBacklog, error)) {
|
|
t := &takeInState{memo: map[string]string{}, raised: map[string]bool{}, saidWait: map[string]string{}}
|
|
for {
|
|
var batch []takeInMsg
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case m := <-in:
|
|
batch = append(batch, m)
|
|
}
|
|
gather := time.NewTimer(takeInGather)
|
|
more:
|
|
for len(batch) < startTakeIns {
|
|
select {
|
|
case m := <-in:
|
|
batch = append(batch, m)
|
|
case <-gather.C:
|
|
break more
|
|
case <-ctx.Done():
|
|
gather.Stop()
|
|
for _, m := range batch {
|
|
m.nak(0)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
gather.Stop()
|
|
said := t.take(ctx, steps, batch)
|
|
if b, err := backlog(ctx); err == nil {
|
|
said += fmt.Sprintf("; backlog %d", b.Total())
|
|
}
|
|
doneKeysSay("done keys: " + said)
|
|
}
|
|
}
|
|
|
|
// takeInState is what the take-in keeps between batches in this process: the trunk each module was last taken in at,
|
|
// the modules whose condition it raised, and the wait last said of each.
|
|
type takeInState struct {
|
|
memo map[string]string
|
|
raised map[string]bool
|
|
saidWait map[string]string
|
|
}
|
|
|
|
// take takes one batch in and acknowledges or refuses each key, raising and clearing the modules' conditions. It answers
|
|
// the batch in a line.
|
|
func (t *takeInState) take(ctx context.Context, steps takeInSteps, batch []takeInMsg) string {
|
|
keys := make([]delivered, len(batch))
|
|
for i, m := range batch {
|
|
keys[i] = m.delivered
|
|
}
|
|
settled, passes := steps.batch(ctx, keys, t.memo)
|
|
verdicts := make(map[string]verdict, len(settled))
|
|
var taken, nothing, waiting, failed int
|
|
for _, r := range settled {
|
|
verdicts[r.module] = r.verdict
|
|
o := doneKeyObservation(r.module)
|
|
switch {
|
|
case r.err != nil:
|
|
failed++
|
|
doneKeysSay(fmt.Sprintf("done keys: %s: %v", r.module, r.err))
|
|
if keeper := conditionsFrom; keeper != nil && !t.raised[r.module] && ctx.Err() == nil &&
|
|
!errors.Is(r.err, context.Canceled) {
|
|
o.Summary += r.err.Error()
|
|
if _, err := keeper.Observe(ctx, o); err == nil {
|
|
t.raised[r.module] = true
|
|
}
|
|
}
|
|
case r.waits != "":
|
|
waiting++
|
|
if t.saidWait[r.module] != r.waits {
|
|
doneKeysSay(fmt.Sprintf("done keys: %s %s", r.module, r.waits))
|
|
t.saidWait[r.module] = r.waits
|
|
}
|
|
case r.taken:
|
|
taken++
|
|
delete(t.saidWait, r.module)
|
|
if r.said != "" {
|
|
doneKeysSay("done keys: " + r.said)
|
|
}
|
|
// Cleared on every take-in, whoever raised it: a controller before a restart, whose key was delivered again.
|
|
if keeper := conditionsFrom; keeper != nil {
|
|
if _, err := keeper.Clear(ctx, o.Key(), "composed and granted"); err == nil {
|
|
delete(t.raised, r.module)
|
|
}
|
|
}
|
|
case r.nothingNew:
|
|
nothing++
|
|
}
|
|
}
|
|
for _, m := range batch {
|
|
if verdicts[m.module] == refused {
|
|
if ctx.Err() != nil {
|
|
m.nak(0) // cut by the stop: the next controller takes it at once
|
|
} else {
|
|
m.nak(refusalDelay(m.deliveries))
|
|
}
|
|
} else {
|
|
m.ack()
|
|
}
|
|
}
|
|
return fmt.Sprintf("%d key(s) of %d module(s): %d taken in, %d with nothing new, %d waiting, %d failed; %d composer pass(es)",
|
|
len(batch), len(settled), taken, nothing, waiting, failed, passes)
|
|
}
|
|
|
|
// takeInBacklog is the take-in's backlog on the bus: keys not delivered yet, and keys delivered and not acknowledged
|
|
// (being taken in, or refused and waiting for their delay).
|
|
type takeInBacklog struct {
|
|
Pending uint64 `json:"pending"`
|
|
Taking uint64 `json:"taking"`
|
|
}
|
|
|
|
// Total is every key the take-in has not acknowledged.
|
|
func (b takeInBacklog) Total() uint64 { return b.Pending + b.Taking }
|
|
|
|
// backlogOf reads the consumers' own counts from the bus.
|
|
func backlogOf(ctx context.Context, consumers []jetstream.Consumer) (takeInBacklog, error) {
|
|
var b takeInBacklog
|
|
for _, c := range consumers {
|
|
info, err := c.Info(ctx)
|
|
if err != nil {
|
|
return takeInBacklog{}, err
|
|
}
|
|
b.Pending += info.NumPending
|
|
b.Taking += uint64(info.NumAckPending)
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
// readTakeInBacklog is the take-in's backlog for a reader in any process (`status`): the consumers asked by name.
|
|
func readTakeInBacklog(ctx context.Context) (takeInBacklog, error) {
|
|
keys, closeKeys, err := openBuilderKeys()
|
|
if err != nil {
|
|
return takeInBacklog{}, err
|
|
}
|
|
defer closeKeys()
|
|
var consumers []jetstream.Consumer
|
|
for _, bucket := range broker.TakeInBuckets {
|
|
c, err := keys.JS.Consumer(ctx, "KV_"+bucket, broker.TakeInConsumer)
|
|
if err != nil {
|
|
return takeInBacklog{}, fmt.Errorf("the take-in's consumer on %s: %w", bucket, err)
|
|
}
|
|
consumers = append(consumers, c)
|
|
}
|
|
return backlogOf(ctx, consumers)
|
|
}
|
|
|
|
// kindTakeInStalled is the condition of a take-in that holds work and settles none (ADR 0544 §6); kindTakeInNotConsumed
|
|
// that of a bucket whose consumer could not be made.
|
|
const (
|
|
kindTakeInStalled = "take-in-stalled"
|
|
kindTakeInNotConsumed = "take-in-not-consumed"
|
|
)
|
|
|
|
func takeInStalledObservation(b takeInBacklog, since time.Time) conditions.Observation {
|
|
return conditions.Observation{Scope: conditions.ScopeCore, ID: "take-in", Token: kindTakeInStalled,
|
|
Kind: kindTakeInStalled, Severity: conditions.Urgent, Resolver: conditions.ResolverOperator, Source: "done keys",
|
|
Summary: fmt.Sprintf("the take-in of done keys has %d key(s) waiting and has settled none since %s",
|
|
b.Total(), since.UTC().Format(time.RFC3339))}
|
|
}
|
|
|
|
// takeInNotConsumedObservation is the condition of bucket's consumer not made, with its own id, so it is raised and
|
|
// cleared apart from a stall and from the other bucket's.
|
|
func takeInNotConsumedObservation(bucket string, err error) conditions.Observation {
|
|
summary := "the take-in's consumer on " + bucket + " is made"
|
|
if err != nil {
|
|
summary = fmt.Sprintf("the take-in's consumer on %s is not made, so its keys are not taken in until the next "+
|
|
"start: %v", bucket, err)
|
|
}
|
|
return conditions.Observation{Scope: conditions.ScopeCore, ID: "take-in-" + bucket, Token: kindTakeInNotConsumed,
|
|
Kind: kindTakeInNotConsumed, Severity: conditions.Urgent, Resolver: conditions.ResolverOperator,
|
|
Source: "done keys", Summary: summary}
|
|
}
|
|
|
|
// takeInMoving counts the keys handed to the take-in loop and the keys it settled.
|
|
type takeInMoving struct {
|
|
handed, settled atomic.Uint64
|
|
}
|
|
|
|
// stallWatch judges each look: stalled once there was work to do (keys not delivered yet, or keys handed to the loop
|
|
// and not settled) and nothing settled for takeInStall. A refused key waiting for its delay is no work of the loop's:
|
|
// with nothing else, a quiet mesh is not a stall; a loop that hangs on its batch is (the review of ADR 0544). Its first
|
|
// look always says it changed, so a condition a controller before this one raised is cleared once this one moves.
|
|
type stallWatch struct {
|
|
mu sync.Mutex
|
|
settled uint64
|
|
movedAt time.Time
|
|
stalled bool
|
|
known bool
|
|
}
|
|
|
|
// look takes a reading at now: the backlog, and how many keys were handed and settled; it answers whether the take-in
|
|
// stalls and whether that changed (always on the first look).
|
|
func (w *stallWatch) look(b takeInBacklog, handed, settled uint64, now time.Time) (stalled, changed bool) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
work := b.Pending > 0 || handed > settled
|
|
if !work || settled != w.settled || w.movedAt.IsZero() {
|
|
w.movedAt = now
|
|
}
|
|
w.settled = settled
|
|
nowStalled := work && now.Sub(w.movedAt) >= takeInStall
|
|
changed = !w.known || nowStalled != w.stalled
|
|
w.stalled, w.known = nowStalled, true
|
|
return nowStalled, changed
|
|
}
|
|
|
|
// watchTakeInStall reads the backlog every minute and raises take-in-stalled while the take-in holds work and settles
|
|
// none, clearing it when it moves, and at its first look when it does not stall: a watchdog's timer, as research 072 §2
|
|
// keeps them until the health part leaves the controller.
|
|
func watchTakeInStall(ctx context.Context, backlog func(context.Context) (takeInBacklog, error), moving *takeInMoving) {
|
|
w := &stallWatch{}
|
|
tick := time.NewTicker(time.Minute)
|
|
defer tick.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-tick.C:
|
|
}
|
|
b, err := backlog(ctx)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
stalled, changed := w.look(b, moving.handed.Load(), moving.settled.Load(), time.Now())
|
|
keeper := conditionsFrom
|
|
if !changed || keeper == nil {
|
|
continue
|
|
}
|
|
w.mu.Lock()
|
|
since := w.movedAt
|
|
w.mu.Unlock()
|
|
o := takeInStalledObservation(b, since)
|
|
if stalled {
|
|
doneKeysSay("done keys: " + o.Summary)
|
|
_, _ = keeper.Observe(ctx, o)
|
|
} else {
|
|
_, _ = keeper.Clear(ctx, o.Key(), "the take-in moves")
|
|
}
|
|
}
|
|
}
|