summary: moves availability code and adds testcases

extracts the availability calculations to own testable module
This commit is contained in:
Michael Schmoock
2020-07-23 20:41:22 +02:00
committed by Christian Decker
parent 01b075117f
commit 5a6489c3ae
3 changed files with 194 additions and 41 deletions

View File

@@ -2,7 +2,7 @@
from pyln.client import Plugin, Millisatoshi from pyln.client import Plugin, Millisatoshi
from packaging import version from packaging import version
from collections import namedtuple from collections import namedtuple
from datetime import datetime from summary_avail import *
import pyln.client import pyln.client
from math import floor, log10 from math import floor, log10
import requests import requests
@@ -30,20 +30,6 @@ summary_description = "Gets summary information about this node.\n"\
"Pass a list of scids to the {exclude} parameter"\ "Pass a list of scids to the {exclude} parameter"\
" to exclude some channels from the outputs." " to exclude some channels from the outputs."
# Global state to measure online% and last_seen
peerstate = {}
# ensure an rpc peer is added
def addpeer(p):
pid = p['id']
if not pid in peerstate:
peerstate[pid] = {
'connected' : p['connected'],
'last_seen' : datetime.now() if p['connected'] else None,
'availability' : 1.0 if p['connected'] else 0.0
}
class PeerThread(threading.Thread): class PeerThread(threading.Thread):
def __init__(self): def __init__(self):
@@ -51,36 +37,16 @@ class PeerThread(threading.Thread):
self.daemon = True self.daemon = True
def run(self): def run(self):
interval = 5 * 60 # collect peer state once in a while
window = 3 * 24 * 60 * 60 # 72hr availability window
count = 0
# delay initial execution, so peers have a chance to connect on startup # delay initial execution, so peers have a chance to connect on startup
time.sleep(interval) time.sleep(plugin.avail_interval)
while True: while True:
count += 1
leadwin = max(min(window, count * interval), interval)
samples = leadwin / interval
alpha = 1.0 / samples
beta = 1.0 - alpha
try: try:
peers = plugin.rpc.listpeers() rpcpeers = plugin.rpc.listpeers()
for p in peers['peers']: trace_availability(plugin, rpcpeers)
pid = p['id'] time.sleep(plugin.avail_interval)
addpeer(p)
if p['connected']:
peerstate[pid]['last_seen'] = datetime.now()
peerstate[pid]['connected'] = True
peerstate[pid]['availability'] = 1.0 * alpha + peerstate[pid]['availability'] * beta
else:
peerstate[pid]['connected'] = False
peerstate[pid]['availability'] = 0.0 * alpha + peerstate[pid]['availability'] * beta
except Exception as ex: except Exception as ex:
plugin.log("[PeerThread] " + str(ex), 'warn') plugin.log("[PeerThread] " + str(ex), 'warn')
time.sleep(interval)
class PriceThread(threading.Thread): class PriceThread(threading.Thread):
@@ -177,7 +143,7 @@ def summary(plugin, exclude=''):
reply['num_gossipers'] = 0 reply['num_gossipers'] = 0
for p in peers['peers']: for p in peers['peers']:
pid = p['id'] pid = p['id']
addpeer(p) addpeer(plugin, p)
active_channel = False active_channel = False
for c in p['channels']: for c in p['channels']:
if c['state'] != 'CHANNELD_NORMAL': if c['state'] != 'CHANNELD_NORMAL':
@@ -208,7 +174,7 @@ def summary(plugin, exclude=''):
c['private'], c['private'],
p['connected'], p['connected'],
c['short_channel_id'], c['short_channel_id'],
peerstate[pid]['availability'] plugin.avail_peerstate[pid]['avail']
)) ))
if not active_channel and p['connected']: if not active_channel and p['connected']:
@@ -288,6 +254,12 @@ def init(options, configuration, plugin):
plugin.currency = options['summary-currency'] plugin.currency = options['summary-currency']
plugin.currency_prefix = options['summary-currency-prefix'] plugin.currency_prefix = options['summary-currency-prefix']
plugin.fiat_per_btc = 0 plugin.fiat_per_btc = 0
plugin.avail_peerstate = {}
plugin.avail_count = 0
plugin.avail_interval = float(options['summary-availability-interval'])
plugin.avail_window = 60 * 60 * int(options['summary-availability-window'])
info = plugin.rpc.getinfo() info = plugin.rpc.getinfo()
# Measure availability # Measure availability
@@ -323,4 +295,14 @@ plugin.add_option(
'USD $', 'USD $',
'What prefix to use for currency' 'What prefix to use for currency'
) )
plugin.add_option(
'summary-availability-interval',
300,
'How often in seconds the availability should be calculated.'
)
plugin.add_option(
'summary-availability-window',
72,
'How many hours the availability should be averaged over.'
)
plugin.run() plugin.run()

32
summary/summary_avail.py Normal file
View File

@@ -0,0 +1,32 @@
from datetime import datetime
# ensure an rpc peer is added
def addpeer(p, rpcpeer):
pid = rpcpeer['id']
if not pid in p.avail_peerstate:
p.avail_peerstate[pid] = {
'connected' : rpcpeer['connected'],
'last_seen' : datetime.now() if rpcpeer['connected'] else None,
'avail' : 1.0 if rpcpeer['connected'] else 0.0
}
# exponetially smooth online/offline states of peers
def trace_availability(p, rpcpeers):
p.avail_count += 1
leadwin = max(min(p.avail_window, p.avail_count * p.avail_interval), p.avail_interval)
samples = leadwin / p.avail_interval
alpha = 1.0 / samples
beta = 1.0 - alpha
for rpcpeer in rpcpeers['peers']:
pid = rpcpeer['id']
addpeer(p, rpcpeer)
if rpcpeer['connected']:
p.avail_peerstate[pid]['last_seen'] = datetime.now()
p.avail_peerstate[pid]['connected'] = True
p.avail_peerstate[pid]['avail'] = 1.0 * alpha + p.avail_peerstate[pid]['avail'] * beta
else:
p.avail_peerstate[pid]['connected'] = False
p.avail_peerstate[pid]['avail'] = 0.0 * alpha + p.avail_peerstate[pid]['avail'] * beta

View File

@@ -1,12 +1,151 @@
import subprocess import subprocess
import unittest import unittest
from pyln.client import Plugin
from pyln.testing.fixtures import * # noqa: F401,F403 from pyln.testing.fixtures import * # noqa: F401,F403
from pyln.testing.utils import DEVELOPER from pyln.testing.utils import DEVELOPER
from summary_avail import *
pluginopt = {'plugin': os.path.join(os.path.dirname(__file__), "summary.py")} pluginopt = {'plugin': os.path.join(os.path.dirname(__file__), "summary.py")}
# returns a test plugin stub
def get_stub():
plugin = Plugin()
plugin.avail_peerstate = {}
plugin.avail_count = 0
plugin.avail_interval = 60
plugin.avail_window = 3600
return plugin
# tests the 72hr exponential availibility tracing
# tests base algo and peerstate tracing
def test_summary_avail_101():
# given
plugin = get_stub()
rpcpeers = {
'peers' : [
{ 'id' : '1', 'connected' : True },
{ 'id' : '2', 'connected' : False },
{ 'id' : '3', 'connected' : True },
]
}
# when
for i in range(100):
trace_availability(plugin, rpcpeers)
# then
assert(plugin.avail_peerstate['1']['avail'] == 1.0)
assert(plugin.avail_peerstate['2']['avail'] == 0.0)
assert(plugin.avail_peerstate['3']['avail'] == 1.0)
assert(plugin.avail_peerstate['1']['connected'] == True)
assert(plugin.avail_peerstate['2']['connected'] == False)
assert(plugin.avail_peerstate['3']['connected'] == True)
# tests for 50% downtime
def test_summary_avail_50():
# given
plugin = get_stub()
rpcpeers_on = {
'peers' : [
{ 'id' : '1', 'connected' : True },
]
}
rpcpeers_off = {
'peers' : [
{ 'id' : '1', 'connected' : False },
]
}
# when
for i in range(30):
trace_availability(plugin, rpcpeers_on)
for i in range(30):
trace_availability(plugin, rpcpeers_off)
# then
assert(round(plugin.avail_peerstate['1']['avail'], 3) == 0.5)
# tests for 2/3 downtime
def test_summary_avail_33():
# given
plugin = get_stub()
rpcpeers_on = {
'peers' : [
{ 'id' : '1', 'connected' : True },
]
}
rpcpeers_off = {
'peers' : [
{ 'id' : '1', 'connected' : False },
]
}
# when
for i in range(20):
trace_availability(plugin, rpcpeers_on)
for i in range(40):
trace_availability(plugin, rpcpeers_off)
# then
assert(round(plugin.avail_peerstate['1']['avail'], 3) == 0.333)
# tests for 1/3 downtime
def test_summary_avail_66():
# given
plugin = get_stub()
rpcpeers_on = {
'peers' : [
{ 'id' : '1', 'connected' : True },
]
}
rpcpeers_off = {
'peers' : [
{ 'id' : '1', 'connected' : False },
]
}
# when
for i in range(40):
trace_availability(plugin, rpcpeers_on)
for i in range(20):
trace_availability(plugin, rpcpeers_off)
# then
assert(round(plugin.avail_peerstate['1']['avail'], 3) == 0.667)
# checks the leading window is smaller if interval count is low
# when a node just started
def test_summary_avail_leadwin():
# given
plugin = get_stub()
rpcpeers_on = {
'peers' : [
{ 'id' : '1', 'connected' : True },
]
}
rpcpeers_off = {
'peers' : [
{ 'id' : '1', 'connected' : False },
]
}
# when
trace_availability(plugin, rpcpeers_on)
trace_availability(plugin, rpcpeers_on)
trace_availability(plugin, rpcpeers_off)
# then
assert(round(plugin.avail_peerstate['1']['avail'], 3) == 0.667)
def test_summary_start(node_factory): def test_summary_start(node_factory):
l1 = node_factory.get_node(options=pluginopt) l1 = node_factory.get_node(options=pluginopt)
s = l1.rpc.summary() s = l1.rpc.summary()