lnd/chanfitness/chanevent.go
carla e05b4a8e2e
chanfitness: refactor to store channels by peer
When dealing with online events, we actually need to track our events
by peer, not by channel. All we need to track channels is to have a
set of online events for a peer which at least contain those events.
This change refactors chanfitness to track by peer.
2020-09-08 13:47:18 +02:00

325 lines
9.5 KiB
Go

package chanfitness
import (
"fmt"
"time"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/clock"
)
type eventType int
const (
peerOnlineEvent eventType = iota
peerOfflineEvent
)
// String provides string representations of channel events.
func (e eventType) String() string {
switch e {
case peerOnlineEvent:
return "peer_online"
case peerOfflineEvent:
return "peer_offline"
}
return "unknown"
}
type event struct {
timestamp time.Time
eventType eventType
}
// peerLog tracks events for a peer and its channels. If we currently have no
// channels with the peer, it will simply track its current online state. If we
// do have channels open with the peer, it will track the peer's online and
// offline events so that we can calculate uptime for our channels. A single
// event log is used for these online and offline events, and uptime for a
// channel is calculated by examining a subsection of this log.
type peerLog struct {
// online stores whether the peer is currently online.
online bool
// onlineEvents is a log of timestamped events observed for the peer.
onlineEvents []*event
// clock allows creation of deterministic unit tests.
clock clock.Clock
// channels contains a set of currently open channels. Channels will be
// added and removed from this map as they are opened and closed.
channels map[wire.OutPoint]*channelInfo
}
// newPeerLog creates a log for a peer.
func newPeerLog(clock clock.Clock) *peerLog {
return &peerLog{
clock: clock,
channels: make(map[wire.OutPoint]*channelInfo),
}
}
// channelInfo contains information about a channel.
type channelInfo struct {
// openedAt tracks the first time this channel was seen. This is not
// necessarily the time that it confirmed on chain because channel
// events are not persisted at present.
openedAt time.Time
}
func newChannelInfo(openedAt time.Time) *channelInfo {
return &channelInfo{
openedAt: openedAt,
}
}
// onlineEvent records a peer online or offline event in the log.
func (p *peerLog) onlineEvent(online bool) {
p.online = online
// If we have no channels currently open with the peer, we do not want
// to commit resources to tracking their online state beyond a simple
// online boolean, so we exit early.
if p.channelCount() == 0 {
return
}
p.addEvent(online, p.clock.Now())
}
// addEvent records an online or offline event in our event log.
func (p *peerLog) addEvent(online bool, time time.Time) {
eventType := peerOnlineEvent
if !online {
eventType = peerOfflineEvent
}
event := &event{
timestamp: time,
eventType: eventType,
}
p.onlineEvents = append(p.onlineEvents, event)
}
// addChannel adds a channel to our log. If we have not tracked any online
// events for our peer yet, we create one with our peer's current online state
// so that we know the state that the peer had at channel start, which is
// required to calculate uptime over the channel's lifetime.
func (p *peerLog) addChannel(channelPoint wire.OutPoint) error {
_, ok := p.channels[channelPoint]
if ok {
return fmt.Errorf("channel: %v already present", channelPoint)
}
openTime := p.clock.Now()
p.channels[channelPoint] = newChannelInfo(openTime)
// If we do not have any online events tracked for our peer (which is
// the case when we have no other channels open with the peer), we add
// an event with the peer's current online state so that we know that
// starting state for this peer when a channel was connected (which
// allows us to calculate uptime over the lifetime of the channel).
if len(p.onlineEvents) == 0 {
p.addEvent(p.online, openTime)
}
return nil
}
// removeChannel removes a channel from our log. If we have no more channels
// with the peer after removing this one, we clear our list of events.
func (p *peerLog) removeChannel(channelPoint wire.OutPoint) error {
_, ok := p.channels[channelPoint]
if !ok {
return fmt.Errorf("channel: %v not present", channelPoint)
}
delete(p.channels, channelPoint)
// If we have no more channels in our event log, we can discard all of
// our online events in memory, since we don't need them anymore.
// TODO(carla): this could be done on a per channel basis.
if p.channelCount() == 0 {
p.onlineEvents = nil
}
return nil
}
// channelCount returns the number of channels that we currently have
// with the peer.
func (p *peerLog) channelCount() int {
return len(p.channels)
}
// channelUptime looks up a channel and returns the amount of time that the
// channel has been monitored for and its uptime over this period.
func (p *peerLog) channelUptime(channelPoint wire.OutPoint) (time.Duration,
time.Duration, error) {
channel, ok := p.channels[channelPoint]
if !ok {
return 0, 0, ErrChannelNotFound
}
now := p.clock.Now()
uptime, err := p.uptime(channel.openedAt, now)
if err != nil {
return 0, 0, err
}
return now.Sub(channel.openedAt), uptime, nil
}
// onlinePeriod represents a period of time over which a peer was online.
type onlinePeriod struct {
start, end time.Time
}
// getOnlinePeriods returns a list of all the periods that the event log has
// recorded the remote peer as being online. In the unexpected case where there
// are no events, the function returns early. Online periods are defined as a
// peer online event which is terminated by a peer offline event. If the event
// log ends on a peer online event, it appends a final period which is
// calculated until the present. This function expects the event log provided
// to be ordered by ascending timestamp, and can tolerate multiple consecutive
// online or offline events.
func (p *peerLog) getOnlinePeriods() []*onlinePeriod {
// Return early if there are no events, there are no online periods.
if len(p.onlineEvents) == 0 {
return nil
}
var (
// lastEvent tracks the last event that we had that was of
// a different type to our own. It is used to determine the
// start time of our online periods when we experience an
// offline event, and to track our last recorded state.
lastEvent *event
onlinePeriods []*onlinePeriod
)
// Loop through all events to build a list of periods that the peer was
// online. Online periods are added when they are terminated with a peer
// offline event. If the log ends on an online event, the period between
// the online event and the present is not tracked. The type of the most
// recent event is tracked using the offline bool so that we can add a
// final online period if necessary.
for _, event := range p.onlineEvents {
switch event.eventType {
case peerOnlineEvent:
// If our previous event is nil, we just set it and
// break out of the switch.
if lastEvent == nil {
lastEvent = event
break
}
// If our previous event was an offline event, we update
// it to this event. We do not do this if it was an
// online event because duplicate online events would
// progress our online timestamp forward (rather than
// keep it at our earliest online event timestamp).
if lastEvent.eventType == peerOfflineEvent {
lastEvent = event
}
case peerOfflineEvent:
// If our previous event is nil, we just set it and
// break out of the switch since we cannot record an
// online period from this single event.
if lastEvent == nil {
lastEvent = event
break
}
// If the last event we saw was an online event, we
// add an online period to our set and progress our
// previous event to this offline event. We do not
// do this if we have had duplicate offline events
// because we would be tracking the most recent offline
// event (rather than keep it at our earliest offline
// event timestamp).
if lastEvent.eventType == peerOnlineEvent {
onlinePeriods = append(
onlinePeriods, &onlinePeriod{
start: lastEvent.timestamp,
end: event.timestamp,
},
)
lastEvent = event
}
}
}
// If the last event was an peer offline event, we do not need to
// calculate a final online period and can return online periods as is.
if lastEvent.eventType == peerOfflineEvent {
return onlinePeriods
}
// The log ended on an online event, so we need to add a final online
// period which terminates at the present.
finalEvent := &onlinePeriod{
start: lastEvent.timestamp,
end: p.clock.Now(),
}
// Add the final online period to the set and return.
return append(onlinePeriods, finalEvent)
}
// uptime calculates the total uptime we have recorded for a peer over the
// inclusive range specified. An error is returned if the end of the range is
// before the start or a zero end time is returned.
func (p *peerLog) uptime(start, end time.Time) (time.Duration, error) {
// Error if we are provided with an invalid range to calculate uptime
// for.
if end.Before(start) {
return 0, fmt.Errorf("end time: %v before start time: %v",
end, start)
}
if end.IsZero() {
return 0, fmt.Errorf("zero end time")
}
var uptime time.Duration
for _, p := range p.getOnlinePeriods() {
// The online period ends before the range we're looking at, so
// we can skip over it.
if p.end.Before(start) {
continue
}
// The online period starts after the range we're looking at, so
// can stop calculating uptime.
if p.start.After(end) {
break
}
// If the online period starts before our range, shift the start
// time up so that we only calculate uptime from the start of
// our range.
if p.start.Before(start) {
p.start = start
}
// If the online period ends before our range, shift the end
// time forward so that we only calculate uptime until the end
// of the range.
if p.end.After(end) {
p.end = end
}
uptime += p.end.Sub(p.start)
}
return uptime, nil
}