mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 09:53:54 +01:00
6a3a2ee61d
Allow package users to provide custom callback which will execute whenever a healthcheck fails. Allow package users to provide custom callback which will execute whenever a healthcheck succeeds.
320 lines
8.4 KiB
Go
320 lines
8.4 KiB
Go
// Package healthcheck contains a monitor which takes a set of liveness checks
|
|
// which it periodically checks. If a check fails after its configured number
|
|
// of allowed call attempts, the monitor will send a request to shutdown using
|
|
// the function is is provided in its config. Checks are dispatched in their own
|
|
// goroutines so that they do not block each other.
|
|
package healthcheck
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/lightningnetwork/lnd/ticker"
|
|
)
|
|
|
|
var noOpCallback = func() {}
|
|
|
|
// Config contains configuration settings for our monitor.
|
|
type Config struct {
|
|
// Checks is a set of health checks that assert that lnd has access to
|
|
// critical resources.
|
|
Checks []*Observation
|
|
|
|
// Shutdown should be called to request safe shutdown on failure of a
|
|
// health check.
|
|
Shutdown shutdownFunc
|
|
}
|
|
|
|
// shutdownFunc is the signature we use for a shutdown function which allows us
|
|
// to print our reason for shutdown.
|
|
type shutdownFunc func(format string, params ...interface{})
|
|
|
|
// Monitor periodically checks a series of configured liveness checks to
|
|
// ensure that lnd has access to all critical resources.
|
|
type Monitor struct {
|
|
started int32 // To be used atomically.
|
|
stopped int32 // To be used atomically.
|
|
|
|
cfg *Config
|
|
|
|
quit chan struct{}
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
// NewMonitor returns a monitor with the provided config.
|
|
func NewMonitor(cfg *Config) *Monitor {
|
|
return &Monitor{
|
|
cfg: cfg,
|
|
quit: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// Start launches the goroutines required to run our monitor.
|
|
func (m *Monitor) Start() error {
|
|
if !atomic.CompareAndSwapInt32(&m.started, 0, 1) {
|
|
return errors.New("monitor already started")
|
|
}
|
|
|
|
// Run through all of the health checks that we have configured and
|
|
// start a goroutine for each check.
|
|
for _, check := range m.cfg.Checks {
|
|
check := check
|
|
|
|
// Skip over health checks that are disabled by setting zero
|
|
// attempts.
|
|
if check.Attempts == 0 {
|
|
log.Warnf("check: %v configured with 0 attempts, "+
|
|
"skipping it", check.Name)
|
|
|
|
continue
|
|
}
|
|
|
|
m.wg.Add(1)
|
|
go func(check *Observation) {
|
|
defer m.wg.Done()
|
|
|
|
check.monitor(m.cfg.Shutdown, m.quit)
|
|
}(check)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop sends all goroutines the signal to exit and waits for them to exit.
|
|
func (m *Monitor) Stop() error {
|
|
if !atomic.CompareAndSwapInt32(&m.stopped, 0, 1) {
|
|
return fmt.Errorf("monitor already stopped")
|
|
}
|
|
|
|
log.Info("Health monitor shutting down...")
|
|
defer log.Debug("Health monitor shutdown complete")
|
|
|
|
close(m.quit)
|
|
m.wg.Wait()
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddCheck adds a new healthcheck to our monitor.
|
|
func (m *Monitor) AddCheck(check *Observation) error {
|
|
|
|
m.wg.Add(1)
|
|
go func(check *Observation) {
|
|
defer m.wg.Done()
|
|
|
|
check.monitor(m.cfg.Shutdown, m.quit)
|
|
}(check)
|
|
|
|
return nil
|
|
}
|
|
|
|
// CreateCheck is a helper function that takes a function that produces an error
|
|
// and wraps it in a function that returns its result on an error channel.
|
|
// We do not wait group the goroutine running our checkFunc because we expect
|
|
// to be dealing with health checks that may block; if we wait group them, we
|
|
// may wait forever. Ideally future health checks will allow callers to cancel
|
|
// them early, and we can wait group this.
|
|
func CreateCheck(checkFunc func() error) func() chan error {
|
|
return func() chan error {
|
|
errChan := make(chan error, 1)
|
|
go func() {
|
|
errChan <- checkFunc()
|
|
}()
|
|
|
|
return errChan
|
|
}
|
|
}
|
|
|
|
// Observation represents a liveness check that we periodically check.
|
|
type Observation struct {
|
|
// Name describes the health check.
|
|
Name string
|
|
|
|
// Check runs the health check itself, returning an error channel that
|
|
// is expected to receive nil or an error.
|
|
Check func() chan error
|
|
|
|
// Interval is a ticker which triggers running our check function. This
|
|
// ticker must be started and stopped by the observation.
|
|
Interval ticker.Ticker
|
|
|
|
// Attempts is the number of calls we make for a single check before
|
|
// failing.
|
|
Attempts int
|
|
|
|
// Timeout is the amount of time we allow our check function to take
|
|
// before we time it out.
|
|
Timeout time.Duration
|
|
|
|
// Backoff is the amount of time we back off between retries for failed
|
|
// checks.
|
|
Backoff time.Duration
|
|
|
|
// OnSuccess is a callback which will be executed when the healthcheck
|
|
// succeeds. This is optional.
|
|
OnSuccess func()
|
|
|
|
// OnFailure is a callback which will be executed when the healthcheck
|
|
// fails. This is optional.
|
|
OnFailure func()
|
|
}
|
|
|
|
// ObservationOption describes the signature of a functional option that can be
|
|
// used to modify the behaviour of an Observation.
|
|
type ObservationOption func(*Observation)
|
|
|
|
// WithSuccessCallback configures an observation with a callback to be fired
|
|
// whenever the health check succeeds.
|
|
func WithSuccessCallback(callback func()) ObservationOption {
|
|
return func(o *Observation) {
|
|
o.OnSuccess = callback
|
|
}
|
|
}
|
|
|
|
// WithFailureCallback configures an observation with a callback to be fired
|
|
// whenever the health check reaches its failure threshold.
|
|
func WithFailureCallback(callback func()) ObservationOption {
|
|
return func(o *Observation) {
|
|
o.OnFailure = callback
|
|
}
|
|
}
|
|
|
|
// NewObservation creates an observation.
|
|
func NewObservation(name string, check func() error, interval, timeout,
|
|
backoff time.Duration, attempts int,
|
|
opts ...ObservationOption) *Observation {
|
|
|
|
observation := &Observation{
|
|
Name: name,
|
|
Check: CreateCheck(check),
|
|
Interval: ticker.New(interval),
|
|
Attempts: attempts,
|
|
Timeout: timeout,
|
|
Backoff: backoff,
|
|
}
|
|
|
|
// Apply each option to the observation.
|
|
for _, opt := range opts {
|
|
opt(observation)
|
|
}
|
|
|
|
// Ensure that we default to NO-OP callbacks.
|
|
if observation.OnSuccess == nil {
|
|
observation.OnSuccess = noOpCallback
|
|
}
|
|
|
|
if observation.OnFailure == nil {
|
|
observation.OnFailure = noOpCallback
|
|
}
|
|
|
|
return observation
|
|
}
|
|
|
|
// String returns a string representation of an observation.
|
|
func (o *Observation) String() string {
|
|
return o.Name
|
|
}
|
|
|
|
// monitor executes a health check every time its interval ticks until the quit
|
|
// channel signals that we should shutdown. This function is also responsible
|
|
// for starting and stopping our ticker.
|
|
func (o *Observation) monitor(shutdown shutdownFunc, quit chan struct{}) {
|
|
log.Debugf("Monitoring: %v", o)
|
|
|
|
o.Interval.Resume()
|
|
defer o.Interval.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-o.Interval.Ticks():
|
|
// retryCheck will return errMaxAttemptsReached when
|
|
// the max attempts are reached. In that case we will
|
|
// stop the ticker and quit.
|
|
if o.retryCheck(quit, shutdown) {
|
|
log.Debugf("Health check: max attempts " +
|
|
"failed, monitor exiting")
|
|
return
|
|
}
|
|
|
|
// Exit if we receive the instruction to shutdown.
|
|
case <-quit:
|
|
log.Debug("Health check: monitor quit")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// retryCheck calls a check function until it succeeds, or we reach our
|
|
// configured number of attempts, waiting for our back off period between failed
|
|
// calls. If we fail to obtain a passing health check after the allowed number
|
|
// of calls, we will request shutdown. It returns a bool to indicate whether
|
|
// the max number of attempts is reached.
|
|
func (o *Observation) retryCheck(quit chan struct{},
|
|
shutdown shutdownFunc) bool {
|
|
|
|
var count int
|
|
|
|
for count < o.Attempts {
|
|
// Increment our call count and call the health check endpoint.
|
|
count++
|
|
|
|
// Wait for our check to return, timeout to elapse, or quit
|
|
// signal to be received.
|
|
var err error
|
|
select {
|
|
case err = <-o.Check():
|
|
// If our error is nil, we have passed our health check,
|
|
// so we'll invoke our success callback if defined and
|
|
// then exit.
|
|
if err == nil {
|
|
log.Debug("invoking success callback")
|
|
|
|
// Invoke the success callback.
|
|
o.OnSuccess()
|
|
|
|
return false
|
|
}
|
|
|
|
case <-time.After(o.Timeout):
|
|
err = fmt.Errorf("health check: %v timed out after: "+
|
|
"%v", o, o.Timeout)
|
|
|
|
case <-quit:
|
|
log.Debug("Health check: monitor quit")
|
|
return false
|
|
}
|
|
|
|
// If we have reached our allowed number of attempts, this
|
|
// check has failed so we'll fire the on failure callback
|
|
// and request shutdown.
|
|
if count == o.Attempts {
|
|
log.Debug("invoking failure callback")
|
|
|
|
o.OnFailure()
|
|
|
|
shutdown("Health check: %v failed after %v "+
|
|
"calls", o, o.Attempts)
|
|
return true
|
|
}
|
|
|
|
log.Infof("Health check: %v, call: %v failed with: %v, "+
|
|
"backing off for: %v", o, count, err, o.Backoff)
|
|
|
|
// If we are still within the number of calls allowed for this
|
|
// check, we wait for our back off period to elapse, or exit if
|
|
// we get the signal to shutdown.
|
|
select {
|
|
case <-time.After(o.Backoff):
|
|
|
|
case <-quit:
|
|
log.Debug("Health check: monitor quit")
|
|
return false
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|