Documentation ¶
Overview ¶
Package cruisectl implements a "cruise control" system for Flow by adjusting nodes' latest ProposalTiming in response to changes in the measured view rate and target epoch switchover time.
It uses a PID controller with the projected epoch switchover time as the process variable and the set-point computed using epoch length config. The error is the difference between the projected epoch switchover time, assuming an ideal view time τ, and the target epoch switchover time (based on a schedule).
Index ¶
- type BlockTimeController
- func (ctl *BlockTimeController) EpochEmergencyFallbackTriggered()
- func (ctl *BlockTimeController) EpochSetupPhaseStarted(_ uint64, first *flow.Header)
- func (ctl *BlockTimeController) OnBlockIncorporated(block *model.Block)
- func (ctl *BlockTimeController) TargetPublicationTime(proposalView uint64, timeViewEntered time.Time, parentBlockId flow.Identifier) time.Time
- type Config
- type ControllerParams
- type Ewma
- type LeakyIntegrator
- type ProposalTiming
- type TimedBlock
- type TimingConfig
- func (ctl TimingConfig) GetEnabled() bool
- func (ctl TimingConfig) GetFallbackProposalDuration() time.Duration
- func (ctl TimingConfig) GetMaxViewDuration() time.Duration
- func (ctl TimingConfig) GetMinViewDuration() time.Duration
- func (ctl TimingConfig) SetEnabled(enabled bool) error
- func (ctl TimingConfig) SetFallbackProposalDuration(dur time.Duration) error
- func (ctl TimingConfig) SetMaxViewDuration(dur time.Duration) error
- func (ctl TimingConfig) SetMinViewDuration(dur time.Duration) error
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type BlockTimeController ¶
type BlockTimeController struct { component.Component protocol.Consumer // consumes protocol state events // contains filtered or unexported fields }
BlockTimeController dynamically adjusts the ProposalTiming of this node, based on the measured view rate of the consensus committee as a whole, in order to achieve a desired switchover time for each epoch. In a nutshell, the controller outputs the block time on the happy path, i.e.
- Suppose the node is observing the parent block B0 at some time `x0`.
- The controller determines the duration `d` of how much later the child block B1 should be observed by the committee.
- The controller internally memorizes the latest B0 it has seen and outputs the tuple `(B0, x0, d)`
This low-level controller output `(B0, x0, d)` is wrapped into a `ProposalTiming` interface, specifically `happyPathBlockTime` on the happy path. The purpose of the `ProposalTiming` wrapper is to translate the raw controller output into a form that is useful for the EventHandler. Edge cases, such as initialization or epoch fallback are implemented by other implementations of `ProposalTiming`.
func NewBlockTimeController ¶
func NewBlockTimeController(log zerolog.Logger, metrics module.CruiseCtlMetrics, config *Config, state protocol.State, curView uint64) (*BlockTimeController, error)
NewBlockTimeController returns a new BlockTimeController.
func (*BlockTimeController) EpochEmergencyFallbackTriggered ¶
func (ctl *BlockTimeController) EpochEmergencyFallbackTriggered()
EpochEmergencyFallbackTriggered responds to epoch fallback mode being triggered.
func (*BlockTimeController) EpochSetupPhaseStarted ¶
func (ctl *BlockTimeController) EpochSetupPhaseStarted(_ uint64, first *flow.Header)
EpochSetupPhaseStarted responds to the EpochSetup phase starting for the current epoch. The event is queued for async processing by the worker.
func (*BlockTimeController) OnBlockIncorporated ¶
func (ctl *BlockTimeController) OnBlockIncorporated(block *model.Block)
OnBlockIncorporated listens to notification from HotStuff about incorporating new blocks. The event is queued for async processing by the worker. If the channel is full, the event is discarded - since we are taking an average it doesn't matter if we occasionally miss a sample.
func (*BlockTimeController) TargetPublicationTime ¶
func (ctl *BlockTimeController) TargetPublicationTime(proposalView uint64, timeViewEntered time.Time, parentBlockId flow.Identifier) time.Time
TargetPublicationTime is intended to be called by the EventHandler, whenever it wants to publish a new proposal. The event handler inputs
- proposalView: the view it is proposing for,
- timeViewEntered: the time when the EventHandler entered this view
- parentBlockId: the ID of the parent block, which the EventHandler is building on
TargetPublicationTime returns the time stamp when the new proposal should be broadcasted. For a given view where we are the primary, suppose the actual time we are done building our proposal is P:
- if P < TargetPublicationTime(..), then the EventHandler should wait until `TargetPublicationTime` to broadcast the proposal
- if P >= TargetPublicationTime(..), then the EventHandler should immediately broadcast the proposal
Note: Technically, our metrics capture the publication delay relative to this function's _latest_ call. Currently, the EventHandler is the only caller of this function, and only calls it once per proposal.
Concurrency safe.
type Config ¶
type Config struct { TimingConfig ControllerParams }
Config defines configuration for the BlockTimeController.
func DefaultConfig ¶
func DefaultConfig() *Config
DefaultConfig returns the default config for the BlockTimeController.
type ControllerParams ¶
type ControllerParams struct { // N_ewma defines how historical measurements are incorporated into the EWMA for the proportional error term. // Intuition: Suppose the input changes from x to y instantaneously: // - N_ewma is the number of samples required to move the EWMA output about 2/3 of the way from x to y // Per convention, this must be a _positive_ integer. N_ewma uint // N_itg defines how historical measurements are incorporated into the integral error term. // Intuition: For a constant error x: // - the integrator value will saturate at `x•N_itg` // - an integrator initialized at 0 reaches 2/3 of the saturation value after N_itg samples // Per convention, this must be a _positive_ integer. N_itg uint // KP, KI, KD, are the coefficients to the PID controller and define its response. // KP adjusts the proportional term (responds to the magnitude of error). // KI adjusts the integral term (responds to the error sum over a recent time interval). // KD adjusts the derivative term (responds to the rate of change, i.e. time derivative, of the error). KP, KI, KD float64 }
ControllerParams specifies the BlockTimeController's internal parameters.
type Ewma ¶
type Ewma struct {
// contains filtered or unexported fields
}
Ewma implements the exponentially weighted moving average with smoothing factor α. The Ewma is a filter commonly applied to time-discrete signals. Mathematically, it is represented by the recursive update formula
value ← α·v + (1-α)·value
where `v` the next observation. Intuitively, the loss factor `α` relates to the time window of N observations that we average over. For example, let α ≡ 1/N and consider an input that suddenly changes from x to y as a step function. Then N is _roughly_ the number of samples required to move the output average about 2/3 of the way from x to y. For numeric stability, we require α to satisfy 0 < a < 1. Not concurrency safe.
func NewEwma ¶
NewEwma instantiates a new exponentially weighted moving average. The smoothing factor `alpha` relates to the averaging time window. Let `alpha` ≡ 1/N and consider an input that suddenly changes from x to y as a step function. Then N is roughly the number of samples required to move the output average about 2/3 of the way from x to y. For numeric stability, we require `alpha` to satisfy 0 < `alpha` < 1.
func (*Ewma) AddObservation ¶
AddObservation adds the value `v` to the EWMA. Returns the updated value.
func (*Ewma) AddRepeatedObservation ¶
AddRepeatedObservation adds k consecutive observations with the same value v. Returns the updated value.
type LeakyIntegrator ¶
type LeakyIntegrator struct {
// contains filtered or unexported fields
}
LeakyIntegrator is a filter commonly applied to time-discrete signals. Intuitively, it sums values over a limited time window. This implementation is parameterized by the loss factor `ß`:
value ← v + (1-ß)·value
where `v` the next observation. Intuitively, the loss factor `ß` relates to the time window of N observations that we integrate over. For example, let ß ≡ 1/N and consider a constant input x:
- the integrator value will saturate at x·N
- an integrator initialized at 0 reaches 2/3 of the saturation value after N samples
For numeric stability, we require ß to satisfy 0 < ß < 1. Further details on Leaky Integrator: https://www.music.mcgill.ca/~gary/307/week2/node4.html Not concurrency safe.
func NewLeakyIntegrator ¶
func NewLeakyIntegrator(beta, initialValue float64) (LeakyIntegrator, error)
NewLeakyIntegrator instantiates a new leaky integrator with loss factor `beta`, where `beta relates to window of N observations that we integrate over. For example, let `beta` ≡ 1/N and consider a constant input x. The integrator value will saturate at x·N. An integrator initialized at 0 reaches 2/3 of the saturation value after N samples. For numeric stability, we require `beta` to satisfy 0 < `beta` < 1.
func (*LeakyIntegrator) AddObservation ¶
func (e *LeakyIntegrator) AddObservation(v float64) float64
AddObservation adds the value `v` to the LeakyIntegrator. Returns the updated value.
func (*LeakyIntegrator) AddRepeatedObservation ¶
func (e *LeakyIntegrator) AddRepeatedObservation(v float64, k int) float64
AddRepeatedObservation adds k consecutive observations with the same value v. Returns the updated value.
func (*LeakyIntegrator) Value ¶
func (e *LeakyIntegrator) Value() float64
type ProposalTiming ¶
type ProposalTiming interface { hotstuff.ProposalDurationProvider // ObservationView returns the view of the observation that the controller // processed and generated this ProposalTiming instance in response. ObservationView() uint64 // ObservationTime returns the time, when the controller received the // leading to the generation of this ProposalTiming instance. ObservationTime() time.Time }
ProposalTiming encapsulates the output of the BlockTimeController. On the happy path, the controller observes a block and generates a specific ProposalTiming in response. For the happy path, the ProposalTiming describes when the child proposal should be broadcast. However, observations other than blocks might also be used to instantiate ProposalTiming objects, e.g. controller instantiation, a disabled controller, etc. The purpose of ProposalTiming is to convert the controller output to timing information that the EventHandler understands. By convention, ProposalTiming should be treated as immutable.
type TimedBlock ¶
type TimedBlock struct { Block *model.Block TimeObserved time.Time // timestamp when BlockTimeController received the block, per convention in UTC }
TimedBlock represents a block, with a timestamp recording when the BlockTimeController received the block
type TimingConfig ¶
type TimingConfig struct { // FallbackProposalDelay is the minimal block construction delay. When used, it behaves like the // old command line flag `block-rate-delay`. Specifically, the primary measures the duration from // starting to construct its proposal to the proposal being ready to be published. If this // duration is _less_ than FallbackProposalDelay, the primary delays broadcasting its proposal // by the remainder needed to reach `FallbackProposalDelay` // It is used: // - when Enabled is false // - when epoch fallback has been triggered FallbackProposalDelay *atomic.Duration // MaxViewDuration is a hard maximum on the total view time targeted by ProposalTiming. // If the BlockTimeController computes a larger desired ProposalTiming value // based on the observed error and tuning, this value will be used instead. MaxViewDuration *atomic.Duration // MinViewDuration is a hard maximum on the total view time targeted by ProposalTiming. // If the BlockTimeController computes a smaller desired ProposalTiming value // based on the observed error and tuning, this value will be used instead. MinViewDuration *atomic.Duration // Enabled defines whether responsive control of the GetProposalTiming is enabled. // When disabled, the FallbackProposalDelay is used. Enabled *atomic.Bool }
TimingConfig specifies the BlockTimeController's limits of authority.
func (TimingConfig) GetEnabled ¶
func (ctl TimingConfig) GetEnabled() bool
GetEnabled returns whether the controller is enabled.
func (TimingConfig) GetFallbackProposalDuration ¶
func (ctl TimingConfig) GetFallbackProposalDuration() time.Duration
GetFallbackProposalDuration returns the proposal duration used when Cruise Control is not active.
func (TimingConfig) GetMaxViewDuration ¶
func (ctl TimingConfig) GetMaxViewDuration() time.Duration
GetMaxViewDuration returns the max view duration returned by the controller.
func (TimingConfig) GetMinViewDuration ¶
func (ctl TimingConfig) GetMinViewDuration() time.Duration
GetMinViewDuration returns the min view duration returned by the controller.
func (TimingConfig) SetEnabled ¶
func (ctl TimingConfig) SetEnabled(enabled bool) error
SetEnabled sets whether the controller is enabled.
func (TimingConfig) SetFallbackProposalDuration ¶
func (ctl TimingConfig) SetFallbackProposalDuration(dur time.Duration) error
SetFallbackProposalDuration sets the proposal duration used when Cruise Control is not active.
func (TimingConfig) SetMaxViewDuration ¶
func (ctl TimingConfig) SetMaxViewDuration(dur time.Duration) error
SetMaxViewDuration sets the max view duration returned by the controller.
func (TimingConfig) SetMinViewDuration ¶
func (ctl TimingConfig) SetMinViewDuration(dur time.Duration) error
SetMinViewDuration sets the min view duration returned by the controller.