Merge pull request #4909 from carlaKC/mc-paramsapi
routing: allow runtime updates to mission control config
This commit is contained in:
21 files changed
+1155
-254
No files matched your search
@@ -58,10 +58,11 @@ func newIntegratedRoutingContext(t *testing.T) *integratedRoutingContext {
|
||||
finalExpiry: 40,
|
||||
|
||||
mcCfg: MissionControlConfig{
|
||||
PenaltyHalfLife: 30 * time.Minute,
|
||||
AprioriHopProbability: 0.6,
|
||||
AprioriWeight: 0.5,
|
||||
SelfNode: source.pubkey,
|
||||
ProbabilityEstimatorCfg: ProbabilityEstimatorCfg{
|
||||
PenaltyHalfLife: 30 * time.Minute,
|
||||
AprioriHopProbability: 0.6,
|
||||
AprioriWeight: 0.5,
|
||||
},
|
||||
},
|
||||
|
||||
pathFindingCfg: PathFindingConfig{
|
||||
@@ -123,7 +124,7 @@ func (c *integratedRoutingContext) testPayment(maxParts uint32,
|
||||
|
||||
// Instantiate a new mission control with the current configuration
|
||||
// values.
|
||||
mc, err := NewMissionControl(db, &c.mcCfg)
|
||||
mc, err := NewMissionControl(db, c.source.pubkey, &c.mcCfg)
|
||||
if err != nil {
|
||||
c.t.Fatal(err)
|
||||
}
|
||||
|
||||
+99
-35
@@ -1,6 +1,8 @@
|
||||
package routing
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -57,6 +59,17 @@ const (
|
||||
DefaultMinFailureRelaxInterval = time.Minute
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidMcHistory is returned if we get a negative mission control
|
||||
// history count.
|
||||
ErrInvalidMcHistory = errors.New("mission control history must be " +
|
||||
">= 0")
|
||||
|
||||
// ErrInvalidFailureInterval is returned if we get an invalid failure
|
||||
// interval.
|
||||
ErrInvalidFailureInterval = errors.New("failure interval must be >= 0")
|
||||
)
|
||||
|
||||
// NodeResults contains previous results from a node to its peers.
|
||||
type NodeResults map[route.Vertex]TimedPairResult
|
||||
|
||||
@@ -78,7 +91,8 @@ type MissionControl struct {
|
||||
// external function to enable deterministic unit tests.
|
||||
now func() time.Time
|
||||
|
||||
cfg *MissionControlConfig
|
||||
// selfNode is our pubkey.
|
||||
selfNode route.Vertex
|
||||
|
||||
store *missionControlStore
|
||||
|
||||
@@ -97,34 +111,43 @@ type MissionControl struct {
|
||||
// MissionControlConfig defines parameters that control mission control
|
||||
// behaviour.
|
||||
type MissionControlConfig struct {
|
||||
// PenaltyHalfLife defines after how much time a penalized node or
|
||||
// channel is back at 50% probability.
|
||||
PenaltyHalfLife time.Duration
|
||||
|
||||
// AprioriHopProbability is the assumed success probability of a hop in
|
||||
// a route when no other information is available.
|
||||
AprioriHopProbability float64
|
||||
// ProbabilityEstimatorConfig is the config we will use for probability
|
||||
// calculations.
|
||||
ProbabilityEstimatorCfg
|
||||
|
||||
// MaxMcHistory defines the maximum number of payment results that are
|
||||
// held on disk.
|
||||
MaxMcHistory int
|
||||
|
||||
// AprioriWeight is a value in the range [0, 1] that defines to what
|
||||
// extent historical results should be extrapolated to untried
|
||||
// connections. Setting it to one will completely ignore historical
|
||||
// results and always assume the configured a priori probability for
|
||||
// untried connections. A value of zero will ignore the a priori
|
||||
// probability completely and only base the probability on historical
|
||||
// results, unless there are none available.
|
||||
AprioriWeight float64
|
||||
|
||||
// MinFailureRelaxInterval is the minimum time that must have passed
|
||||
// since the previously recorded failure before the failure amount may
|
||||
// be raised.
|
||||
MinFailureRelaxInterval time.Duration
|
||||
}
|
||||
|
||||
// SelfNode is our own pubkey.
|
||||
SelfNode route.Vertex
|
||||
func (c *MissionControlConfig) validate() error {
|
||||
if err := c.ProbabilityEstimatorCfg.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.MaxMcHistory < 0 {
|
||||
return ErrInvalidMcHistory
|
||||
}
|
||||
|
||||
if c.MinFailureRelaxInterval < 0 {
|
||||
return ErrInvalidFailureInterval
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns a string representation of a mission control config.
|
||||
func (c *MissionControlConfig) String() string {
|
||||
return fmt.Sprintf("Penalty Half Life: %v, Apriori Hop "+
|
||||
"Probablity: %v, Maximum History: %v, Apriori Weight: %v, "+
|
||||
"Minimum Failure Relax Interval: %v", c.PenaltyHalfLife,
|
||||
c.AprioriHopProbability, c.MaxMcHistory, c.AprioriWeight,
|
||||
c.MinFailureRelaxInterval)
|
||||
}
|
||||
|
||||
// TimedPairResult describes a timestamped pair result.
|
||||
@@ -177,13 +200,14 @@ type paymentResult struct {
|
||||
}
|
||||
|
||||
// NewMissionControl returns a new instance of missionControl.
|
||||
func NewMissionControl(db kvdb.Backend, cfg *MissionControlConfig) (
|
||||
*MissionControl, error) {
|
||||
func NewMissionControl(db kvdb.Backend, self route.Vertex,
|
||||
cfg *MissionControlConfig) (*MissionControl, error) {
|
||||
|
||||
log.Debugf("Instantiating mission control with config: "+
|
||||
"PenaltyHalfLife=%v, AprioriHopProbability=%v, "+
|
||||
"AprioriWeight=%v", cfg.PenaltyHalfLife,
|
||||
cfg.AprioriHopProbability, cfg.AprioriWeight)
|
||||
log.Debugf("Instantiating mission control with config: %v", cfg)
|
||||
|
||||
if err := cfg.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
store, err := newMissionControlStore(db, cfg.MaxMcHistory)
|
||||
if err != nil {
|
||||
@@ -191,16 +215,14 @@ func NewMissionControl(db kvdb.Backend, cfg *MissionControlConfig) (
|
||||
}
|
||||
|
||||
estimator := &probabilityEstimator{
|
||||
aprioriHopProbability: cfg.AprioriHopProbability,
|
||||
aprioriWeight: cfg.AprioriWeight,
|
||||
penaltyHalfLife: cfg.PenaltyHalfLife,
|
||||
prevSuccessProbability: prevSuccessProbability,
|
||||
ProbabilityEstimatorCfg: cfg.ProbabilityEstimatorCfg,
|
||||
prevSuccessProbability: prevSuccessProbability,
|
||||
}
|
||||
|
||||
mc := &MissionControl{
|
||||
state: newMissionControlState(cfg.MinFailureRelaxInterval),
|
||||
now: time.Now,
|
||||
cfg: cfg,
|
||||
selfNode: self,
|
||||
store: store,
|
||||
estimator: estimator,
|
||||
}
|
||||
@@ -216,6 +238,9 @@ func NewMissionControl(db kvdb.Backend, cfg *MissionControlConfig) (
|
||||
func (m *MissionControl) init() error {
|
||||
log.Debugf("Mission control state reconstruction started")
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
start := time.Now()
|
||||
|
||||
results, err := m.store.fetchAll()
|
||||
@@ -233,6 +258,43 @@ func (m *MissionControl) init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfig returns the config that mission control is currently configured
|
||||
// with. All fields are copied by value, so we do not need to worry about
|
||||
// mutation.
|
||||
func (m *MissionControl) GetConfig() *MissionControlConfig {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
return &MissionControlConfig{
|
||||
ProbabilityEstimatorCfg: m.estimator.ProbabilityEstimatorCfg,
|
||||
MaxMcHistory: m.store.maxRecords,
|
||||
MinFailureRelaxInterval: m.state.minFailureRelaxInterval,
|
||||
}
|
||||
}
|
||||
|
||||
// SetConfig validates the config provided and updates mission control's config
|
||||
// if it is valid.
|
||||
func (m *MissionControl) SetConfig(cfg *MissionControlConfig) error {
|
||||
if cfg == nil {
|
||||
return errors.New("nil mission control config")
|
||||
}
|
||||
|
||||
if err := cfg.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
log.Infof("Updating mission control cfg: %v", cfg)
|
||||
|
||||
m.store.maxRecords = cfg.MaxMcHistory
|
||||
m.state.minFailureRelaxInterval = cfg.MinFailureRelaxInterval
|
||||
m.estimator.ProbabilityEstimatorCfg = cfg.ProbabilityEstimatorCfg
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetHistory resets the history of MissionControl returning it to a state as
|
||||
// if no payment attempts have been made.
|
||||
func (m *MissionControl) ResetHistory() error {
|
||||
@@ -262,7 +324,7 @@ func (m *MissionControl) GetProbability(fromNode, toNode route.Vertex,
|
||||
results, _ := m.state.getLastPairResult(fromNode)
|
||||
|
||||
// Use a distinct probability estimation function for local channels.
|
||||
if fromNode == m.cfg.SelfNode {
|
||||
if fromNode == m.selfNode {
|
||||
return m.estimator.getLocalPairProbability(now, results, toNode)
|
||||
}
|
||||
|
||||
@@ -309,6 +371,9 @@ func (m *MissionControl) ReportPaymentFail(paymentID uint64, rt *route.Route,
|
||||
failureSourceIdx *int, failure lnwire.FailureMessage) (
|
||||
*channeldb.FailureReason, error) {
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
timestamp := m.now()
|
||||
|
||||
result := &paymentResult{
|
||||
@@ -329,6 +394,9 @@ func (m *MissionControl) ReportPaymentFail(paymentID uint64, rt *route.Route,
|
||||
func (m *MissionControl) ReportPaymentSuccess(paymentID uint64,
|
||||
rt *route.Route) error {
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
timestamp := m.now()
|
||||
|
||||
result := &paymentResult{
|
||||
@@ -371,10 +439,6 @@ func (m *MissionControl) applyPaymentResult(
|
||||
result.failure,
|
||||
)
|
||||
|
||||
// Update mission control state using the interpretation.
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
if i.policyFailure != nil {
|
||||
if m.state.requestSecondChance(
|
||||
result.timeReply,
|
||||
|
||||
@@ -78,12 +78,13 @@ func createMcTestContext(t *testing.T) *mcTestContext {
|
||||
// restartMc creates a new instances of mission control on the same database.
|
||||
func (ctx *mcTestContext) restartMc() {
|
||||
mc, err := NewMissionControl(
|
||||
ctx.db,
|
||||
ctx.db, mcTestSelf,
|
||||
&MissionControlConfig{
|
||||
PenaltyHalfLife: testPenaltyHalfLife,
|
||||
AprioriHopProbability: testAprioriHopProbability,
|
||||
AprioriWeight: testAprioriWeight,
|
||||
SelfNode: mcTestSelf,
|
||||
ProbabilityEstimatorCfg: ProbabilityEstimatorCfg{
|
||||
PenaltyHalfLife: testPenaltyHalfLife,
|
||||
AprioriHopProbability: testAprioriHopProbability,
|
||||
AprioriWeight: testAprioriWeight,
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -104,6 +104,7 @@ func (m *mockPaymentSessionSource) NewPaymentSessionEmpty() PaymentSession {
|
||||
}
|
||||
|
||||
type mockMissionControl struct {
|
||||
MissionControl
|
||||
}
|
||||
|
||||
var _ MissionController = (*mockMissionControl)(nil)
|
||||
|
||||
@@ -33,7 +33,7 @@ func TestRequestRoute(t *testing.T) {
|
||||
func() (routingGraph, func(), error) {
|
||||
return &sessionGraph{}, func() {}, nil
|
||||
},
|
||||
&MissionControl{cfg: &MissionControlConfig{}},
|
||||
&MissionControl{},
|
||||
PathFindingConfig{},
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package routing
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
@@ -8,25 +9,61 @@ import (
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
)
|
||||
|
||||
// probabilityEstimator returns node and pair probabilities based on historical
|
||||
// payment results.
|
||||
type probabilityEstimator struct {
|
||||
// penaltyHalfLife defines after how much time a penalized node or
|
||||
var (
|
||||
// ErrInvalidHalflife is returned when we get an invalid half life.
|
||||
ErrInvalidHalflife = errors.New("penalty half life must be >= 0")
|
||||
|
||||
// ErrInvalidHopProbability is returned when we get an invalid hop
|
||||
// probability.
|
||||
ErrInvalidHopProbability = errors.New("hop probability must be in [0;1]")
|
||||
|
||||
// ErrInvalidAprioriWeight is returned when we get an apriori weight
|
||||
// that is out of range.
|
||||
ErrInvalidAprioriWeight = errors.New("apriori weight must be in [0;1]")
|
||||
)
|
||||
|
||||
// ProbabilityEstimatorCfg contains configuration for our probability estimator.
|
||||
type ProbabilityEstimatorCfg struct {
|
||||
// PenaltyHalfLife defines after how much time a penalized node or
|
||||
// channel is back at 50% probability.
|
||||
penaltyHalfLife time.Duration
|
||||
PenaltyHalfLife time.Duration
|
||||
|
||||
// aprioriHopProbability is the assumed success probability of a hop in
|
||||
// AprioriHopProbability is the assumed success probability of a hop in
|
||||
// a route when no other information is available.
|
||||
aprioriHopProbability float64
|
||||
AprioriHopProbability float64
|
||||
|
||||
// aprioriWeight is a value in the range [0, 1] that defines to what
|
||||
// AprioriWeight is a value in the range [0, 1] that defines to what
|
||||
// extent historical results should be extrapolated to untried
|
||||
// connections. Setting it to one will completely ignore historical
|
||||
// results and always assume the configured a priori probability for
|
||||
// untried connections. A value of zero will ignore the a priori
|
||||
// probability completely and only base the probability on historical
|
||||
// results, unless there are none available.
|
||||
aprioriWeight float64
|
||||
AprioriWeight float64
|
||||
}
|
||||
|
||||
func (p ProbabilityEstimatorCfg) validate() error {
|
||||
if p.PenaltyHalfLife < 0 {
|
||||
return ErrInvalidHalflife
|
||||
}
|
||||
|
||||
if p.AprioriHopProbability < 0 || p.AprioriHopProbability > 1 {
|
||||
return ErrInvalidHopProbability
|
||||
}
|
||||
|
||||
if p.AprioriWeight < 0 || p.AprioriWeight > 1 {
|
||||
return ErrInvalidAprioriWeight
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// probabilityEstimator returns node and pair probabilities based on historical
|
||||
// payment results.
|
||||
type probabilityEstimator struct {
|
||||
// ProbabilityEstimatorCfg contains configuration options for our
|
||||
// estimator.
|
||||
ProbabilityEstimatorCfg
|
||||
|
||||
// prevSuccessProbability is the assumed probability for node pairs that
|
||||
// successfully relayed the previous attempt.
|
||||
@@ -41,14 +78,14 @@ func (p *probabilityEstimator) getNodeProbability(now time.Time,
|
||||
|
||||
// If the channel history is not to be taken into account, we can return
|
||||
// early here with the configured a priori probability.
|
||||
if p.aprioriWeight == 1 {
|
||||
return p.aprioriHopProbability
|
||||
if p.AprioriWeight == 1 {
|
||||
return p.AprioriHopProbability
|
||||
}
|
||||
|
||||
// If there is no channel history, our best estimate is still the a
|
||||
// priori probability.
|
||||
if len(results) == 0 {
|
||||
return p.aprioriHopProbability
|
||||
return p.AprioriHopProbability
|
||||
}
|
||||
|
||||
// The value of the apriori weight is in the range [0, 1]. Convert it to
|
||||
@@ -58,7 +95,7 @@ func (p *probabilityEstimator) getNodeProbability(now time.Time,
|
||||
// the weighted average calculation below. When the apriori weight
|
||||
// approaches 1, the apriori factor goes to infinity. It will heavily
|
||||
// outweigh any observations that have been collected.
|
||||
aprioriFactor := 1/(1-p.aprioriWeight) - 1
|
||||
aprioriFactor := 1/(1-p.AprioriWeight) - 1
|
||||
|
||||
// Calculate a weighted average consisting of the apriori probability
|
||||
// and historical observations. This is the part that incentivizes nodes
|
||||
@@ -76,7 +113,7 @@ func (p *probabilityEstimator) getNodeProbability(now time.Time,
|
||||
// effectively prunes all channels of the node forever. This is the most
|
||||
// aggressive way in which we can penalize nodes and unlikely to yield
|
||||
// good results in a real network.
|
||||
probabilitiesTotal := p.aprioriHopProbability * aprioriFactor
|
||||
probabilitiesTotal := p.AprioriHopProbability * aprioriFactor
|
||||
totalWeight := aprioriFactor
|
||||
|
||||
for _, result := range results {
|
||||
@@ -106,7 +143,7 @@ func (p *probabilityEstimator) getNodeProbability(now time.Time,
|
||||
// the result is fresh and asymptotically approaches zero over time. The rate at
|
||||
// which this happens is controlled by the penaltyHalfLife parameter.
|
||||
func (p *probabilityEstimator) getWeight(age time.Duration) float64 {
|
||||
exp := -age.Hours() / p.penaltyHalfLife.Hours()
|
||||
exp := -age.Hours() / p.PenaltyHalfLife.Hours()
|
||||
return math.Pow(2, exp)
|
||||
}
|
||||
|
||||
|
||||
@@ -40,9 +40,11 @@ func newEstimatorTestContext(t *testing.T) *estimatorTestContext {
|
||||
return &estimatorTestContext{
|
||||
t: t,
|
||||
estimator: &probabilityEstimator{
|
||||
aprioriHopProbability: aprioriHopProb,
|
||||
aprioriWeight: aprioriWeight,
|
||||
penaltyHalfLife: time.Hour,
|
||||
ProbabilityEstimatorCfg: ProbabilityEstimatorCfg{
|
||||
AprioriHopProbability: aprioriHopProb,
|
||||
AprioriWeight: aprioriWeight,
|
||||
PenaltyHalfLife: time.Hour,
|
||||
},
|
||||
prevSuccessProbability: aprioriPrevSucProb,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -85,13 +85,15 @@ func createTestCtxFromGraphInstance(startingHeight uint32, graphInstance *testGr
|
||||
}
|
||||
|
||||
mcConfig := &MissionControlConfig{
|
||||
PenaltyHalfLife: time.Hour,
|
||||
AprioriHopProbability: 0.9,
|
||||
AprioriWeight: 0.5,
|
||||
ProbabilityEstimatorCfg: ProbabilityEstimatorCfg{
|
||||
PenaltyHalfLife: time.Hour,
|
||||
AprioriHopProbability: 0.9,
|
||||
AprioriWeight: 0.5,
|
||||
},
|
||||
}
|
||||
|
||||
mc, err := NewMissionControl(
|
||||
graphInstance.graph.Database(),
|
||||
graphInstance.graph.Database(), route.Vertex{},
|
||||
mcConfig,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
Reference in new issue
Block a user