212 lines
7.1 KiB
Go
212 lines
7.1 KiB
Go
|
package autopilot
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"sync"
|
||
|
"time"
|
||
|
|
||
|
hclog "github.com/hashicorp/go-hclog"
|
||
|
"github.com/hashicorp/raft"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
// These constants were take from what exists in Consul at the time of module extraction.
|
||
|
|
||
|
DefaultUpdateInterval = 2 * time.Second
|
||
|
DefaultReconcileInterval = 10 * time.Second
|
||
|
)
|
||
|
|
||
|
// Option is an option to be used when creating a new Autopilot instance
|
||
|
type Option func(*Autopilot)
|
||
|
|
||
|
// WithUpdateInterval returns an Option to set the Autopilot instance's
|
||
|
// update interval.
|
||
|
func WithUpdateInterval(t time.Duration) Option {
|
||
|
if t == 0 {
|
||
|
t = DefaultUpdateInterval
|
||
|
}
|
||
|
return func(a *Autopilot) {
|
||
|
a.updateInterval = t
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// WithReconcileInterval returns an Option to set the Autopilot instance's
|
||
|
// reconcile interval.
|
||
|
func WithReconcileInterval(t time.Duration) Option {
|
||
|
if t == 0 {
|
||
|
t = DefaultReconcileInterval
|
||
|
}
|
||
|
return func(a *Autopilot) {
|
||
|
a.reconcileInterval = t
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// WithLogger returns an Option to set the Autopilot instance's logger
|
||
|
func WithLogger(logger hclog.Logger) Option {
|
||
|
if logger == nil {
|
||
|
logger = hclog.Default()
|
||
|
}
|
||
|
|
||
|
return func(a *Autopilot) {
|
||
|
a.logger = logger.Named("autopilot")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// withTimeProvider returns an Option which overrides and Autopilot instance's
|
||
|
// time provider with the given one. This should only be used in tests
|
||
|
// as a means of making some time.Time values in an autopilot state deterministic.
|
||
|
// For real uses the default runtimeTimeProvider should be used.
|
||
|
func withTimeProvider(provider timeProvider) Option {
|
||
|
return func(a *Autopilot) {
|
||
|
a.time = provider
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// WithPromoter returns an option to set the Promoter type that Autpilot will
|
||
|
// use. When the option is not given the default StablePromoter from this package
|
||
|
// will be used.
|
||
|
func WithPromoter(promoter Promoter) Option {
|
||
|
if promoter == nil {
|
||
|
promoter = DefaultPromoter()
|
||
|
}
|
||
|
|
||
|
return func(a *Autopilot) {
|
||
|
a.promoter = promoter
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Autopilot is the type to manage a running Raft instance.
|
||
|
//
|
||
|
// Each Raft node in the cluster will have a corresponding Autopilot instance but
|
||
|
// only 1 Autopilot instance should run at a time in the cluster. So when a node
|
||
|
// gains Raft leadership the corresponding Autopilot instance should have it's
|
||
|
// Start method called. Then if leadership is lost that node should call the
|
||
|
// Stop method on the Autopilot instance.
|
||
|
type Autopilot struct {
|
||
|
logger hclog.Logger
|
||
|
// delegate is used to get information about the system such as Raft server
|
||
|
// states, known servers etc.
|
||
|
delegate ApplicationIntegration
|
||
|
// promoter is used to calculate promotions, demotions and leadership transfers
|
||
|
// given a particular autopilot State. The interface also contains methods
|
||
|
// for filling in parts of the autopilot state that the core module doesn't
|
||
|
// control such as the Ext fields on the Server and State types.
|
||
|
promoter Promoter
|
||
|
// raft is an interface that implements all the parts of the Raft library interface
|
||
|
// that we use. It is an interface to allow for mocking raft during testing.
|
||
|
raft Raft
|
||
|
// time is an interface with a single method for getting the current time - `Now`.
|
||
|
// In some tests this will be the MockTimeProvider which allows tests to be more
|
||
|
// deterministic but for running systems this should not be overrided from the
|
||
|
// default which is the runtimeTimeProvider and is a small shim around calling
|
||
|
// time.Now.
|
||
|
time timeProvider
|
||
|
|
||
|
// reconcileInterval is how long between rounds of performing promotions, demotions
|
||
|
// and leadership transfers.
|
||
|
reconcileInterval time.Duration
|
||
|
|
||
|
// updateInterval is the time between the periodic state updates. These periodic
|
||
|
// state updates take in known servers from the delegate, request Raft stats be
|
||
|
// fetched and pull in other inputs such as the Raft configuration to create
|
||
|
// an updated view of the Autopilot State.
|
||
|
updateInterval time.Duration
|
||
|
|
||
|
// state is the structure that autopilot uses to make decisions about what to do.
|
||
|
// This field should be considered immutable and no modifications to an existing
|
||
|
// state should be made but instead a new state is created and set to this field
|
||
|
// while holding the stateLock.
|
||
|
state *State
|
||
|
// stateLock is meant to only protect the state field. This just prevents
|
||
|
// the periodic state update and consumers requesting the autopilot state from
|
||
|
// racing.
|
||
|
stateLock sync.RWMutex
|
||
|
|
||
|
// startTime is recorded so that we can make better determinations about server
|
||
|
// stability during the initial period of time after autopilot first starts.
|
||
|
// If autopilot has just started the default behavior to check if a server is
|
||
|
// stable will not work as it will ensure the server has been healthy for
|
||
|
// the configured server stabilization time. If that configure time is longer
|
||
|
// than the amount of time autopilot has been running you can run into issues
|
||
|
// with leadership flapping during some scenarios where a cluster is being
|
||
|
// brought up.
|
||
|
startTime time.Time
|
||
|
|
||
|
// running is a simple bool to indicate whether the go routines to actually
|
||
|
// execute autopilot are currently running
|
||
|
running bool
|
||
|
|
||
|
// removeDeadCh is used to trigger the running autopilot go routines to
|
||
|
// find and remove any dead/failed servers
|
||
|
removeDeadCh chan struct{}
|
||
|
|
||
|
// reconcileCh is used to trigger an immediate round of reconciliation.
|
||
|
reconcileCh chan struct{}
|
||
|
|
||
|
// shutdown is a function that can be execute to shutdown a running
|
||
|
// autopilot's go routines.
|
||
|
shutdown context.CancelFunc
|
||
|
// done is a chan that will be closed when the running autopilot go
|
||
|
// routines have exited. Technically closing it is the very last
|
||
|
// thing done in the go routine but at that point enough state has
|
||
|
// been cleaned up that we would then allow it to be started
|
||
|
// immediately afterward
|
||
|
done chan struct{}
|
||
|
|
||
|
// runLock is meant to protect all of the fields regarding coordination
|
||
|
// of whether the autopilot go routines are running and
|
||
|
// starting/stopping them.
|
||
|
runLock sync.Mutex
|
||
|
}
|
||
|
|
||
|
// New will create a new Autopilot instance utilizing the given Raft and Delegate.
|
||
|
// If the WithPromoter option is not provided the default StablePromoter will
|
||
|
// be used.
|
||
|
func New(raft Raft, delegate ApplicationIntegration, options ...Option) *Autopilot {
|
||
|
a := &Autopilot{
|
||
|
raft: raft,
|
||
|
delegate: delegate,
|
||
|
promoter: DefaultPromoter(),
|
||
|
logger: hclog.Default().Named("autopilot"),
|
||
|
// should this be buffered?
|
||
|
removeDeadCh: make(chan struct{}, 1),
|
||
|
reconcileInterval: DefaultReconcileInterval,
|
||
|
updateInterval: DefaultUpdateInterval,
|
||
|
time: &runtimeTimeProvider{},
|
||
|
}
|
||
|
|
||
|
for _, opt := range options {
|
||
|
opt(a)
|
||
|
}
|
||
|
|
||
|
return a
|
||
|
}
|
||
|
|
||
|
// RemoveDeadServers will trigger an immediate removal of dead/failed servers.
|
||
|
func (a *Autopilot) RemoveDeadServers() {
|
||
|
select {
|
||
|
case a.removeDeadCh <- struct{}{}:
|
||
|
default:
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// GetState retrieves the current autopilot State
|
||
|
func (a *Autopilot) GetState() *State {
|
||
|
a.stateLock.Lock()
|
||
|
defer a.stateLock.Unlock()
|
||
|
return a.state
|
||
|
}
|
||
|
|
||
|
// GetServerHealth returns the latest ServerHealth for a given server.
|
||
|
// The returned struct should not be modified or else it will im
|
||
|
func (a *Autopilot) GetServerHealth(id raft.ServerID) *ServerHealth {
|
||
|
state := a.GetState()
|
||
|
|
||
|
srv, ok := state.Servers[id]
|
||
|
if ok {
|
||
|
return &srv.Health
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|