mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
The module already called itself github.com/luxfi/node while containing no node in Go: cmd/luxd was twenty-one lines that printed the size of a few types, and the Go column of the matrix measured a VM plugin. A repo that fetches the thing it claims to be has not replaced it, so the Go sources move in — 1515 files, the daemon and everything under it — and the build reaches outside this module for nothing. Rust and C++ sit beside it unchanged and are still measured against it by the differential. That is the point of one repo: the reference implementation and the two ports are the same checkout, so parity is a thing you can run rather than a thing you assert. Verified: builds, and the closure carries no lux-private — checked at build time rather than believed. The daemon starts twelve chains on a local network (P X C Q Z A B G K M F, plus D through its plugin) and reports healthy.
343 lines
8.6 KiB
Go
343 lines
8.6 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package health
|
|
|
|
import (
|
|
"cmp"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
"sync"
|
|
"time"
|
|
|
|
apihealth "github.com/luxfi/api/health"
|
|
apitypes "github.com/luxfi/api/types"
|
|
"github.com/luxfi/log"
|
|
"github.com/luxfi/math/set"
|
|
"github.com/luxfi/metric"
|
|
"github.com/luxfi/util"
|
|
)
|
|
|
|
var (
|
|
allTags = []string{AllTag}
|
|
|
|
errRestrictedTag = errors.New("restricted tag")
|
|
errDuplicateCheck = errors.New("duplicate check")
|
|
)
|
|
|
|
type worker struct {
|
|
log log.Logger
|
|
name string
|
|
failingChecks metric.GaugeVec
|
|
checksLock sync.RWMutex
|
|
checks map[string]*taggedChecker
|
|
|
|
resultsLock sync.RWMutex
|
|
results map[string]apihealth.Result
|
|
numFailingApplicationChecks int
|
|
tags map[string]set.Set[string] // tag -> set of check names
|
|
|
|
startOnce sync.Once
|
|
closeOnce sync.Once
|
|
wg sync.WaitGroup
|
|
closer chan struct{}
|
|
}
|
|
|
|
type taggedChecker struct {
|
|
checker Checker
|
|
isApplicationCheck bool
|
|
tags []string
|
|
}
|
|
|
|
func newWorker(
|
|
log log.Logger,
|
|
name string,
|
|
failingChecks metric.GaugeVec,
|
|
) *worker {
|
|
// Initialize the number of failing checks to 0 for all checks
|
|
for _, tag := range []string{AllTag, ApplicationTag} {
|
|
failingChecks.With(metric.Labels{
|
|
CheckLabel: name,
|
|
TagLabel: tag,
|
|
}).Set(0)
|
|
}
|
|
return &worker{
|
|
log: log,
|
|
name: name,
|
|
failingChecks: failingChecks,
|
|
checks: make(map[string]*taggedChecker),
|
|
results: make(map[string]apihealth.Result),
|
|
closer: make(chan struct{}),
|
|
tags: make(map[string]set.Set[string]),
|
|
}
|
|
}
|
|
|
|
func (w *worker) RegisterCheck(name string, check Checker, tags ...string) error {
|
|
// We ensure [AllTag] isn't contained in [tags] to prevent metrics from
|
|
// double counting.
|
|
if slices.Contains(tags, AllTag) {
|
|
return fmt.Errorf("%w: %q", errRestrictedTag, AllTag)
|
|
}
|
|
|
|
w.checksLock.Lock()
|
|
defer w.checksLock.Unlock()
|
|
|
|
if _, ok := w.checks[name]; ok {
|
|
return fmt.Errorf("%w: %q", errDuplicateCheck, name)
|
|
}
|
|
|
|
// Acquire resultsLock before modifying tags and results maps
|
|
w.resultsLock.Lock()
|
|
|
|
// Add the check to each tag
|
|
for _, tag := range tags {
|
|
names, ok := w.tags[tag]
|
|
if !ok {
|
|
names = make(set.Set[string])
|
|
}
|
|
names.Add(name)
|
|
w.tags[tag] = names
|
|
}
|
|
// Add the special AllTag descriptor
|
|
names, ok := w.tags[AllTag]
|
|
if !ok {
|
|
names = make(set.Set[string])
|
|
}
|
|
names.Add(name)
|
|
w.tags[AllTag] = names
|
|
|
|
applicationChecks := w.tags[ApplicationTag]
|
|
tc := &taggedChecker{
|
|
checker: check,
|
|
isApplicationCheck: applicationChecks.Contains(name),
|
|
tags: tags,
|
|
}
|
|
w.checks[name] = tc
|
|
w.results[name] = notYetRunResult
|
|
|
|
w.resultsLock.Unlock()
|
|
|
|
// Whenever a new check is added - it is failing
|
|
w.log.Info("registered new check and initialized its state to failing",
|
|
log.UserString("workerName", w.name),
|
|
log.UserString("checkName", name),
|
|
log.Reflect("tags", tags),
|
|
)
|
|
|
|
// If this is a new application-wide check, then all of the registered tags
|
|
// now have one additional failing check.
|
|
w.updateMetrics(tc, false /*=healthy*/, true /*=register*/)
|
|
return nil
|
|
}
|
|
|
|
func (w *worker) RegisterMonotonicCheck(name string, checker Checker, tags ...string) error {
|
|
var result utils.Atomic[any]
|
|
return w.RegisterCheck(name, CheckerFunc(func(ctx context.Context) (any, error) {
|
|
details := result.Get()
|
|
if details != nil {
|
|
return details, nil
|
|
}
|
|
|
|
details, err := checker.HealthCheck(ctx)
|
|
if err == nil {
|
|
result.Set(details)
|
|
}
|
|
return details, err
|
|
}), tags...)
|
|
}
|
|
|
|
func (w *worker) Results(tags ...string) (map[string]apihealth.Result, bool) {
|
|
w.resultsLock.RLock()
|
|
defer w.resultsLock.RUnlock()
|
|
|
|
// if no tags are specified, return all checks
|
|
if len(tags) == 0 {
|
|
tags = allTags
|
|
}
|
|
|
|
names := make(set.Set[string])
|
|
tagSet := set.Of(tags...)
|
|
tagSet.Add(ApplicationTag) // we always want to include the application tag
|
|
for tag := range tagSet {
|
|
if tagSet, ok := w.tags[tag]; ok {
|
|
names = names.Union(tagSet)
|
|
}
|
|
}
|
|
|
|
results := make(map[string]apihealth.Result, names.Len())
|
|
healthy := true
|
|
for name := range names {
|
|
if result, ok := w.results[name]; ok {
|
|
results[name] = result
|
|
healthy = healthy && result.Error == nil
|
|
}
|
|
}
|
|
return results, healthy
|
|
}
|
|
|
|
func (w *worker) Start(ctx context.Context, freq time.Duration) {
|
|
w.startOnce.Do(func() {
|
|
detachedCtx := context.WithoutCancel(ctx)
|
|
w.wg.Add(1)
|
|
go func() {
|
|
ticker := time.NewTicker(freq)
|
|
defer func() {
|
|
ticker.Stop()
|
|
w.wg.Done()
|
|
}()
|
|
|
|
w.runChecks(detachedCtx)
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
w.runChecks(detachedCtx)
|
|
case <-w.closer:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
})
|
|
}
|
|
|
|
func (w *worker) Stop() {
|
|
w.closeOnce.Do(func() {
|
|
close(w.closer)
|
|
w.wg.Wait()
|
|
})
|
|
}
|
|
|
|
func (w *worker) runChecks(ctx context.Context) {
|
|
w.checksLock.RLock()
|
|
// Copy the [w.checks] map to collect the checks that we will be running
|
|
// during this iteration. If [w.checks] is modified during this iteration of
|
|
// [runChecks], then the added check will not be run until the next
|
|
// iteration.
|
|
checks := maps.Clone(w.checks)
|
|
w.checksLock.RUnlock()
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(len(checks))
|
|
for name, check := range checks {
|
|
go w.runCheck(ctx, &wg, name, check)
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
func (w *worker) runCheck(ctx context.Context, wg *sync.WaitGroup, name string, check *taggedChecker) {
|
|
defer wg.Done()
|
|
|
|
start := time.Now()
|
|
|
|
// To avoid any deadlocks when [RegisterCheck] is called with a lock
|
|
// that is grabbed by [check.HealthCheck], we ensure that no locks
|
|
// are held when [check.HealthCheck] is called.
|
|
details, err := check.checker.HealthCheck(ctx)
|
|
end := time.Now()
|
|
|
|
// A check says what it likes — the disk check answers with an object, the
|
|
// BLS check with a sentence, a chain's check with whatever suits it — so
|
|
// the details are encoded HERE, where the concrete type is still known,
|
|
// and travel as the bytes of their own JSON. That is the one shape every
|
|
// check shares, and a caller reads exactly what was produced.
|
|
said, encodeErr := apitypes.RawOf(details)
|
|
if encodeErr != nil {
|
|
said, err = nil, cmp.Or(err, fmt.Errorf("check details could not be encoded: %w", encodeErr))
|
|
}
|
|
|
|
result := apihealth.Result{
|
|
Details: said,
|
|
Timestamp: apitypes.TimeOf(end),
|
|
Duration: end.Sub(start),
|
|
}
|
|
|
|
w.resultsLock.Lock()
|
|
defer w.resultsLock.Unlock()
|
|
prevResult := w.results[name]
|
|
if err != nil {
|
|
errString := err.Error()
|
|
result.Error = &errString
|
|
|
|
result.ContiguousFailures = prevResult.ContiguousFailures + 1
|
|
if prevResult.ContiguousFailures > 0 {
|
|
result.TimeOfFirstFailure = prevResult.TimeOfFirstFailure
|
|
} else {
|
|
first := apitypes.TimeOf(end)
|
|
result.TimeOfFirstFailure = &first
|
|
}
|
|
|
|
if prevResult.Error == nil {
|
|
w.log.Warn("check started failing",
|
|
log.UserString("workerName", w.name),
|
|
log.UserString("checkName", name),
|
|
log.Reflect("tags", check.tags),
|
|
log.Reflect("error", err),
|
|
)
|
|
w.updateMetrics(check, false /*=healthy*/, false /*=register*/)
|
|
}
|
|
} else if prevResult.Error != nil {
|
|
w.log.Info("check started passing",
|
|
log.UserString("workerName", w.name),
|
|
log.UserString("checkName", name),
|
|
log.Reflect("tags", check.tags),
|
|
)
|
|
w.updateMetrics(check, true /*=healthy*/, false /*=register*/)
|
|
}
|
|
w.results[name] = result
|
|
}
|
|
|
|
// updateMetrics updates the metrics for the given check. If [healthy] is true,
|
|
// then the check is considered healthy and the metrics are decremented.
|
|
// Otherwise, the check is considered unhealthy and the metrics are incremented.
|
|
// [register] must be true only if this is the first time the check is being
|
|
// registered.
|
|
func (w *worker) updateMetrics(tc *taggedChecker, healthy bool, register bool) {
|
|
if tc.isApplicationCheck {
|
|
// Note: [w.tags] will include AllTag.
|
|
for tag := range w.tags {
|
|
gauge := w.failingChecks.With(metric.Labels{
|
|
CheckLabel: w.name,
|
|
TagLabel: tag,
|
|
})
|
|
if healthy {
|
|
gauge.Dec()
|
|
} else {
|
|
gauge.Inc()
|
|
}
|
|
}
|
|
if healthy {
|
|
w.numFailingApplicationChecks--
|
|
} else {
|
|
w.numFailingApplicationChecks++
|
|
}
|
|
} else {
|
|
for _, tag := range tc.tags {
|
|
gauge := w.failingChecks.With(metric.Labels{
|
|
CheckLabel: w.name,
|
|
TagLabel: tag,
|
|
})
|
|
if healthy {
|
|
gauge.Dec()
|
|
} else {
|
|
gauge.Inc()
|
|
// If this is the first time this tag was registered, we also need to
|
|
// account for the currently failing application-wide checks.
|
|
if register && w.tags[tag].Len() == 1 {
|
|
gauge.Add(float64(w.numFailingApplicationChecks))
|
|
}
|
|
}
|
|
}
|
|
gauge := w.failingChecks.With(metric.Labels{
|
|
CheckLabel: w.name,
|
|
TagLabel: AllTag,
|
|
})
|
|
if healthy {
|
|
gauge.Dec()
|
|
} else {
|
|
gauge.Inc()
|
|
}
|
|
}
|
|
}
|