Merge pull request #4909 from carlaKC/mc-paramsapi

routing: allow runtime updates to mission control config
This commit is contained in:
Olaoluwa Osuntokun authored and GitHub committed 2021-02-10 18:51:53 -08:00
commit 7b0ea3c029
21 files changed
+1155 -254

No files matched your search

+6 -5
View File
@@ -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
View File
@@ -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,
+6 -5
View File
@@ -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 {
+1
View File
@@ -104,6 +104,7 @@ func (m *mockPaymentSessionSource) NewPaymentSessionEmpty() PaymentSession {
}
type mockMissionControl struct {
MissionControl
}
var _ MissionController = (*mockMissionControl)(nil)
+1 -1
View File
@@ -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 {
+52 -15
View File
@@ -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)
}
+5 -3
View File
@@ -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,
},
}
+6 -4
View File
@@ -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 {