mirror of
https://github.com/lightninglabs/faraday.git
synced 2026-08-20 13:28:33 +02:00
Merge pull request #7 from lightninglabs/rpc-addserver
trmrpc: Add rpcserver and cli tool
This commit is contained in:
commit
76cb7cdade
21 changed files with 1322 additions and 155 deletions
6
Makefile
6
Makefile
|
|
@ -58,6 +58,7 @@ $(GOACC_BIN):
|
|||
build:
|
||||
@$(call print, "Building terminator.")
|
||||
$(GOBUILD) $(PKG)/cmd/terminator
|
||||
$(GOBUILD) $(PKG)/cmd/trmcli
|
||||
|
||||
install:
|
||||
@$(call print, "Installing terminator.")
|
||||
|
|
@ -87,6 +88,10 @@ goveralls: $(GOVERALLS_BIN)
|
|||
@$(call print, "Sending coverage report.")
|
||||
$(GOVERALLS_BIN) -coverprofile=coverage.txt -service=travis-ci
|
||||
|
||||
rpc:
|
||||
@$(call print, "Compiling protos.")
|
||||
cd ./trmrpc; ./gen_protos.sh
|
||||
|
||||
travis-race: lint unit-race
|
||||
|
||||
travis-cover: lint unit-cover goveralls
|
||||
|
|
@ -121,4 +126,5 @@ list:
|
|||
clean:
|
||||
@$(call print, "Cleaning source.$(NC)")
|
||||
$(RM) ./terminator
|
||||
$(RM) ./trmcli
|
||||
$(RM) coverage.txt
|
||||
|
|
|
|||
79
cmd/trmcli/close_recommendations.go
Normal file
79
cmd/trmcli/close_recommendations.go
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/lightninglabs/terminator/trmrpc"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultMinMonitored = time.Hour * 24 * 7 * 4 // four weeks in hours
|
||||
defaultOutlierMultiplier = 3
|
||||
)
|
||||
|
||||
var closeRecommendationCommand = cli.Command{
|
||||
Name: "closerecs",
|
||||
Category: "channel",
|
||||
Usage: "Get close recommendations for currently open channels.",
|
||||
Flags: []cli.Flag{
|
||||
cli.Int64Flag{
|
||||
Name: "min_monitored",
|
||||
Usage: "amount of time in seconds a channel should " +
|
||||
"be monitored for to be eligible for close",
|
||||
Value: int64(defaultMinMonitored.Seconds()),
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "outlier_mult",
|
||||
Usage: "(optional with outlier strategy) Number of " +
|
||||
"inter quartile ranges a channel should be " +
|
||||
"from quartiles to be considered an outlier. " +
|
||||
"Recommended values are 1.5 for aggressive " +
|
||||
"recommendations and 3 for conservative ones.",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "uptime_threshold",
|
||||
Usage: "(optional) Uptime percentage threshold " +
|
||||
"underneath which a channel will be recommended " +
|
||||
"for close.",
|
||||
},
|
||||
},
|
||||
Action: queryCloseRecommendations,
|
||||
}
|
||||
|
||||
func queryCloseRecommendations(ctx *cli.Context) error {
|
||||
client, cleanup := getClient(ctx)
|
||||
defer cleanup()
|
||||
|
||||
// Set monitored value from cli and default outlier multiplier. The
|
||||
// outlier multiplier will be overwritten if the user provided it.
|
||||
req := &trmrpc.CloseRecommendationsRequest{
|
||||
MinimumMonitored: ctx.Int64("min_monitored"),
|
||||
OutlierMultiplier: float32(defaultOutlierMultiplier),
|
||||
}
|
||||
|
||||
// If an a custom outlier multiple was set, use it.
|
||||
if ctx.IsSet("outlier_mult") {
|
||||
req.OutlierMultiplier = float32(ctx.Float64("outlier_mult"))
|
||||
}
|
||||
|
||||
// If an uptime threshold was set, use it.
|
||||
if ctx.IsSet("uptime_threshold") {
|
||||
uptimeThreshold := float32(ctx.Float64("uptime_threshold"))
|
||||
req.Threshold =
|
||||
&trmrpc.CloseRecommendationsRequest_UptimeThreshold{
|
||||
UptimeThreshold: uptimeThreshold,
|
||||
}
|
||||
}
|
||||
|
||||
rpcCtx := context.Background()
|
||||
recs, err := client.CloseRecommendations(rpcCtx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(recs)
|
||||
|
||||
return nil
|
||||
}
|
||||
32
cmd/trmcli/main.go
Normal file
32
cmd/trmcli/main.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultRPCPort = "8419"
|
||||
defaultRPCHostPort = "localhost:" + defaultRPCPort
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "trmcli"
|
||||
app.Usage = "command line tool for terminator"
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "rpcserver",
|
||||
Value: defaultRPCHostPort,
|
||||
Usage: "host:port of terminator",
|
||||
},
|
||||
}
|
||||
app.Commands = []cli.Command{
|
||||
closeRecommendationCommand,
|
||||
}
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
}
|
||||
97
cmd/trmcli/utils.go
Normal file
97
cmd/trmcli/utils.go
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/lightninglabs/protobuf-hex-display/jsonpb"
|
||||
"github.com/lightninglabs/protobuf-hex-display/proto"
|
||||
"github.com/lightninglabs/terminator/trmrpc"
|
||||
"github.com/lightningnetwork/lnd/lncfg"
|
||||
"github.com/urfave/cli"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var (
|
||||
// maxMsgRecvSize is the largest message our client will receive. We
|
||||
// set this to 200MiB atm.
|
||||
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200)
|
||||
)
|
||||
|
||||
// fatal logs and error and exits.
|
||||
func fatal(err error) {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "[trmcli] %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// printRespJSON prints a proto message as json.
|
||||
func printRespJSON(resp proto.Message) {
|
||||
jsonMarshaler := &jsonpb.Marshaler{
|
||||
EmitDefaults: true,
|
||||
Indent: " ",
|
||||
}
|
||||
|
||||
jsonStr, err := jsonMarshaler.MarshalToString(resp)
|
||||
if err != nil {
|
||||
fmt.Println("unable to decode response: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(jsonStr)
|
||||
}
|
||||
|
||||
// getClient returns a terminator client.
|
||||
func getClient(ctx *cli.Context) (trmrpc.TerminatorServerClient, func()) {
|
||||
conn := getClientConn(ctx)
|
||||
|
||||
cleanUp := func() {
|
||||
if err := conn.Close(); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
return trmrpc.NewTerminatorServerClient(conn), cleanUp
|
||||
}
|
||||
|
||||
// getClientConn gets a client connection to the address provided by the
|
||||
// rpcserver flag.
|
||||
func getClientConn(ctx *cli.Context) *grpc.ClientConn {
|
||||
// We need to use a custom dialer so we can also connect to unix sockets
|
||||
// and not just TCP addresses.
|
||||
genericDialer := clientAddressDialer(defaultRPCPort)
|
||||
|
||||
opts := []grpc.DialOption{
|
||||
grpc.WithContextDialer(genericDialer),
|
||||
grpc.WithDefaultCallOptions(maxMsgRecvSize),
|
||||
// TODO(carla): add tls and remove this option.
|
||||
grpc.WithInsecure(),
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(ctx.GlobalString("rpcserver"), opts...)
|
||||
if err != nil {
|
||||
fatal(fmt.Errorf("unable to connect to RPC server: %v", err))
|
||||
}
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
// ClientAddressDialer parsed client address and returns a dialer.
|
||||
func clientAddressDialer(defaultPort string) func(context.Context,
|
||||
string) (net.Conn, error) {
|
||||
|
||||
return func(ctx context.Context, addr string) (net.Conn, error) {
|
||||
parsedAddr, err := lncfg.ParseAddressString(
|
||||
addr, defaultPort, net.ResolveTCPAddr,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d := net.Dialer{}
|
||||
return d.DialContext(
|
||||
ctx, parsedAddr.Network(), parsedAddr.String(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ const (
|
|||
defaultNetwork = "mainnet"
|
||||
defaultMinimumMonitor = time.Hour * 24 * 7 * 4 // four weeks in hours
|
||||
defaultDebugLevel = "info"
|
||||
defaultRPCListen = "localhost:8419"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
|
|
@ -48,6 +49,9 @@ type config struct {
|
|||
// DebugLevel is a string defining the log level for the service either
|
||||
// for all subsystems the same or individual level by subsystem.
|
||||
DebugLevel string `long:"debuglevel" description:"Debug level for termaintor and its subsystems."`
|
||||
|
||||
// RPCListen is the listen address for the terminator rpc server.
|
||||
RPCListen string `long:"rpclisten" description:"Address to listen on for gRPC clients"`
|
||||
}
|
||||
|
||||
// loadConfig starts with a skeleton default config, and reads in user provided
|
||||
|
|
@ -62,6 +66,7 @@ func loadConfig() (*config, error) {
|
|||
MacaroonFile: defaultMacaroon,
|
||||
MinimumMonitored: defaultMinimumMonitor,
|
||||
DebugLevel: defaultDebugLevel,
|
||||
RPCListen: defaultRPCListen,
|
||||
}
|
||||
|
||||
// Parse command line options to obtain user specified values.
|
||||
|
|
|
|||
|
|
@ -7,30 +7,15 @@ import (
|
|||
"sort"
|
||||
)
|
||||
|
||||
const (
|
||||
// weakOutlierMultiplier is the multiplier that we apply to the
|
||||
// inter-quartile range to calculate weak outliers. Using this value is
|
||||
// less cautious than using the strong outlier multiplier because it
|
||||
// will flag values that are closer to the lower/upper quartiles as
|
||||
// outliers.
|
||||
weakOutlierMultiplier = 1.5
|
||||
|
||||
// strongOutlierMultiplier is the multiplier that we apply to the
|
||||
// inter-quartile range to calculate strong outliers. Using this value is
|
||||
// more cautious than using the weak outlier multiplier because it will only
|
||||
// flag extreme outliers.
|
||||
strongOutlierMultiplier = 3
|
||||
)
|
||||
|
||||
var (
|
||||
// errNoValues is returned when an attempt is made to calculate the median of
|
||||
// a zero length array.
|
||||
errNoValues = errors.New("can't calculate median for zero length " +
|
||||
"array")
|
||||
|
||||
// ErrTooFewValues is returned when there are too few values provided to
|
||||
// errTooFewValues is returned when there are too few values provided to
|
||||
// calculate quartiles.
|
||||
ErrTooFewValues = errors.New("can't calculate quartiles for fewer than 3 " +
|
||||
errTooFewValues = errors.New("can't calculate quartiles for fewer than 3 " +
|
||||
"elements")
|
||||
)
|
||||
|
||||
|
|
@ -80,7 +65,7 @@ func (d Dataset) rawValues() []float64 {
|
|||
func (d Dataset) quartiles() (float64, float64, error) {
|
||||
valueCount := len(d)
|
||||
if valueCount < 3 {
|
||||
return 0, 0, ErrTooFewValues
|
||||
return 0, 0, errTooFewValues
|
||||
}
|
||||
|
||||
// Get the cutoff points for calculating the lower and upper quartiles.
|
||||
|
|
@ -113,6 +98,11 @@ func (d Dataset) quartiles() (float64, float64, error) {
|
|||
return lowerQuartile, upperQuartile, nil
|
||||
}
|
||||
|
||||
// Value returns the value that a label is associated with in a set.
|
||||
func (d Dataset) Value(label string) float64 {
|
||||
return d[label]
|
||||
}
|
||||
|
||||
// OutlierResult returns the results of an outlier check.
|
||||
type OutlierResult struct {
|
||||
// UpperOutlier is true if the value is an upper outlier in the dataset.
|
||||
|
|
@ -126,21 +116,16 @@ type OutlierResult struct {
|
|||
// upper or lower outlier (or not an outlier) for the dataset.
|
||||
// If a value is an upper or lower outlier, the result is recorded in the
|
||||
// corresponding bool in an outlier result.
|
||||
func (d Dataset) isIQROutlier(value float64, lowerQuartile,
|
||||
upperQuartile float64, strong bool) *OutlierResult {
|
||||
func (d Dataset) isIQROutlier(value, lowerQuartile, upperQuartile,
|
||||
multiplier float64) *OutlierResult {
|
||||
|
||||
interquartileRange := upperQuartile - lowerQuartile
|
||||
|
||||
// quartileDistance is the distance from the upper/lower quartile a value
|
||||
// must be to be considered an outlier. A larger quartile distance more
|
||||
// strictly classifies outliers, because they are required to be further
|
||||
// from the upper/lower quartile. Based on whether we want to find strong or
|
||||
// weak outliers, the inter-quartile range (which is the base unit for this
|
||||
// distance) is multiplier by a strong or weak multiplier.
|
||||
quartileDistance := interquartileRange * weakOutlierMultiplier
|
||||
if strong {
|
||||
quartileDistance = interquartileRange * strongOutlierMultiplier
|
||||
}
|
||||
// from the upper/lower quartile.
|
||||
quartileDistance := interquartileRange * multiplier
|
||||
|
||||
return &OutlierResult{
|
||||
// A value is considered to be a upper outlier if it lies above the
|
||||
|
|
@ -156,44 +141,93 @@ func (d Dataset) isIQROutlier(value float64, lowerQuartile,
|
|||
|
||||
// GetOutliers returns a map of the labels in the dataset to outlier results
|
||||
// which indicate whether the associated value is an upper or lower inter-
|
||||
// quartile outlier.
|
||||
// quartile outlier. If there are too few values to calculate inter-quartile
|
||||
// outliers, it will return false values for all data points.
|
||||
//
|
||||
// Strong is set to adjust whether we check for a strong or weak outlier. Strong
|
||||
// outliers are 3 inter-quartile ranges below/above the lower/upper quartile and
|
||||
// weak outliers are 1.5 inter-quartile ranges below/above the lower/upper
|
||||
// quartile.
|
||||
// An outlier multiplier is provided to determine how strictly we classify
|
||||
// outliers; lower values will identify more outliers, thus being more strict,
|
||||
// and higher values will identify fewer outliers, thus being less strict.
|
||||
// Multipliers less than 1.5 are considered to provide "weak outliers", because
|
||||
// the values are still relatively close the the rest of the dataset.
|
||||
// Multipliers more than 3 are considered to provide "strong outliers" because
|
||||
// they identify values that are far from the rest of the dataset.
|
||||
//
|
||||
// The effect of this value is illustrated in the example below:
|
||||
// Given some random set of data, with lower quartile = 5 and upper quartile
|
||||
// = 6, the inter-quartile range is 1.
|
||||
//
|
||||
// LQ UQ
|
||||
// [ 1 2 5 5 5 6 6 6 8 11 ]
|
||||
//
|
||||
// For strong outliers, we multiply the inter-quartile range by 3 then check
|
||||
// whether a value is below the lower quartile or above the upper quartile by
|
||||
// that amount to determine whether it is a lower or upper outlier.
|
||||
// Strong lower outlier bound: 5 - (1 * 3) = 2
|
||||
// For larger values, eg multiplier=3, we will detect fewer outliers:
|
||||
// Lower outlier bound: 5 - (1 * 3) = 2
|
||||
// -> 1 is a strong lower outlier
|
||||
// Strong upper outlier bound: 6 + (1 * 3) = 9
|
||||
// Upper outlier bound: 6 + (1 * 3) = 9
|
||||
// -> 11 is a strong upper outlier
|
||||
//
|
||||
// For weak outliers, we perform the same check, but we multiply the inter-
|
||||
// quartile range by 1.5 rather than 3.
|
||||
// For smaller values, eg multiplier=1.5, we detect more outliers:
|
||||
// Weak lower outlier bound: 5 - (1 * 1.5) = 3.5
|
||||
// -> 1 and 2 are weak lower outliers
|
||||
// Weak upper outlier bound: 6 + (1 *1.5) = 7.5
|
||||
// -> 8 and 11 are weak upper outliers
|
||||
func (d Dataset) GetOutliers(strong bool) (map[string]*OutlierResult, error) {
|
||||
func (d Dataset) GetOutliers(outlierMultiplier float64) (
|
||||
map[string]*OutlierResult, error) {
|
||||
|
||||
outliers := make(map[string]*OutlierResult, len(d))
|
||||
|
||||
lower, upper, err := d.quartiles()
|
||||
// If we could not calculate quartiles because there are too few values,
|
||||
// we cannot calculate outliers so we return a map with all false
|
||||
// outlier results.
|
||||
if err == errTooFewValues {
|
||||
log.Info(err)
|
||||
|
||||
// Return a map with no outliers.
|
||||
for label := range d {
|
||||
outliers[label] = &OutlierResult{
|
||||
UpperOutlier: false,
|
||||
LowerOutlier: false,
|
||||
}
|
||||
}
|
||||
return outliers, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
outliers := make(map[string]*OutlierResult, len(d))
|
||||
|
||||
// If we could could calculate quartiles for the dataset, we get
|
||||
// outliers and populate a result map.
|
||||
for label, value := range d {
|
||||
outliers[label] = d.isIQROutlier(value, lower, upper, strong)
|
||||
outliers[label] = d.isIQROutlier(
|
||||
value, lower, upper, outlierMultiplier,
|
||||
)
|
||||
}
|
||||
|
||||
return outliers, nil
|
||||
}
|
||||
|
||||
// GetThreshold returns the set of values in a dataset <= or > a given
|
||||
// threshold. The below bool is used to toggle whether we identify values
|
||||
// above or below the threshold.
|
||||
func (d Dataset) GetThreshold(thresholdValue float64, below bool) map[string]bool {
|
||||
threshold := make(map[string]bool, len(d.rawValues()))
|
||||
|
||||
for label, value := range d {
|
||||
// If we are looking for values below the threshold, check the
|
||||
// current value then move on to the next one.
|
||||
if below {
|
||||
// If the value is below or equal to the threshold, we
|
||||
// set the label's value in the map to true. Otherwise
|
||||
// we set it to false.
|
||||
threshold[label] = value <= thresholdValue
|
||||
continue
|
||||
}
|
||||
|
||||
// We are looking for values above the threshold. Set the
|
||||
// label's value to true if the value is greater than the
|
||||
// threshold.
|
||||
threshold[label] = value > thresholdValue
|
||||
}
|
||||
|
||||
return threshold
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package dataset
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
|
@ -71,7 +72,7 @@ func TestQuartiles(t *testing.T) {
|
|||
{
|
||||
name: "no elements",
|
||||
values: []float64{},
|
||||
expectedErr: ErrTooFewValues,
|
||||
expectedErr: errTooFewValues,
|
||||
},
|
||||
{
|
||||
name: "three elements",
|
||||
|
|
@ -146,15 +147,21 @@ func TestIsOutlier(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
values map[string]float64
|
||||
expectedError error
|
||||
expectedOutliers map[string]*OutlierResult
|
||||
strong bool
|
||||
multiplier float64
|
||||
}{
|
||||
{
|
||||
name: "too few values",
|
||||
expectedError: ErrTooFewValues,
|
||||
values: make(map[string]float64),
|
||||
strong: true,
|
||||
name: "too few values - all false",
|
||||
values: map[string]float64{
|
||||
"a": 1,
|
||||
},
|
||||
multiplier: 3,
|
||||
expectedOutliers: map[string]*OutlierResult{
|
||||
"a": {
|
||||
UpperOutlier: false,
|
||||
LowerOutlier: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "lower outlier",
|
||||
|
|
@ -166,7 +173,7 @@ func TestIsOutlier(t *testing.T) {
|
|||
"e": 8,
|
||||
"f": 10,
|
||||
},
|
||||
strong: true,
|
||||
multiplier: 3,
|
||||
expectedOutliers: map[string]*OutlierResult{
|
||||
"a": {
|
||||
UpperOutlier: false,
|
||||
|
|
@ -189,7 +196,7 @@ func TestIsOutlier(t *testing.T) {
|
|||
"e": 3,
|
||||
"f": 10,
|
||||
},
|
||||
strong: true,
|
||||
multiplier: 3,
|
||||
expectedOutliers: map[string]*OutlierResult{
|
||||
"a": noOutlier,
|
||||
"b": noOutlier,
|
||||
|
|
@ -212,14 +219,9 @@ func TestIsOutlier(t *testing.T) {
|
|||
|
||||
dataset := New(test.values)
|
||||
|
||||
outliers, err := dataset.GetOutliers(test.strong)
|
||||
if err != test.expectedError {
|
||||
t.Fatalf("expected: %v, got: %v", test.expectedError, err)
|
||||
}
|
||||
|
||||
// If the error is non-nil, there is no need for further checks.
|
||||
outliers, err := dataset.GetOutliers(test.multiplier)
|
||||
if err != nil {
|
||||
return
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
for label, outlier := range outliers {
|
||||
|
|
@ -242,3 +244,69 @@ func TestIsOutlier(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetThreshold tests getting thresholds for a dataset.
|
||||
func TestGetThreshold(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dataset Dataset
|
||||
threshold float64
|
||||
below bool
|
||||
expectedValues map[string]bool
|
||||
}{
|
||||
{
|
||||
name: "no values",
|
||||
dataset: make(map[string]float64),
|
||||
threshold: 1,
|
||||
below: false,
|
||||
expectedValues: make(map[string]bool),
|
||||
},
|
||||
{
|
||||
name: "below and equal",
|
||||
dataset: map[string]float64{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"c": 3,
|
||||
},
|
||||
threshold: 2,
|
||||
below: true,
|
||||
expectedValues: map[string]bool{
|
||||
"a": true,
|
||||
"b": true,
|
||||
"c": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "above and equal",
|
||||
dataset: map[string]float64{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"c": 3,
|
||||
},
|
||||
threshold: 2,
|
||||
below: false,
|
||||
expectedValues: map[string]bool{
|
||||
"a": false,
|
||||
"b": false,
|
||||
"c": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
values := test.dataset.GetThreshold(
|
||||
test.threshold, test.below,
|
||||
)
|
||||
|
||||
if !reflect.DeepEqual(test.expectedValues, values) {
|
||||
t.Fatalf("expected: %v, got: %v",
|
||||
test.expectedValues, values)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
26
dataset/log.go
Normal file
26
dataset/log.go
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
package dataset
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btclog"
|
||||
"github.com/lightningnetwork/lnd/build"
|
||||
)
|
||||
|
||||
// Subsystem defines the logging code for this subsystem.
|
||||
const Subsystem = "DSET"
|
||||
|
||||
// log is a logger that is initialized with no output filters. This
|
||||
// means the package will not perform any logging by default until the
|
||||
// caller requests it.
|
||||
var log btclog.Logger
|
||||
|
||||
// The default amount of logging is none.
|
||||
func init() {
|
||||
UseLogger(build.NewSubLogger(Subsystem, nil))
|
||||
}
|
||||
|
||||
// UseLogger uses a specified Logger to output package logging info.
|
||||
// This should be used in preference to SetLogWriter if the caller is also
|
||||
// using btclog.
|
||||
func UseLogger(logger btclog.Logger) {
|
||||
log = logger
|
||||
}
|
||||
5
go.mod
5
go.mod
|
|
@ -3,9 +3,14 @@ module github.com/lightninglabs/terminator
|
|||
require (
|
||||
github.com/btcsuite/btcd v0.20.1-beta
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
|
||||
github.com/golang/protobuf v1.3.3
|
||||
github.com/jessevdk/go-flags v1.4.0
|
||||
github.com/lightninglabs/loop v0.2.4-alpha
|
||||
github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce75d
|
||||
github.com/lightningnetwork/lnd v0.8.0-beta-rc3.0.20191025122959-1a0ab538d53c
|
||||
github.com/urfave/cli v1.20.0
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55
|
||||
google.golang.org/grpc v1.27.0
|
||||
)
|
||||
|
||||
go 1.13
|
||||
|
|
|
|||
12
go.sum
12
go.sum
|
|
@ -51,6 +51,7 @@ github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg
|
|||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc=
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY=
|
||||
|
|
@ -59,6 +60,8 @@ github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2
|
|||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k=
|
||||
github.com/frankban/quicktest v1.2.2 h1:xfmOhhoH5fGPgbEAlhLpJH9p0z/0Qizio9osmvn9IUY=
|
||||
|
|
@ -72,6 +75,7 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
|
|||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
|
|
@ -81,6 +85,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
|||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
||||
|
|
@ -132,6 +138,8 @@ github.com/lightninglabs/loop v0.2.4-alpha/go.mod h1:n/8uTYPcWrU12xAQmUvjvfxKTFW
|
|||
github.com/lightninglabs/neutrino v0.0.0-20190906012717-f087198de655/go.mod h1:awTrhbCWjWNH4yVwZ4IE7nZbvpQ27e7OyD+jao7wRxA=
|
||||
github.com/lightninglabs/neutrino v0.10.0 h1:yWVy2cOCCXbKFdpYCE9vD1fWRJDd9FtGXhUws4l9RkU=
|
||||
github.com/lightninglabs/neutrino v0.10.0/go.mod h1:C3KhCMk1Mcx3j8v0qRVWM1Ow6rIJSvSPnUAq00ZNAfk=
|
||||
github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce75d h1:QWD/5MPnaZfUVP7P8wLa4M8Td2DI7XXHXt2vhVtUgGI=
|
||||
github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce75d/go.mod h1:KDb67YMzoh4eudnzClmvs2FbiLG9vxISmLApUkCa4uI=
|
||||
github.com/lightningnetwork/lightning-onion v0.0.0-20190909101754-850081b08b6a h1:GoWPN4i4jTKRxhVNh9a2vvBBO1Y2seiJB+SopUYoKyo=
|
||||
github.com/lightningnetwork/lightning-onion v0.0.0-20190909101754-850081b08b6a/go.mod h1:rigfi6Af/KqsF7Za0hOgcyq2PNH4AN70AaMRxcJkff4=
|
||||
github.com/lightningnetwork/lnd v0.7.1-beta-rc2.0.20190914085956-35027e52fc22/go.mod h1:VaY0b5o38keUN3Ga6GVb/Mgta4B/CcCXwNvPAvhbv/A=
|
||||
|
|
@ -161,6 +169,7 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
|
|||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
|
|
@ -175,6 +184,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY=
|
||||
github.com/urfave/cli v1.18.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
|
|
@ -240,6 +250,8 @@ google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3
|
|||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0 h1:AzbTB6ux+okLTzP8Ru1Xs41C303zdcfEht7MQnYJt5A=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
|
|
|
|||
4
log.go
4
log.go
|
|
@ -2,7 +2,9 @@ package terminator
|
|||
|
||||
import (
|
||||
"github.com/btcsuite/btclog"
|
||||
"github.com/lightninglabs/terminator/dataset"
|
||||
"github.com/lightninglabs/terminator/recommend"
|
||||
"github.com/lightninglabs/terminator/trmrpc"
|
||||
"github.com/lightningnetwork/lnd/build"
|
||||
)
|
||||
|
||||
|
|
@ -22,6 +24,8 @@ var (
|
|||
func init() {
|
||||
setSubLogger(Subsystem, log, nil)
|
||||
addSubLogger(recommend.Subsystem, recommend.UseLogger)
|
||||
addSubLogger(dataset.Subsystem, dataset.UseLogger)
|
||||
addSubLogger(trmrpc.Subsystem, trmrpc.UseLogger)
|
||||
}
|
||||
|
||||
// UseLogger uses a specified Logger to output package logging info.
|
||||
|
|
|
|||
|
|
@ -18,10 +18,17 @@ import (
|
|||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
)
|
||||
|
||||
// errZeroMinMonitored is returned when the minimum ages provided by the config
|
||||
// is zero.
|
||||
var errZeroMinMonitored = errors.New("must provide a non-zero minimum " +
|
||||
"monitor time for channel exclusion")
|
||||
var (
|
||||
// errZeroMinMonitored is returned when the minimum age provided
|
||||
// by the config is zero.
|
||||
errZeroMinMonitored = errors.New("must provide a non-zero minimum " +
|
||||
"monitor time for channel exclusion")
|
||||
|
||||
// DefaultOutlierMultiplier is the default value used in close
|
||||
// recommendations based on outliers when there is no user provided
|
||||
// value.
|
||||
DefaultOutlierMultiplier float64 = 3
|
||||
)
|
||||
|
||||
// CloseRecommendationConfig provides the functions and parameters required to
|
||||
// provide close recommendations.
|
||||
|
|
@ -30,20 +37,31 @@ type CloseRecommendationConfig struct {
|
|||
// public channels.
|
||||
OpenChannels func() ([]*lnrpc.Channel, error)
|
||||
|
||||
// StrongOutlier is set to true if only extreme outliers should be
|
||||
// recommended for close. A strong outlier is one which is 3 inter-
|
||||
// quartile ranges below the lower quartile (or above the upper quartile)
|
||||
// amd a weak outlier is only 1.5 inter-quartile ranges away. Choosing
|
||||
// to recommend strong outliers is a more cautious approach, because the
|
||||
// recommendations will be more lenient, only recommending extreme outliers
|
||||
// for closure.
|
||||
StrongOutlier bool
|
||||
// OutlierMultiplier is the number of inter quartile ranges a value
|
||||
// should be away from the lower/upper quartile to be considered an
|
||||
// outlier. Recommended values are 1.5 for more aggressive recommendations
|
||||
// and 3 for more cautious recommendations.
|
||||
OutlierMultiplier float64
|
||||
|
||||
// UptimeThreshold is the uptime percentage over the channel's observed
|
||||
// lifetime beneath which channels will be recommended for close. This
|
||||
// value is expressed as a percentage in [0,1], and will default to 0 if
|
||||
// it is not set.
|
||||
UptimeThreshold float64
|
||||
|
||||
// MinimumMonitored is the minimum amount of time that a channel must have
|
||||
// been monitored for before it is considered for closing.
|
||||
MinimumMonitored time.Duration
|
||||
}
|
||||
|
||||
// Recommendation provides the value that a close recommendation was
|
||||
// based on, and a boolean indicating whether we recommend closing the
|
||||
// channel.
|
||||
type Recommendation struct {
|
||||
Value float64
|
||||
RecommendClose bool
|
||||
}
|
||||
|
||||
// Report contains a set of close recommendations and information about the
|
||||
// number of channels considered for close.
|
||||
type Report struct {
|
||||
|
|
@ -54,9 +72,15 @@ type Report struct {
|
|||
// for long enough to be considered for close.
|
||||
ConsideredChannels int
|
||||
|
||||
// Recommendations is a map of chanel outpoints to a bool which indicates
|
||||
// whether we should close the channel.
|
||||
Recommendations map[string]bool
|
||||
// OutlierRecommendations is a map of chanel outpoints to a bool which
|
||||
// indicates whether we should close the channel based on whether it is
|
||||
// an outlier.
|
||||
OutlierRecommendations map[string]Recommendation
|
||||
|
||||
// ThresholdRecommendations is a map of chanel outpoints to a bool which
|
||||
// indicates whether we should close the channel based on whether it is
|
||||
// below a user provided threshold.
|
||||
ThresholdRecommendations map[string]Recommendation
|
||||
}
|
||||
|
||||
// CloseRecommendations returns a report which contains information about the
|
||||
|
|
@ -82,34 +106,68 @@ func CloseRecommendations(cfg *CloseRecommendationConfig) (*Report, error) {
|
|||
// been monitored for longer than the minimum time.
|
||||
uptime := getUptimeDataset(filtered)
|
||||
|
||||
recs, err := getCloseRecs(uptime, cfg.StrongOutlier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Report{
|
||||
report := &Report{
|
||||
TotalChannels: len(channels),
|
||||
ConsideredChannels: len(uptime),
|
||||
Recommendations: recs,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// getCloseRecs generates map of channel outpoint strings to booleans indicating
|
||||
// whether we recommend closing a channel.
|
||||
func getCloseRecs(uptime dataset.Dataset,
|
||||
strongOutlier bool) (map[string]bool, error) {
|
||||
|
||||
outliers, err := uptime.GetOutliers(strongOutlier)
|
||||
// Get close recommendations based on outliers.
|
||||
report.OutlierRecommendations, err = getOutlierRecs(
|
||||
uptime, cfg.OutlierMultiplier,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
recommendations := make(map[string]bool)
|
||||
// Get close recommendations based on threshold.
|
||||
report.ThresholdRecommendations = getThresholdRecs(
|
||||
uptime, cfg.UptimeThreshold,
|
||||
)
|
||||
|
||||
for chanpoint, outlier := range outliers {
|
||||
// If the channel is a lower outlier, recommend it for closure.
|
||||
if outlier.LowerOutlier {
|
||||
recommendations[chanpoint] = true
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// getThresholdRecs returns a map of channel points to values that are below a
|
||||
// given threshold.
|
||||
func getThresholdRecs(uptime dataset.Dataset,
|
||||
threshold float64) map[string]Recommendation {
|
||||
|
||||
// Get a map of channel labels to a boolean indicating whether
|
||||
// they are beneath the threshold.
|
||||
thresholdValues := uptime.GetThreshold(threshold, true)
|
||||
|
||||
recommendations := make(
|
||||
map[string]Recommendation, len(thresholdValues),
|
||||
)
|
||||
|
||||
for chanPoint, belowThrehsold := range thresholdValues {
|
||||
recommendations[chanPoint] = Recommendation{
|
||||
Value: uptime.Value(chanPoint),
|
||||
RecommendClose: belowThrehsold,
|
||||
}
|
||||
}
|
||||
|
||||
return recommendations
|
||||
}
|
||||
|
||||
// getOutlierRecs generates map of channel outpoint strings to booleans indicating
|
||||
// whether we recommend closing a channel.
|
||||
func getOutlierRecs(uptime dataset.Dataset,
|
||||
outlierMultiplier float64) (map[string]Recommendation, error) {
|
||||
|
||||
recommendations := make(map[string]Recommendation)
|
||||
|
||||
outliers, err := uptime.GetOutliers(outlierMultiplier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add a recommendation for each channel to our set of recommendations.
|
||||
// If the channel is a lower outlier, we recommend it for close.
|
||||
for chanPoint, outlier := range outliers {
|
||||
recommendations[chanPoint] = Recommendation{
|
||||
Value: uptime.Value(chanPoint),
|
||||
RecommendClose: outlier.LowerOutlier,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +193,7 @@ func filterChannels(openChannels []*lnrpc.Channel,
|
|||
channels[channel.ChannelPoint] = channel
|
||||
}
|
||||
|
||||
log.Debugf("considering: % channels for close out of %v",
|
||||
log.Debugf("considering: %v channels for close out of %v",
|
||||
len(channels), len(openChannels))
|
||||
|
||||
return channels
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ func TestCloseRecommendations(t *testing.T) {
|
|||
return nil, nil
|
||||
},
|
||||
MinMonitored: time.Hour,
|
||||
expectedErr: dataset.ErrTooFewValues,
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "open channels fails",
|
||||
|
|
@ -77,9 +77,9 @@ func TestCloseRecommendations(t *testing.T) {
|
|||
t.Parallel()
|
||||
|
||||
_, err := CloseRecommendations(&CloseRecommendationConfig{
|
||||
OpenChannels: test.OpenChannels,
|
||||
StrongOutlier: true,
|
||||
MinimumMonitored: test.MinMonitored,
|
||||
OpenChannels: test.OpenChannels,
|
||||
OutlierMultiplier: 3,
|
||||
MinimumMonitored: test.MinMonitored,
|
||||
})
|
||||
if err != test.expectedErr {
|
||||
t.Fatalf("expected: %v, got: %v", test.expectedErr, err)
|
||||
|
|
@ -89,15 +89,29 @@ func TestCloseRecommendations(t *testing.T) {
|
|||
}
|
||||
|
||||
// TestGetCloseRecs tests the generating of close recommendations for a set of
|
||||
// channels.
|
||||
// channels. It also contains a test case for when there are too few channels
|
||||
// to calculate outliers, to test that the error is silenced and no
|
||||
// recommendations are provided.
|
||||
func TestGetCloseRecs(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
channelUptimes map[string]float64
|
||||
expectedRecs map[string]bool
|
||||
strongOutlier bool
|
||||
name string
|
||||
channelUptimes map[string]float64
|
||||
expectedRecs map[string]Recommendation
|
||||
outlierMultiplier float64
|
||||
}{
|
||||
{
|
||||
name: "not enough values, all false",
|
||||
channelUptimes: map[string]float64{
|
||||
"a:0": 0.7,
|
||||
},
|
||||
expectedRecs: map[string]Recommendation{
|
||||
"a:0": {
|
||||
Value: 0.7,
|
||||
RecommendClose: false,
|
||||
},
|
||||
},
|
||||
outlierMultiplier: 2,
|
||||
},
|
||||
{
|
||||
name: "similar values, weak outlier no recommendations",
|
||||
channelUptimes: map[string]float64{
|
||||
|
|
@ -105,8 +119,12 @@ func TestGetCloseRecs(t *testing.T) {
|
|||
"a:1": 0.6,
|
||||
"a:20": 0.5,
|
||||
},
|
||||
strongOutlier: false,
|
||||
expectedRecs: map[string]bool{},
|
||||
outlierMultiplier: 1.5,
|
||||
expectedRecs: map[string]Recommendation{
|
||||
"a:0": {Value: 0.7, RecommendClose: false},
|
||||
"a:1": {Value: 0.6, RecommendClose: false},
|
||||
"a:20": {Value: 0.5, RecommendClose: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "similar values, strong outlier no recommendations",
|
||||
|
|
@ -115,8 +133,12 @@ func TestGetCloseRecs(t *testing.T) {
|
|||
"a:1": 0.6,
|
||||
"a:2": 0.5,
|
||||
},
|
||||
strongOutlier: true,
|
||||
expectedRecs: map[string]bool{},
|
||||
outlierMultiplier: 3,
|
||||
expectedRecs: map[string]Recommendation{
|
||||
"a:0": {Value: 0.7, RecommendClose: false},
|
||||
"a:1": {Value: 0.6, RecommendClose: false},
|
||||
"a:2": {Value: 0.5, RecommendClose: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "lower outlier recommended for close",
|
||||
|
|
@ -128,9 +150,35 @@ func TestGetCloseRecs(t *testing.T) {
|
|||
"a:4": 0.5,
|
||||
"a:5": 0.1,
|
||||
},
|
||||
strongOutlier: true,
|
||||
expectedRecs: map[string]bool{
|
||||
"a:5": true,
|
||||
outlierMultiplier: 3,
|
||||
expectedRecs: map[string]Recommendation{
|
||||
"a:0": {Value: 0.6, RecommendClose: false},
|
||||
"a:1": {Value: 0.6, RecommendClose: false},
|
||||
"a:2": {Value: 0.5, RecommendClose: false},
|
||||
"a:3": {Value: 0.5, RecommendClose: false},
|
||||
"a:4": {Value: 0.5, RecommendClose: false},
|
||||
"a:5": {Value: 0.1, RecommendClose: true},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "zero multiplier replaced with default",
|
||||
channelUptimes: map[string]float64{
|
||||
"a:0": 0.6,
|
||||
"a:1": 0.6,
|
||||
"a:2": 0.5,
|
||||
"a:3": 0.5,
|
||||
"a:4": 0.5,
|
||||
"a:5": 0.1,
|
||||
},
|
||||
outlierMultiplier: 0,
|
||||
expectedRecs: map[string]Recommendation{
|
||||
"a:0": {Value: 0.6, RecommendClose: false},
|
||||
"a:1": {Value: 0.6, RecommendClose: false},
|
||||
"a:2": {Value: 0.5, RecommendClose: false},
|
||||
"a:3": {Value: 0.5, RecommendClose: false},
|
||||
"a:4": {Value: 0.5, RecommendClose: false},
|
||||
"a:5": {Value: 0.1, RecommendClose: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -143,17 +191,24 @@ func TestGetCloseRecs(t *testing.T) {
|
|||
|
||||
uptimeData := dataset.New(test.channelUptimes)
|
||||
|
||||
recs, err := getCloseRecs(uptimeData, test.strongOutlier)
|
||||
recs, err := getOutlierRecs(uptimeData, test.outlierMultiplier)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
// Run through our expected set of recommendations and check that
|
||||
// they match the set returned in the report.
|
||||
|
||||
if len(test.expectedRecs) != len(recs) {
|
||||
t.Fatalf("expected: %v recommendations, got: %v",
|
||||
len(test.expectedRecs), len(recs))
|
||||
}
|
||||
|
||||
// Run through our expected set of true recommendations
|
||||
// and check that they match the set returned in the report.
|
||||
for channel, expectClose := range test.expectedRecs {
|
||||
recClose := recs[channel]
|
||||
if recClose != expectClose {
|
||||
t.Fatalf("expected close rec: %v for channel: %v,"+
|
||||
" got: %v", expectClose, channel, recClose)
|
||||
t.Fatalf("expected close rec: %v"+
|
||||
" for channel: %v, got: %v",
|
||||
expectClose, channel, recClose)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,12 +2,11 @@
|
|||
package terminator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/lightninglabs/loop/lndclient"
|
||||
"github.com/lightninglabs/terminator/recommend"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightninglabs/terminator/trmrpc"
|
||||
"github.com/lightningnetwork/lnd/signal"
|
||||
)
|
||||
|
||||
// Main is the real entry point for terminator. It is required to ensure that
|
||||
|
|
@ -18,9 +17,6 @@ func Main() error {
|
|||
return fmt.Errorf("error loading config: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// NewBasicClient get a lightning rpc client with
|
||||
client, err := lndclient.NewBasicClient(
|
||||
config.RPCServer,
|
||||
|
|
@ -30,44 +26,28 @@ func Main() error {
|
|||
lndclient.MacFilename(config.MacaroonFile),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot connect to lightning client: %v", err)
|
||||
return fmt.Errorf("cannot connect to lightning client: %v",
|
||||
err)
|
||||
}
|
||||
|
||||
// Get channel close recommendations for the current set of open public
|
||||
// channels.
|
||||
report, err := recommend.CloseRecommendations(
|
||||
&recommend.CloseRecommendationConfig{
|
||||
// OpenChannels provides all of the open, public channels for the
|
||||
// node.
|
||||
OpenChannels: func() (channels []*lnrpc.Channel, e error) {
|
||||
resp, err := client.ListChannels(ctx,
|
||||
&lnrpc.ListChannelsRequest{
|
||||
PublicOnly: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Instantiate the terminator gRPC server.
|
||||
server := trmrpc.NewRPCServer(
|
||||
&trmrpc.Config{
|
||||
LightningClient: client,
|
||||
RPCListen: config.RPCListen,
|
||||
},
|
||||
)
|
||||
|
||||
return resp.Channels, nil
|
||||
},
|
||||
|
||||
// For the first iteration of the terminator, do not allow users
|
||||
// to configure recommendations to penalize weak outliers.
|
||||
StrongOutlier: true,
|
||||
|
||||
// Set the minimum monitor time to the value provided in our config.
|
||||
MinimumMonitored: config.MinimumMonitored,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get close recommendations: %v", err)
|
||||
if err := server.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("Considering: %v channels for closure from a "+
|
||||
"total of: %v. Produced %v recommendations.", report.ConsideredChannels,
|
||||
report.TotalChannels, len(report.Recommendations))
|
||||
// Run until the user terminates.
|
||||
<-signal.ShutdownChannel()
|
||||
log.Infof("Received shutdown signal.")
|
||||
|
||||
for channel, rec := range report.Recommendations {
|
||||
log.Infof("%v: %v", channel, rec)
|
||||
if err := server.Stop(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("That's all for now. I will be back.")
|
||||
|
|
|
|||
8
trmrpc/README.md
Normal file
8
trmrpc/README.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
trmrpc
|
||||
=====
|
||||
This package implements terminator's RPC client and server.
|
||||
|
||||
## Generate protobuf definitions
|
||||
1. Follow the installation steps provided in lnd's [installation instructions](https://github.com/lightningnetwork/lnd/blob/master/lnrpc/README.md#generate-protobuf-definitions).
|
||||
|
||||
2. Run [`gen_protos.sh`](https://github.com/lightninglabs/terminator/tree/master/trmrpc/gen_protos.sh) to generate new protobuf definitions.
|
||||
66
trmrpc/close_recommendations.go
Normal file
66
trmrpc/close_recommendations.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package trmrpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/lightninglabs/terminator/recommend"
|
||||
)
|
||||
|
||||
// parseRequest parses a rpc close recommendation request and returns the
|
||||
// close recommendation config that the request requires.
|
||||
func parseRequest(ctx context.Context, cfg *Config,
|
||||
req *CloseRecommendationsRequest) *recommend.CloseRecommendationConfig {
|
||||
|
||||
// Create a close recommendations config with the minimum monitored
|
||||
// value provided in the request and the default outlier multiplier.
|
||||
recConfig := &recommend.CloseRecommendationConfig{
|
||||
OpenChannels: cfg.wrapListChannels(ctx, true),
|
||||
MinimumMonitored: time.Second *
|
||||
time.Duration(req.MinimumMonitored),
|
||||
OutlierMultiplier: recommend.DefaultOutlierMultiplier,
|
||||
}
|
||||
|
||||
// If a non-zero outlier multiple was provided, set it on the config.
|
||||
if req.OutlierMultiplier != 0 {
|
||||
recConfig.OutlierMultiplier = float64(req.OutlierMultiplier)
|
||||
}
|
||||
|
||||
threshold, ok := req.Threshold.(*CloseRecommendationsRequest_UptimeThreshold)
|
||||
if ok {
|
||||
recConfig.UptimeThreshold = float64(threshold.UptimeThreshold)
|
||||
}
|
||||
|
||||
return recConfig
|
||||
}
|
||||
|
||||
// parseResponse parses the response obtained getting a close recommendation
|
||||
// and converts it to a close recommendation response.
|
||||
func parseResponse(report *recommend.Report) *CloseRecommendationsResponse {
|
||||
resp := &CloseRecommendationsResponse{
|
||||
TotalChannels: int32(report.TotalChannels),
|
||||
ConsideredChannels: int32(report.ConsideredChannels),
|
||||
}
|
||||
|
||||
for chanPoint, rec := range report.OutlierRecommendations {
|
||||
resp.OutlierRecommendations = append(
|
||||
resp.OutlierRecommendations, &Recommendation{
|
||||
ChanPoint: chanPoint,
|
||||
Value: float32(rec.Value),
|
||||
RecommendClose: rec.RecommendClose,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
for chanPoint, rec := range report.ThresholdRecommendations {
|
||||
resp.ThresholdRecommendations = append(
|
||||
resp.ThresholdRecommendations, &Recommendation{
|
||||
ChanPoint: chanPoint,
|
||||
Value: float32(rec.Value),
|
||||
RecommendClose: rec.RecommendClose,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
9
trmrpc/gen_protos.sh
Executable file
9
trmrpc/gen_protos.sh
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
#!/bin/sh
|
||||
|
||||
echo "Generating terminator gRPC server protos"
|
||||
|
||||
# Generate the protos.
|
||||
protoc -I/usr/local/include -I. \
|
||||
-I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
|
||||
--go_out=plugins=grpc,paths=source_relative:. \
|
||||
rpc.proto
|
||||
26
trmrpc/log.go
Normal file
26
trmrpc/log.go
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
package trmrpc
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btclog"
|
||||
"github.com/lightningnetwork/lnd/build"
|
||||
)
|
||||
|
||||
// Subsystem defines the logging code for this subsystem.
|
||||
const Subsystem = "TRPC"
|
||||
|
||||
// log is a logger that is initialized with no output filters. This
|
||||
// means the package will not perform any logging by default until the
|
||||
// caller requests it.
|
||||
var log btclog.Logger
|
||||
|
||||
// The default amount of logging is none.
|
||||
func init() {
|
||||
UseLogger(build.NewSubLogger(Subsystem, nil))
|
||||
}
|
||||
|
||||
// UseLogger uses a specified Logger to output package logging info.
|
||||
// This should be used in preference to SetLogWriter if the caller is also
|
||||
// using btclog.
|
||||
func UseLogger(logger btclog.Logger) {
|
||||
log = logger
|
||||
}
|
||||
368
trmrpc/rpc.pb.go
Normal file
368
trmrpc/rpc.pb.go
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// source: rpc.proto
|
||||
|
||||
package trmrpc
|
||||
|
||||
import (
|
||||
context "context"
|
||||
fmt "fmt"
|
||||
proto "github.com/golang/protobuf/proto"
|
||||
_ "google.golang.org/genproto/googleapis/api/annotations"
|
||||
grpc "google.golang.org/grpc"
|
||||
math "math"
|
||||
)
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ = proto.Marshal
|
||||
var _ = fmt.Errorf
|
||||
var _ = math.Inf
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the proto package it is being compiled against.
|
||||
// A compilation error at this line likely means your copy of the
|
||||
// proto package needs to be updated.
|
||||
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
|
||||
|
||||
type CloseRecommendationsRequest struct {
|
||||
//
|
||||
//The minimum amount of time in seconds that a channel should have been
|
||||
//monitored by lnd to be eligible for close. This value is in place to
|
||||
//protect against closing of newer channels.
|
||||
MinimumMonitored int64 `protobuf:"varint,1,opt,name=minimum_monitored,json=minimumMonitored,proto3" json:"minimum_monitored,omitempty"`
|
||||
//
|
||||
//The number of inter-quartile ranges a value needs to be beneath the lower
|
||||
//quartile/ above the upper quartile to be considered a lower/upper outlier.
|
||||
//Lower values will be more aggressive in recommending channel closes, and
|
||||
//upper values will be more conservative. Recommended values are 1.5 for
|
||||
//aggressive recommendations and 3 for conservative recommendations.
|
||||
OutlierMultiplier float32 `protobuf:"fixed32,2,opt,name=outlier_multiplier,json=outlierMultiplier,proto3" json:"outlier_multiplier,omitempty"`
|
||||
//
|
||||
//Threshold contains the threshold value that is used to recommend channels
|
||||
//for closure.
|
||||
//
|
||||
// Types that are valid to be assigned to Threshold:
|
||||
// *CloseRecommendationsRequest_UptimeThreshold
|
||||
Threshold isCloseRecommendationsRequest_Threshold `protobuf_oneof:"threshold"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *CloseRecommendationsRequest) Reset() { *m = CloseRecommendationsRequest{} }
|
||||
func (m *CloseRecommendationsRequest) String() string { return proto.CompactTextString(m) }
|
||||
func (*CloseRecommendationsRequest) ProtoMessage() {}
|
||||
func (*CloseRecommendationsRequest) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_77a6da22d6a3feb1, []int{0}
|
||||
}
|
||||
|
||||
func (m *CloseRecommendationsRequest) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_CloseRecommendationsRequest.Unmarshal(m, b)
|
||||
}
|
||||
func (m *CloseRecommendationsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_CloseRecommendationsRequest.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *CloseRecommendationsRequest) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_CloseRecommendationsRequest.Merge(m, src)
|
||||
}
|
||||
func (m *CloseRecommendationsRequest) XXX_Size() int {
|
||||
return xxx_messageInfo_CloseRecommendationsRequest.Size(m)
|
||||
}
|
||||
func (m *CloseRecommendationsRequest) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_CloseRecommendationsRequest.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_CloseRecommendationsRequest proto.InternalMessageInfo
|
||||
|
||||
func (m *CloseRecommendationsRequest) GetMinimumMonitored() int64 {
|
||||
if m != nil {
|
||||
return m.MinimumMonitored
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *CloseRecommendationsRequest) GetOutlierMultiplier() float32 {
|
||||
if m != nil {
|
||||
return m.OutlierMultiplier
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type isCloseRecommendationsRequest_Threshold interface {
|
||||
isCloseRecommendationsRequest_Threshold()
|
||||
}
|
||||
|
||||
type CloseRecommendationsRequest_UptimeThreshold struct {
|
||||
UptimeThreshold float32 `protobuf:"fixed32,3,opt,name=uptime_threshold,json=uptimeThreshold,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*CloseRecommendationsRequest_UptimeThreshold) isCloseRecommendationsRequest_Threshold() {}
|
||||
|
||||
func (m *CloseRecommendationsRequest) GetThreshold() isCloseRecommendationsRequest_Threshold {
|
||||
if m != nil {
|
||||
return m.Threshold
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *CloseRecommendationsRequest) GetUptimeThreshold() float32 {
|
||||
if x, ok := m.GetThreshold().(*CloseRecommendationsRequest_UptimeThreshold); ok {
|
||||
return x.UptimeThreshold
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// XXX_OneofWrappers is for the internal use of the proto package.
|
||||
func (*CloseRecommendationsRequest) XXX_OneofWrappers() []interface{} {
|
||||
return []interface{}{
|
||||
(*CloseRecommendationsRequest_UptimeThreshold)(nil),
|
||||
}
|
||||
}
|
||||
|
||||
type CloseRecommendationsResponse struct {
|
||||
//
|
||||
//The total number of channels, before filtering out channels that are
|
||||
//not eligible for close recommendations.
|
||||
TotalChannels int32 `protobuf:"varint,1,opt,name=total_channels,json=totalChannels,proto3" json:"total_channels,omitempty"`
|
||||
//
|
||||
//The number of channels that were considered for close recommendations.
|
||||
ConsideredChannels int32 `protobuf:"varint,2,opt,name=considered_channels,json=consideredChannels,proto3" json:"considered_channels,omitempty"`
|
||||
//
|
||||
//A map of channels to close recommendations, based out whether they are
|
||||
//outliers in the uptime dataset. The absence of a channel in this set
|
||||
//implies that it was not considered for close because it did not meet
|
||||
//the criteria for close (it is private, or has not been monitored for
|
||||
//long enough to make a decision).
|
||||
OutlierRecommendations []*Recommendation `protobuf:"bytes,3,rep,name=outlier_recommendations,json=outlierRecommendations,proto3" json:"outlier_recommendations,omitempty"`
|
||||
//
|
||||
//A set of channel close recommendations, based out whether they are
|
||||
//beneath the threshold provided in the request. The absence of a channel
|
||||
//in this set implies that it was not considered for close because it
|
||||
//did not meet the criteria for close (it is private, or has not been
|
||||
//monitored for long enough to make a decision).
|
||||
ThresholdRecommendations []*Recommendation `protobuf:"bytes,4,rep,name=threshold_recommendations,json=thresholdRecommendations,proto3" json:"threshold_recommendations,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *CloseRecommendationsResponse) Reset() { *m = CloseRecommendationsResponse{} }
|
||||
func (m *CloseRecommendationsResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*CloseRecommendationsResponse) ProtoMessage() {}
|
||||
func (*CloseRecommendationsResponse) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_77a6da22d6a3feb1, []int{1}
|
||||
}
|
||||
|
||||
func (m *CloseRecommendationsResponse) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_CloseRecommendationsResponse.Unmarshal(m, b)
|
||||
}
|
||||
func (m *CloseRecommendationsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_CloseRecommendationsResponse.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *CloseRecommendationsResponse) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_CloseRecommendationsResponse.Merge(m, src)
|
||||
}
|
||||
func (m *CloseRecommendationsResponse) XXX_Size() int {
|
||||
return xxx_messageInfo_CloseRecommendationsResponse.Size(m)
|
||||
}
|
||||
func (m *CloseRecommendationsResponse) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_CloseRecommendationsResponse.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_CloseRecommendationsResponse proto.InternalMessageInfo
|
||||
|
||||
func (m *CloseRecommendationsResponse) GetTotalChannels() int32 {
|
||||
if m != nil {
|
||||
return m.TotalChannels
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *CloseRecommendationsResponse) GetConsideredChannels() int32 {
|
||||
if m != nil {
|
||||
return m.ConsideredChannels
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *CloseRecommendationsResponse) GetOutlierRecommendations() []*Recommendation {
|
||||
if m != nil {
|
||||
return m.OutlierRecommendations
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *CloseRecommendationsResponse) GetThresholdRecommendations() []*Recommendation {
|
||||
if m != nil {
|
||||
return m.ThresholdRecommendations
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Recommendation struct {
|
||||
//
|
||||
//The channel point [funding txid: outpoint] of the channel being considered
|
||||
//for close.
|
||||
ChanPoint string `protobuf:"bytes,1,opt,name=chan_point,json=chanPoint,proto3" json:"chan_point,omitempty"`
|
||||
// The value of the metric that close recommendations were based on.
|
||||
Value float32 `protobuf:"fixed32,2,opt,name=value,proto3" json:"value,omitempty"`
|
||||
// A boolean indicating whether we recommend closing the channel.
|
||||
RecommendClose bool `protobuf:"varint,3,opt,name=recommend_close,json=recommendClose,proto3" json:"recommend_close,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *Recommendation) Reset() { *m = Recommendation{} }
|
||||
func (m *Recommendation) String() string { return proto.CompactTextString(m) }
|
||||
func (*Recommendation) ProtoMessage() {}
|
||||
func (*Recommendation) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_77a6da22d6a3feb1, []int{2}
|
||||
}
|
||||
|
||||
func (m *Recommendation) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_Recommendation.Unmarshal(m, b)
|
||||
}
|
||||
func (m *Recommendation) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_Recommendation.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *Recommendation) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_Recommendation.Merge(m, src)
|
||||
}
|
||||
func (m *Recommendation) XXX_Size() int {
|
||||
return xxx_messageInfo_Recommendation.Size(m)
|
||||
}
|
||||
func (m *Recommendation) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_Recommendation.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_Recommendation proto.InternalMessageInfo
|
||||
|
||||
func (m *Recommendation) GetChanPoint() string {
|
||||
if m != nil {
|
||||
return m.ChanPoint
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Recommendation) GetValue() float32 {
|
||||
if m != nil {
|
||||
return m.Value
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *Recommendation) GetRecommendClose() bool {
|
||||
if m != nil {
|
||||
return m.RecommendClose
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*CloseRecommendationsRequest)(nil), "trmrpc.CloseRecommendationsRequest")
|
||||
proto.RegisterType((*CloseRecommendationsResponse)(nil), "trmrpc.CloseRecommendationsResponse")
|
||||
proto.RegisterType((*Recommendation)(nil), "trmrpc.Recommendation")
|
||||
}
|
||||
|
||||
func init() { proto.RegisterFile("rpc.proto", fileDescriptor_77a6da22d6a3feb1) }
|
||||
|
||||
var fileDescriptor_77a6da22d6a3feb1 = []byte{
|
||||
// 387 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x52, 0xed, 0x6e, 0xd3, 0x40,
|
||||
0x10, 0xc4, 0x36, 0xad, 0xf0, 0x56, 0xa4, 0xe9, 0x51, 0x15, 0x53, 0x8a, 0x14, 0x05, 0x10, 0x91,
|
||||
0x2a, 0x12, 0xa9, 0xbc, 0x01, 0xfd, 0xc3, 0x9f, 0x0a, 0x74, 0xc9, 0x7f, 0xeb, 0xb0, 0x57, 0xc9,
|
||||
0x49, 0x77, 0xb7, 0xc7, 0xdd, 0x39, 0x4f, 0xc3, 0x5b, 0xf0, 0x82, 0xc8, 0x9f, 0x51, 0xd2, 0x28,
|
||||
0xff, 0xec, 0x99, 0xd9, 0xbd, 0xd9, 0x9d, 0x85, 0xd4, 0xd9, 0x62, 0x6e, 0x1d, 0x05, 0x62, 0xe7,
|
||||
0xc1, 0x69, 0x67, 0x8b, 0xdb, 0xbb, 0x35, 0xd1, 0x5a, 0xe1, 0x42, 0x58, 0xb9, 0x10, 0xc6, 0x50,
|
||||
0x10, 0x41, 0x92, 0xf1, 0xad, 0x6a, 0xfa, 0x2f, 0x82, 0xf7, 0x8f, 0x8a, 0x3c, 0x72, 0x2c, 0x48,
|
||||
0x6b, 0x34, 0x65, 0x4b, 0x73, 0xfc, 0x53, 0xa1, 0x0f, 0xec, 0x1e, 0xae, 0xb4, 0x34, 0x52, 0x57,
|
||||
0x3a, 0xd7, 0x64, 0x64, 0x20, 0x87, 0x65, 0x16, 0x4d, 0xa2, 0x59, 0xc2, 0xc7, 0x1d, 0xf1, 0xd4,
|
||||
0xe3, 0xec, 0x2b, 0x30, 0xaa, 0x82, 0x92, 0xe8, 0x72, 0x5d, 0xa9, 0x20, 0x6d, 0xfd, 0x99, 0xc5,
|
||||
0x93, 0x68, 0x16, 0xf3, 0xab, 0x8e, 0x79, 0x1a, 0x08, 0x76, 0x0f, 0xe3, 0xca, 0x06, 0xa9, 0x31,
|
||||
0x0f, 0x1b, 0x87, 0x7e, 0x43, 0xaa, 0xcc, 0x92, 0x5a, 0xfc, 0xe3, 0x05, 0xbf, 0x6c, 0x99, 0x55,
|
||||
0x4f, 0x7c, 0xbf, 0x80, 0x74, 0x50, 0x4d, 0xff, 0xc6, 0x70, 0x77, 0xdc, 0xb5, 0xb7, 0x64, 0x3c,
|
||||
0xb2, 0xcf, 0x30, 0x0a, 0x14, 0x84, 0xca, 0x8b, 0x8d, 0x30, 0x06, 0x95, 0x6f, 0x3c, 0x9f, 0xf1,
|
||||
0xd7, 0x0d, 0xfa, 0xd8, 0x81, 0x6c, 0x01, 0x6f, 0x0a, 0x32, 0x5e, 0x96, 0xe8, 0xb0, 0xdc, 0x69,
|
||||
0xe3, 0x46, 0xcb, 0x76, 0xd4, 0x50, 0xf0, 0x13, 0xde, 0xf6, 0x13, 0xba, 0xfd, 0xa7, 0xb3, 0x64,
|
||||
0x92, 0xcc, 0x2e, 0x1e, 0x6e, 0xe6, 0xed, 0xda, 0xe7, 0xfb, 0xce, 0xf8, 0x4d, 0x57, 0x76, 0x60,
|
||||
0x98, 0x2d, 0xe1, 0xdd, 0x30, 0xd6, 0xb3, 0x96, 0x2f, 0x4f, 0xb6, 0xcc, 0x86, 0xc2, 0x83, 0xa6,
|
||||
0x53, 0x03, 0xa3, 0x7d, 0x88, 0x7d, 0x00, 0xa8, 0xa7, 0xcb, 0x2d, 0x49, 0x13, 0x9a, 0x5d, 0xa4,
|
||||
0x3c, 0xad, 0x91, 0x5f, 0x35, 0xc0, 0xae, 0xe1, 0x6c, 0x2b, 0x54, 0x85, 0x5d, 0x56, 0xed, 0x0f,
|
||||
0xfb, 0x02, 0x97, 0x83, 0xa3, 0xbc, 0xa8, 0xd7, 0xdd, 0xc4, 0xf3, 0x8a, 0x8f, 0x06, 0xb8, 0x09,
|
||||
0xe1, 0xa1, 0x82, 0xf1, 0x0a, 0x9d, 0x96, 0x46, 0x04, 0x72, 0x4b, 0x74, 0x5b, 0x74, 0x4c, 0xc0,
|
||||
0xf5, 0xb1, 0x84, 0xd8, 0xc7, 0x7e, 0x9a, 0x13, 0x57, 0x77, 0xfb, 0xe9, 0xb4, 0xa8, 0x0d, 0xf9,
|
||||
0xf7, 0x79, 0x73, 0xc2, 0xdf, 0xfe, 0x07, 0x00, 0x00, 0xff, 0xff, 0x3f, 0x08, 0x22, 0xe1, 0xf5,
|
||||
0x02, 0x00, 0x00,
|
||||
}
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ context.Context
|
||||
var _ grpc.ClientConn
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
const _ = grpc.SupportPackageIsVersion4
|
||||
|
||||
// TerminatorServerClient is the client API for TerminatorServer service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
|
||||
type TerminatorServerClient interface {
|
||||
CloseRecommendations(ctx context.Context, in *CloseRecommendationsRequest, opts ...grpc.CallOption) (*CloseRecommendationsResponse, error)
|
||||
}
|
||||
|
||||
type terminatorServerClient struct {
|
||||
cc *grpc.ClientConn
|
||||
}
|
||||
|
||||
func NewTerminatorServerClient(cc *grpc.ClientConn) TerminatorServerClient {
|
||||
return &terminatorServerClient{cc}
|
||||
}
|
||||
|
||||
func (c *terminatorServerClient) CloseRecommendations(ctx context.Context, in *CloseRecommendationsRequest, opts ...grpc.CallOption) (*CloseRecommendationsResponse, error) {
|
||||
out := new(CloseRecommendationsResponse)
|
||||
err := c.cc.Invoke(ctx, "/trmrpc.TerminatorServer/CloseRecommendations", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// TerminatorServerServer is the server API for TerminatorServer service.
|
||||
type TerminatorServerServer interface {
|
||||
CloseRecommendations(context.Context, *CloseRecommendationsRequest) (*CloseRecommendationsResponse, error)
|
||||
}
|
||||
|
||||
func RegisterTerminatorServerServer(s *grpc.Server, srv TerminatorServerServer) {
|
||||
s.RegisterService(&_TerminatorServer_serviceDesc, srv)
|
||||
}
|
||||
|
||||
func _TerminatorServer_CloseRecommendations_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(CloseRecommendationsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(TerminatorServerServer).CloseRecommendations(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/trmrpc.TerminatorServer/CloseRecommendations",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(TerminatorServerServer).CloseRecommendations(ctx, req.(*CloseRecommendationsRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
var _TerminatorServer_serviceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "trmrpc.TerminatorServer",
|
||||
HandlerType: (*TerminatorServerServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "CloseRecommendations",
|
||||
Handler: _TerminatorServer_CloseRecommendations_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "rpc.proto",
|
||||
}
|
||||
84
trmrpc/rpc.proto
Normal file
84
trmrpc/rpc.proto
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
syntax = "proto3";
|
||||
|
||||
import "google/api/annotations.proto";
|
||||
|
||||
package trmrpc;
|
||||
|
||||
service TerminatorServer {
|
||||
rpc CloseRecommendations (CloseRecommendationsRequest) returns (CloseRecommendationsResponse);
|
||||
}
|
||||
|
||||
message CloseRecommendationsRequest {
|
||||
/*
|
||||
The minimum amount of time in seconds that a channel should have been
|
||||
monitored by lnd to be eligible for close. This value is in place to
|
||||
protect against closing of newer channels.
|
||||
*/
|
||||
int64 minimum_monitored = 1;
|
||||
|
||||
/*
|
||||
The number of inter-quartile ranges a value needs to be beneath the lower
|
||||
quartile/ above the upper quartile to be considered a lower/upper outlier.
|
||||
Lower values will be more aggressive in recommending channel closes, and
|
||||
upper values will be more conservative. Recommended values are 1.5 for
|
||||
aggressive recommendations and 3 for conservative recommendations.
|
||||
*/
|
||||
float outlier_multiplier = 2;
|
||||
|
||||
/*
|
||||
Threshold contains the threshold value that is used to recommend channels
|
||||
for closure.
|
||||
*/
|
||||
oneof threshold{
|
||||
/*
|
||||
The threshold percentage uptime over observed lifetime beneath which
|
||||
channels will be recommended for closure.
|
||||
*/
|
||||
float uptime_threshold = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message CloseRecommendationsResponse{
|
||||
/*
|
||||
The total number of channels, before filtering out channels that are
|
||||
not eligible for close recommendations.
|
||||
*/
|
||||
int32 total_channels = 1;
|
||||
|
||||
/*
|
||||
The number of channels that were considered for close recommendations.
|
||||
*/
|
||||
int32 considered_channels = 2;
|
||||
|
||||
/*
|
||||
A map of channels to close recommendations, based out whether they are
|
||||
outliers in the uptime dataset. The absence of a channel in this set
|
||||
implies that it was not considered for close because it did not meet
|
||||
the criteria for close (it is private, or has not been monitored for
|
||||
long enough to make a decision).
|
||||
*/
|
||||
repeated Recommendation outlier_recommendations = 3;
|
||||
|
||||
/*
|
||||
A set of channel close recommendations, based out whether they are
|
||||
beneath the threshold provided in the request. The absence of a channel
|
||||
in this set implies that it was not considered for close because it
|
||||
did not meet the criteria for close (it is private, or has not been
|
||||
monitored for long enough to make a decision).
|
||||
*/
|
||||
repeated Recommendation threshold_recommendations = 4;
|
||||
}
|
||||
|
||||
message Recommendation{
|
||||
/*
|
||||
The channel point [funding txid: outpoint] of the channel being considered
|
||||
for close.
|
||||
*/
|
||||
string chan_point = 1;
|
||||
|
||||
// The value of the metric that close recommendations were based on.
|
||||
float value = 2;
|
||||
|
||||
// A boolean indicating whether we recommend closing the channel.
|
||||
bool recommend_close = 3;
|
||||
}
|
||||
145
trmrpc/rpcserver.go
Normal file
145
trmrpc/rpcserver.go
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
// Package trmrpc contains the proto files, generated code and server logic
|
||||
// for the terminator's grpc server which serves requests for close
|
||||
// recommendations.
|
||||
//
|
||||
// The Terminator server interface is implemented by the RPCServer struct.
|
||||
// To keep this file readable, each function implemented by the interface
|
||||
// has a file named after the function call which contains rpc parsing
|
||||
// code for the request and response. If the call requires extensive
|
||||
// additional logic, and unexported function with the same name should
|
||||
// be created in this file as well.
|
||||
package trmrpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/lightninglabs/terminator/recommend"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// RPCServer implements the terminator service, serving requests over grpc.
|
||||
type RPCServer struct {
|
||||
// To be used atomically.
|
||||
started int32
|
||||
|
||||
// To be used atomically.
|
||||
stopped int32
|
||||
|
||||
// cfg contains closures and settings required for operation.
|
||||
cfg *Config
|
||||
|
||||
// grpcServer is the main gRPC RPCServer that this RPC server will
|
||||
// register itself with and accept client requests from.
|
||||
grpcServer *grpc.Server
|
||||
|
||||
// rpcListener is the to use when starting the grpc server.
|
||||
rpcListener net.Listener
|
||||
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// Config provides closures and settings required to run the rpc server.
|
||||
type Config struct {
|
||||
// LightningClient is a client which can be used to query lnd.
|
||||
LightningClient lnrpc.LightningClient
|
||||
|
||||
// RPCListen is the address:port that the rpc server should listen
|
||||
// on.
|
||||
RPCListen string
|
||||
}
|
||||
|
||||
// wrapListChannels wraps the listchannels call to lnd, with a publicOnly bool
|
||||
// that can be used to toggle whether private channels are included.
|
||||
func (c *Config) wrapListChannels(ctx context.Context,
|
||||
publicOnly bool) func() ([]*lnrpc.Channel, error) {
|
||||
|
||||
return func() (channels []*lnrpc.Channel, e error) {
|
||||
resp, err := c.LightningClient.ListChannels(
|
||||
ctx,
|
||||
&lnrpc.ListChannelsRequest{
|
||||
PublicOnly: publicOnly,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.Channels, nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewRPCServer returns a server which will listen for rpc requests on the
|
||||
// rpc listen address provided. Note that the server returned is not running,
|
||||
// and should be started using Start().
|
||||
func NewRPCServer(cfg *Config) *RPCServer {
|
||||
var opts []grpc.ServerOption
|
||||
grpcServer := grpc.NewServer(opts...)
|
||||
|
||||
return &RPCServer{
|
||||
cfg: cfg,
|
||||
grpcServer: grpcServer,
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the listener and server.
|
||||
func (s *RPCServer) Start() error {
|
||||
if atomic.AddInt32(&s.started, 1) != 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start the gRPC RPCServer listening for HTTP/2 connections.
|
||||
log.Info("Starting gRPC listener")
|
||||
grpcListener, err := net.Listen("tcp", s.cfg.RPCListen)
|
||||
if err != nil {
|
||||
return fmt.Errorf("RPC RPCServer unable to listen on %v",
|
||||
s.cfg.RPCListen)
|
||||
|
||||
}
|
||||
s.rpcListener = grpcListener
|
||||
|
||||
RegisterTerminatorServerServer(s.grpcServer, s)
|
||||
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
if err := s.grpcServer.Serve(s.rpcListener); err != nil {
|
||||
log.Errorf("could not serve grpc server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the grpc listener and server.
|
||||
func (s *RPCServer) Stop() error {
|
||||
if atomic.AddInt32(&s.stopped, 1) != 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop the grpc server and wait for all go routines to terminate.
|
||||
s.grpcServer.Stop()
|
||||
s.wg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseRecommendations provides a set of close recommendations for the
|
||||
// current set of open channels.
|
||||
func (s *RPCServer) CloseRecommendations(ctx context.Context,
|
||||
req *CloseRecommendationsRequest) (*CloseRecommendationsResponse,
|
||||
error) {
|
||||
|
||||
cfg := parseRequest(ctx, s.cfg, req)
|
||||
|
||||
report, err := recommend.CloseRecommendations(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return parseResponse(report), nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue