Act under a lease, keep accounts by order, one writer at composition (hq to-be 45 Phase 2)
Two controllers could both act (issue 204), a reconcile's report could overtake the apply after it and the digest decided (issue 267), and a grant could make a second writer of a machine's report. - The lease (internal/lease, ADR 0229): mesh-controller_lease key `holder`, 15 s age, renewed every 5 s by compare-and-set; the epoch is the revision it was taken at. The gate is the clock (stops 3 s before expiry); a refused renewal is a loss and the process exits; a holder that stops gives it back. serve takes it before asserting the bus. Epochs kept in the store (migration 0068 controller_epoch) as a floor: a bucket raised from nothing is compacted past it. Unleased (no epoch, S12 urgent) only when nobody holds it and the bus will not let it be written. A shell command acts under the holder's epoch, or its own lease when none. - Declarations carry `epoch` inside the signed envelope, only to a machine whose latest account carried a report_sequence (mesh-host #35); would-send is composed with the epoch last sent. Allot and the send both pass the gate. - Reports: contract in internal/link/order.go (epoch, sequence, report_sequence, older_than, refused_older). Accounts kept by epoch, then sequence, then report sequence; older refused, counted; unordered reports keep the digest rule. Plans by compare-and-set on a revision, with epoch. Conditions and calls carry the epoch and are not written off the lease. - S12 and S13 (naming the writer by epoch) watched, D5 run; reset of the bucket said. Writers table compiled in and enforced in PermissionsFor; the controller no longer publishes mesh.control.>. A contract per consumed kind, and the empty-on-error lint over the repository. - mesh-host pinned to its main with the epoch in the validator (D1 validates the envelope as sent). Needs mesh-host's genesis lock with the lease grant (mesh-host PR) for TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose.
This commit is contained in:
+113
-13
@@ -5,7 +5,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
"github.com/novox/mesh-controller/internal/conditions"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
)
|
||||
|
||||
@@ -70,6 +72,8 @@ const (
|
||||
// staleRefusalsAllowed in staleRefusalsWithin are what S13 lets pass from one writer.
|
||||
staleRefusalsAllowed = 5
|
||||
staleRefusalsWithin = 5 * time.Minute
|
||||
// leaseBound is how long the lease may go unrenewed (S12): the key's age.
|
||||
leaseBound = broker.LeaseTTL
|
||||
)
|
||||
|
||||
// callBounds are the verbs that may run longer than callDefault, and how long (S7).
|
||||
@@ -164,13 +168,20 @@ var signalsTable = []signalRow{
|
||||
return newestOf(f.machines, func(m machineFacts) time.Time { return m.toolsHeard })
|
||||
}},
|
||||
{Row: "S12", Signal: "the controller lease renewed", Emitter: "controller", Trigger: "every 5 s",
|
||||
Bound: "15 s", Kind: "lease-lost", Severity: conditions.Urgent, Phase: 2,
|
||||
Deferred: "the lease is built in Phase 2 (to-be 45 §6): there is nothing renewed to watch yet, and a " +
|
||||
"second controller is caught today by its consumers being bound (standingBy)"},
|
||||
Bound: "15 s (the key's age); a holder that lost the lease, or stopped renewing and was taken over, and a " +
|
||||
"lease bucket found raised again from nothing, are said for an hour after; a controller serving without " +
|
||||
"the lease, for as long as it does",
|
||||
Kind: "lease-lost", Severity: conditions.Urgent, Phase: 2,
|
||||
needs: func(f *signalFacts) error { return f.leaseErr }, watch: watchLease,
|
||||
newest: func(f *signalFacts) time.Time { return f.lease.renewed }},
|
||||
{Row: "S13", Signal: "stale refusals", Emitter: "every receiver (rule 2)", Trigger: "each refusal",
|
||||
Bound: "more than 5 from one machine in 5 min", Kind: "stale-writer", Severity: conditions.Warning, Phase: 1,
|
||||
Bound: "more than 5 from one writer in 5 min: a controller epoch, a controller that claimed none, or a " +
|
||||
"machine's node-engine whose accounts the controller refused",
|
||||
Kind: "stale-writer", Severity: conditions.Warning, Phase: 2,
|
||||
needs: func(*signalFacts) error { return nil }, watch: watchStaleRefusals,
|
||||
newest: func(f *signalFacts) time.Time { return time.Time{} }},
|
||||
newest: func(f *signalFacts) time.Time {
|
||||
return newestOf(f.staleRefusals, func(w link.WriterRefusals) time.Time { return w.Last })
|
||||
}},
|
||||
{Row: "S14", Signal: "facts snapshot exported", Emitter: "controller", Trigger: "daily",
|
||||
Bound: "2 days", Kind: "facts-stale", Severity: conditions.Warning, Phase: 5,
|
||||
Deferred: "the facts snapshot is built in Phase 5 (to-be 45 §9): nothing exports one yet"},
|
||||
@@ -450,16 +461,105 @@ func watchTools(f *signalFacts) []conditions.Observation {
|
||||
|
||||
func watchStaleRefusals(f *signalFacts) []conditions.Observation {
|
||||
var out []conditions.Observation
|
||||
for node, n := range f.staleRefusals {
|
||||
if n <= staleRefusalsAllowed {
|
||||
for _, w := range f.staleRefusals {
|
||||
if w.Count <= staleRefusalsAllowed {
|
||||
continue
|
||||
}
|
||||
out = append(out, conditions.Observation{Scope: conditions.ScopeMachine, ID: node, Kind: "stale-writer",
|
||||
Machine: node, Severity: conditions.Warning,
|
||||
Summary: fmt.Sprintf("%s refused %d declarations in %s as older than the one it holds: a controller "+
|
||||
"is sending what it has moved past (which one is said once declarations carry an epoch, Phase 2)",
|
||||
node, n, staleRefusalsWithin),
|
||||
Said: fmt.Sprintf("%d stale refusals in %s", n, staleRefusalsWithin)})
|
||||
scope, id, machine := conditions.ScopeCore, "controller.unnamed", ""
|
||||
named := w.Writer
|
||||
switch {
|
||||
case w.Epoch > 0:
|
||||
id = fmt.Sprintf("controller.epoch-%d", w.Epoch)
|
||||
if e, ok := f.epochs[w.Epoch]; ok {
|
||||
named = fmt.Sprintf("the controller of epoch %d (%s%s)", w.Epoch, e.Instance, endedWords(e))
|
||||
}
|
||||
case w.Writer == link.WriterNodeEngine(firstOf(w.Receivers)) || strings.HasPrefix(w.Writer, "the node-engine on "):
|
||||
node := strings.TrimPrefix(w.Writer, "the node-engine on ")
|
||||
scope, id, machine = conditions.ScopeMachine, node, node
|
||||
}
|
||||
if machine == "" && len(w.Receivers) > 0 {
|
||||
machine = w.Receivers[0]
|
||||
}
|
||||
var also []string
|
||||
for _, r := range w.Receivers {
|
||||
if r != machine && r != "controller" {
|
||||
also = append(also, r)
|
||||
}
|
||||
}
|
||||
out = append(out, conditions.Observation{Scope: scope, ID: id, Kind: "stale-writer", Token: "stale-writer",
|
||||
Machine: machine, Also: also, Severity: conditions.Warning,
|
||||
Summary: fmt.Sprintf("%s was refused %d time(s) in %s as older than what its receivers hold (%s): a "+
|
||||
"writer is sending what the mesh has moved past", named, w.Count, staleRefusalsWithin,
|
||||
strings.Join(w.Receivers, ", ")),
|
||||
Said: fmt.Sprintf("%d stale refusals in %s, the last at %s", w.Count, staleRefusalsWithin,
|
||||
w.Last.UTC().Format(time.RFC3339))})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// firstOf is a list's first, empty for none.
|
||||
func firstOf(list []string) string {
|
||||
if len(list) == 0 {
|
||||
return ""
|
||||
}
|
||||
return list[0]
|
||||
}
|
||||
|
||||
// endedWords is how an epoch ended, for a sentence naming it; nothing while it is held.
|
||||
func endedWords(e inventory.Epoch) string {
|
||||
if e.Ended == nil {
|
||||
return ", still holding the lease"
|
||||
}
|
||||
return fmt.Sprintf(", %s at %s", e.How, e.Ended.UTC().Format("15:04:05 MST"))
|
||||
}
|
||||
|
||||
// watchLease is S12: the lease held and renewed by this controller, and every holder that lost it.
|
||||
func watchLease(f *signalFacts) []conditions.Observation {
|
||||
var out []conditions.Observation
|
||||
l := f.lease
|
||||
if l.unleased != "" {
|
||||
out = append(out, conditions.Observation{Scope: conditions.ScopeCore, ID: "controller.lease", Token: "unleased",
|
||||
Kind: "lease-lost", Machine: f.host, Severity: conditions.Urgent,
|
||||
Summary: "the controller serves WITHOUT the lease: nothing keeps a second controller from acting " +
|
||||
"beside it, and its declarations carry no epoch. A bus whose user list is older than this " +
|
||||
"controller does not grant it the lease's bucket: a push of the machine holding the bus sends " +
|
||||
"the list that does, and the controller takes the lease within five seconds — " + l.unleased,
|
||||
Said: l.unleased})
|
||||
} else if l.held && !l.renewed.IsZero() && f.now.Sub(l.renewed) > leaseBound {
|
||||
out = append(out, conditions.Observation{Scope: conditions.ScopeCore, ID: "controller.lease", Token: "late",
|
||||
Kind: "lease-lost", Machine: f.host, Severity: conditions.Urgent,
|
||||
Summary: fmt.Sprintf("the controller has not renewed its lease (epoch %d) since %s, past the key's age "+
|
||||
"of %s: another controller may take it", l.epoch, l.renewed.UTC().Format(time.RFC3339), leaseBound),
|
||||
Said: fmt.Sprintf("not renewed for %s", ago(f.now.Sub(l.renewed)))})
|
||||
}
|
||||
if !l.reset.IsZero() && f.now.Sub(l.reset) <= advisoryQuiet {
|
||||
out = append(out, conditions.Observation{Scope: conditions.ScopeCore, ID: "controller.lease", Token: "reset",
|
||||
Kind: "lease-lost", Machine: f.host, Severity: conditions.Urgent,
|
||||
Summary: "the controller lease's bucket was raised again from nothing: " + l.resetSaid,
|
||||
Said: l.resetSaid})
|
||||
}
|
||||
var lost []string
|
||||
newest := inventory.Epoch{}
|
||||
for _, e := range l.ended {
|
||||
if e.Ended == nil || e.How == inventory.EpochReleased || f.now.Sub(*e.Ended) > advisoryQuiet {
|
||||
continue
|
||||
}
|
||||
lost = append(lost, fmt.Sprintf("epoch %d (%s) %s at %s", e.Epoch, e.Instance, e.How,
|
||||
e.Ended.UTC().Format("15:04:05 MST")))
|
||||
if newest.Ended == nil || e.Ended.After(*newest.Ended) {
|
||||
newest = e
|
||||
}
|
||||
}
|
||||
if len(lost) > 0 {
|
||||
how := "lost it: its renewal was refused or could not be made"
|
||||
if newest.How == inventory.EpochExpired {
|
||||
how = "stopped renewing it without giving it back, and was taken over"
|
||||
}
|
||||
out = append(out, conditions.Observation{Scope: conditions.ScopeCore, ID: "controller.lease", Token: "lost",
|
||||
Kind: "lease-lost", Machine: newest.Host, Severity: conditions.Urgent,
|
||||
Summary: fmt.Sprintf("the controller of epoch %d (%s) %s; the controller of epoch %d acts now", newest.Epoch,
|
||||
newest.Instance, how, l.epoch),
|
||||
Said: strings.Join(lost, "; ")})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user