Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
353 lines (304 sloc)
9.24 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package decision | |
| import ( | |
| "sync" | |
| "time" | |
| "github.com/benbjohnson/clock" | |
| peer "github.com/libp2p/go-libp2p-core/peer" | |
| ) | |
| const ( | |
| // the alpha for the EWMA used to track short term usefulness | |
| shortTermAlpha = 0.5 | |
| // the alpha for the EWMA used to track long term usefulness | |
| longTermAlpha = 0.05 | |
| // how frequently the engine should sample usefulness. Peers that | |
| // interact every shortTerm time period are considered "active". | |
| shortTerm = 10 * time.Second | |
| // long term ratio defines what "long term" means in terms of the | |
| // shortTerm duration. Peers that interact once every longTermRatio are | |
| // considered useful over the long term. | |
| longTermRatio = 10 | |
| // long/short term scores for tagging peers | |
| longTermScore = 10 // this is a high tag but it grows _very_ slowly. | |
| shortTermScore = 10 // this is a high tag but it'll go away quickly if we aren't using the peer. | |
| ) | |
| // Stores the data exchange relationship between two peers. | |
| type scoreledger struct { | |
| // Partner is the remote Peer. | |
| partner peer.ID | |
| // tracks bytes sent... | |
| bytesSent uint64 | |
| // ...and received. | |
| bytesRecv uint64 | |
| // lastExchange is the time of the last data exchange. | |
| lastExchange time.Time | |
| // These scores keep track of how useful we think this peer is. Short | |
| // tracks short-term usefulness and long tracks long-term usefulness. | |
| shortScore, longScore float64 | |
| // Score keeps track of the score used in the peer tagger. We track it | |
| // here to avoid unnecessarily updating the tags in the connection manager. | |
| score int | |
| // exchangeCount is the number of exchanges with this peer | |
| exchangeCount uint64 | |
| // the record lock | |
| lock sync.RWMutex | |
| clock clock.Clock | |
| } | |
| // Receipt is a summary of the ledger for a given peer | |
| // collecting various pieces of aggregated data for external | |
| // reporting purposes. | |
| type Receipt struct { | |
| Peer string | |
| Value float64 | |
| Sent uint64 | |
| Recv uint64 | |
| Exchanged uint64 | |
| } | |
| // Increments the sent counter. | |
| func (l *scoreledger) AddToSentBytes(n int) { | |
| l.lock.Lock() | |
| defer l.lock.Unlock() | |
| l.exchangeCount++ | |
| l.lastExchange = l.clock.Now() | |
| l.bytesSent += uint64(n) | |
| } | |
| // Increments the received counter. | |
| func (l *scoreledger) AddToReceivedBytes(n int) { | |
| l.lock.Lock() | |
| defer l.lock.Unlock() | |
| l.exchangeCount++ | |
| l.lastExchange = l.clock.Now() | |
| l.bytesRecv += uint64(n) | |
| } | |
| // Returns the Receipt for this ledger record. | |
| func (l *scoreledger) Receipt() *Receipt { | |
| l.lock.RLock() | |
| defer l.lock.RUnlock() | |
| return &Receipt{ | |
| Peer: l.partner.String(), | |
| Value: float64(l.bytesSent) / float64(l.bytesRecv+1), | |
| Sent: l.bytesSent, | |
| Recv: l.bytesRecv, | |
| Exchanged: l.exchangeCount, | |
| } | |
| } | |
| // DefaultScoreLedger is used by Engine as the default ScoreLedger. | |
| type DefaultScoreLedger struct { | |
| // the score func | |
| scorePeer ScorePeerFunc | |
| // is closed on Close | |
| closing chan struct{} | |
| // protects the fields immediatly below | |
| lock sync.RWMutex | |
| // ledgerMap lists score ledgers by their partner key. | |
| ledgerMap map[peer.ID]*scoreledger | |
| // how frequently the engine should sample peer usefulness | |
| peerSampleInterval time.Duration | |
| // used by the tests to detect when a sample is taken | |
| sampleCh chan struct{} | |
| clock clock.Clock | |
| } | |
| // scoreWorker keeps track of how "useful" our peers are, updating scores in the | |
| // connection manager. | |
| // | |
| // It does this by tracking two scores: short-term usefulness and long-term | |
| // usefulness. Short-term usefulness is sampled frequently and highly weights | |
| // new observations. Long-term usefulness is sampled less frequently and highly | |
| // weights on long-term trends. | |
| // | |
| // In practice, we do this by keeping two EWMAs. If we see an interaction | |
| // within the sampling period, we record the score, otherwise, we record a 0. | |
| // The short-term one has a high alpha and is sampled every shortTerm period. | |
| // The long-term one has a low alpha and is sampled every | |
| // longTermRatio*shortTerm period. | |
| // | |
| // To calculate the final score, we sum the short-term and long-term scores then | |
| // adjust it ±25% based on our debt ratio. Peers that have historically been | |
| // more useful to us than we are to them get the highest score. | |
| func (dsl *DefaultScoreLedger) scoreWorker() { | |
| ticker := dsl.clock.Ticker(dsl.peerSampleInterval) | |
| defer ticker.Stop() | |
| type update struct { | |
| peer peer.ID | |
| score int | |
| } | |
| var ( | |
| lastShortUpdate, lastLongUpdate time.Time | |
| updates []update | |
| ) | |
| for i := 0; ; i = (i + 1) % longTermRatio { | |
| var now time.Time | |
| select { | |
| case now = <-ticker.C: | |
| case <-dsl.closing: | |
| return | |
| } | |
| // The long term update ticks every `longTermRatio` short | |
| // intervals. | |
| updateLong := i == 0 | |
| dsl.lock.Lock() | |
| for _, l := range dsl.ledgerMap { | |
| l.lock.Lock() | |
| // Update the short-term score. | |
| if l.lastExchange.After(lastShortUpdate) { | |
| l.shortScore = ewma(l.shortScore, shortTermScore, shortTermAlpha) | |
| } else { | |
| l.shortScore = ewma(l.shortScore, 0, shortTermAlpha) | |
| } | |
| // Update the long-term score. | |
| if updateLong { | |
| if l.lastExchange.After(lastLongUpdate) { | |
| l.longScore = ewma(l.longScore, longTermScore, longTermAlpha) | |
| } else { | |
| l.longScore = ewma(l.longScore, 0, longTermAlpha) | |
| } | |
| } | |
| // Calculate the new score. | |
| // | |
| // The accounting score adjustment prefers peers _we_ | |
| // need over peers that need us. This doesn't help with | |
| // leeching. | |
| var lscore float64 | |
| if l.bytesRecv == 0 { | |
| lscore = 0 | |
| } else { | |
| lscore = float64(l.bytesRecv) / float64(l.bytesRecv+l.bytesSent) | |
| } | |
| score := int((l.shortScore + l.longScore) * (lscore*.5 + .75)) | |
| // Avoid updating the connection manager unless there's a change. This can be expensive. | |
| if l.score != score { | |
| // put these in a list so we can perform the updates outside _global_ the lock. | |
| updates = append(updates, update{l.partner, score}) | |
| l.score = score | |
| } | |
| l.lock.Unlock() | |
| } | |
| dsl.lock.Unlock() | |
| // record the times. | |
| lastShortUpdate = now | |
| if updateLong { | |
| lastLongUpdate = now | |
| } | |
| // apply the updates | |
| for _, update := range updates { | |
| dsl.scorePeer(update.peer, update.score) | |
| } | |
| // Keep the memory. It's not much and it saves us from having to allocate. | |
| updates = updates[:0] | |
| // Used by the tests | |
| if dsl.sampleCh != nil { | |
| dsl.sampleCh <- struct{}{} | |
| } | |
| } | |
| } | |
| // Returns the score ledger for the given peer or nil if that peer | |
| // is not on the ledger. | |
| func (dsl *DefaultScoreLedger) find(p peer.ID) *scoreledger { | |
| // Take a read lock (as it's less expensive) to check if we have | |
| // a ledger for the peer. | |
| dsl.lock.RLock() | |
| l, ok := dsl.ledgerMap[p] | |
| dsl.lock.RUnlock() | |
| if ok { | |
| return l | |
| } | |
| return nil | |
| } | |
| // Returns a new scoreledger. | |
| func newScoreLedger(p peer.ID, clock clock.Clock) *scoreledger { | |
| return &scoreledger{ | |
| partner: p, | |
| clock: clock, | |
| } | |
| } | |
| // Lazily instantiates a ledger. | |
| func (dsl *DefaultScoreLedger) findOrCreate(p peer.ID) *scoreledger { | |
| l := dsl.find(p) | |
| if l != nil { | |
| return l | |
| } | |
| // There's no ledger, so take a write lock, then check again and | |
| // create the ledger if necessary. | |
| dsl.lock.Lock() | |
| defer dsl.lock.Unlock() | |
| l, ok := dsl.ledgerMap[p] | |
| if !ok { | |
| l = newScoreLedger(p, dsl.clock) | |
| dsl.ledgerMap[p] = l | |
| } | |
| return l | |
| } | |
| // GetReceipt returns aggregated data communication with a given peer. | |
| func (dsl *DefaultScoreLedger) GetReceipt(p peer.ID) *Receipt { | |
| l := dsl.find(p) | |
| if l != nil { | |
| return l.Receipt() | |
| } | |
| // Return a blank receipt otherwise. | |
| return &Receipt{ | |
| Peer: p.String(), | |
| Value: 0, | |
| Sent: 0, | |
| Recv: 0, | |
| Exchanged: 0, | |
| } | |
| } | |
| // Starts the default ledger sampling process. | |
| func (dsl *DefaultScoreLedger) Start(scorePeer ScorePeerFunc) { | |
| dsl.init(scorePeer) | |
| go dsl.scoreWorker() | |
| } | |
| // Stops the sampling process. | |
| func (dsl *DefaultScoreLedger) Stop() { | |
| close(dsl.closing) | |
| } | |
| // Initializes the score ledger. | |
| func (dsl *DefaultScoreLedger) init(scorePeer ScorePeerFunc) { | |
| dsl.lock.Lock() | |
| defer dsl.lock.Unlock() | |
| dsl.scorePeer = scorePeer | |
| } | |
| // Increments the sent counter for the given peer. | |
| func (dsl *DefaultScoreLedger) AddToSentBytes(p peer.ID, n int) { | |
| l := dsl.findOrCreate(p) | |
| l.AddToSentBytes(n) | |
| } | |
| // Increments the received counter for the given peer. | |
| func (dsl *DefaultScoreLedger) AddToReceivedBytes(p peer.ID, n int) { | |
| l := dsl.findOrCreate(p) | |
| l.AddToReceivedBytes(n) | |
| } | |
| // PeerConnected should be called when a new peer connects, meaning | |
| // we should open accounting. | |
| func (dsl *DefaultScoreLedger) PeerConnected(p peer.ID) { | |
| dsl.lock.Lock() | |
| defer dsl.lock.Unlock() | |
| _, ok := dsl.ledgerMap[p] | |
| if !ok { | |
| dsl.ledgerMap[p] = newScoreLedger(p, dsl.clock) | |
| } | |
| } | |
| // PeerDisconnected should be called when a peer disconnects to | |
| // clean up the accounting. | |
| func (dsl *DefaultScoreLedger) PeerDisconnected(p peer.ID) { | |
| dsl.lock.Lock() | |
| defer dsl.lock.Unlock() | |
| delete(dsl.ledgerMap, p) | |
| } | |
| // Creates a new instance of the default score ledger. | |
| func NewDefaultScoreLedger() *DefaultScoreLedger { | |
| return &DefaultScoreLedger{ | |
| ledgerMap: make(map[peer.ID]*scoreledger), | |
| closing: make(chan struct{}), | |
| peerSampleInterval: shortTerm, | |
| clock: clock.New(), | |
| } | |
| } | |
| // Creates a new instance of the default score ledger with testing | |
| // parameters. | |
| func NewTestScoreLedger(peerSampleInterval time.Duration, sampleCh chan struct{}, clock clock.Clock) *DefaultScoreLedger { | |
| dsl := NewDefaultScoreLedger() | |
| dsl.peerSampleInterval = peerSampleInterval | |
| dsl.sampleCh = sampleCh | |
| dsl.clock = clock | |
| return dsl | |
| } |