peer: Use lru cache module for nonces.

This updates the concrete peer sent nonces LRU cache to make use of the
new generic LRU cache provided by the lru module.

It also removes the nor longer necessary specialized implementation.
This commit is contained in:
Dave Collins 2019-03-18 10:08:45 -05:00
parent eb1368a3c4
commit fa047415fd
No known key found for this signature in database
GPG Key ID: B8904D9D9C93D1F2
3 changed files with 2 additions and 281 deletions

View File

@ -1,126 +0,0 @@
// Copyright (c) 2015 The btcsuite developers
// Copyright (c) 2016 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package peer
import (
"bytes"
"container/list"
"fmt"
"sync"
)
// lruNonceCache provides a concurrency safe cache that is limited to a maximum
// number of items with eviction for the oldest entry when the limit is
// exceeded.
type lruNonceCache struct {
mtx sync.Mutex
nonceCache map[uint64]*list.Element // nearly O(1) lookups
nonceList *list.List // O(1) insert, update, delete
limit uint
}
// String returns the cache as a human-readable string.
//
// This function is safe for concurrent access.
func (m *lruNonceCache) String() string {
m.mtx.Lock()
defer m.mtx.Unlock()
lastEntryNum := len(m.nonceCache) - 1
curEntry := 0
buf := bytes.NewBufferString("[")
for nonce := range m.nonceCache {
buf.WriteString(fmt.Sprintf("%d", nonce))
if curEntry < lastEntryNum {
buf.WriteString(", ")
}
curEntry++
}
buf.WriteString("]")
return fmt.Sprintf("<%d>%s", m.limit, buf.String())
}
// Exists returns whether or not the passed nonce is in the cache.
//
// This function is safe for concurrent access.
func (m *lruNonceCache) Exists(nonce uint64) bool {
m.mtx.Lock()
_, exists := m.nonceCache[nonce]
m.mtx.Unlock()
return exists
}
// Add adds the passed nonce to the cache and handles eviction of the oldest item
// if adding the new item would exceed the max limit. Adding an existing item
// makes it the most recently used item.
//
// This function is safe for concurrent access.
func (m *lruNonceCache) Add(nonce uint64) {
m.mtx.Lock()
defer m.mtx.Unlock()
// When the limit is zero, nothing can be added to the cache, so just
// return.
if m.limit == 0 {
return
}
// When the entry already exists move it to the front of the list
// thereby marking it most recently used.
if node, exists := m.nonceCache[nonce]; exists {
m.nonceList.MoveToFront(node)
return
}
// Evict the least recently used entry (back of the list) if the the new
// entry would exceed the size limit for the cache. Also reuse the list
// node so a new one doesn't have to be allocated.
if uint(len(m.nonceCache))+1 > m.limit {
node := m.nonceList.Back()
lru := node.Value.(uint64)
// Evict least recently used item.
delete(m.nonceCache, lru)
// Reuse the list node of the item that was just evicted for the
// new item.
node.Value = nonce
m.nonceList.MoveToFront(node)
m.nonceCache[nonce] = node
return
}
// The limit hasn't been reached yet, so just add the new item.
node := m.nonceList.PushFront(nonce)
m.nonceCache[nonce] = node
}
// Delete deletes the passed nonce from the cache (if it exists).
//
// This function is safe for concurrent access.
func (m *lruNonceCache) Delete(nonce uint64) {
m.mtx.Lock()
if node, exists := m.nonceCache[nonce]; exists {
m.nonceList.Remove(node)
delete(m.nonceCache, nonce)
}
m.mtx.Unlock()
}
// newLruNonceCache returns a new nonce cache that is limited to the number of
// entries specified by limit. When the number of entries exceeds the limit,
// the oldest (least recently used) entry will be removed to make room for the
// new entry.
func newLruNonceCache(limit uint) *lruNonceCache {
m := lruNonceCache{
nonceCache: make(map[uint64]*list.Element),
nonceList: list.New(),
limit: limit,
}
return &m
}

View File

@ -1,153 +0,0 @@
// Copyright (c) 2015 The btcsuite developers
// Copyright (c) 2016 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package peer
import (
"fmt"
"testing"
)
// TestLruNonceCache ensures the lruNonceCache behaves as expected including
// limiting, eviction of least-recently used entries, specific entry removal,
// and existence tests.
func TestLruNonceCache(t *testing.T) {
// Create a bunch of fake nonces to use in testing the lru nonce code.
numNonces := 10
nonces := make([]uint64, 0, numNonces)
for i := 0; i < numNonces; i++ {
nonces = append(nonces, uint64(i))
}
tests := []struct {
name string
limit int
}{
{name: "limit 0", limit: 0},
{name: "limit 1", limit: 1},
{name: "limit 5", limit: 5},
{name: "limit 7", limit: 7},
{name: "limit one less than available", limit: numNonces - 1},
{name: "limit all available", limit: numNonces},
}
testLoop:
for i, test := range tests {
// Create a new lru nonce cache limited by the specified test
// limit and add all of the test nonces. This will cause
// evicition since there are more test nonces than the limits.
lruNonceCache := newLruNonceCache(uint(test.limit))
for j := 0; j < numNonces; j++ {
lruNonceCache.Add(nonces[j])
}
// Ensure the limited number of most recent entries in the list
// exist.
for j := numNonces - test.limit; j < numNonces; j++ {
if !lruNonceCache.Exists(nonces[j]) {
t.Errorf("Exists #%d (%s) entry %d does not "+
"exist", i, test.name, nonces[j])
continue testLoop
}
}
// Ensure the entries before the limited number of most recent
// entries in the list do not exist.
for j := 0; j < numNonces-test.limit; j++ {
if lruNonceCache.Exists(nonces[j]) {
t.Errorf("Exists #%d (%s) entry %d exists", i,
test.name, nonces[j])
continue testLoop
}
}
// Readd the entry that should currently be the least-recently
// used entry so it becomes the most-recently used entry, then
// force an eviction by adding an entry that doesn't exist and
// ensure the evicted entry is the new least-recently used
// entry.
//
// This check needs at least 2 entries.
if test.limit > 1 {
origLruIndex := numNonces - test.limit
lruNonceCache.Add(nonces[origLruIndex])
lruNonceCache.Add(uint64(numNonces) + 1)
// Ensure the original lru entry still exists since it
// was updated and should've have become the lru entry.
if !lruNonceCache.Exists(nonces[origLruIndex]) {
t.Errorf("LRU #%d (%s) entry %d does not exist",
i, test.name, nonces[origLruIndex])
continue testLoop
}
// Ensure the entry that should've become the new lru
// entry was evicted.
newLruIndex := origLruIndex + 1
if lruNonceCache.Exists(nonces[newLruIndex]) {
t.Errorf("LRU #%d (%s) entry %d exists", i,
test.name, nonces[newLruIndex])
continue testLoop
}
}
// Delete all of the entries in the list, including those that
// don't exist in the cache, and ensure they no longer exist.
for j := 0; j < numNonces; j++ {
lruNonceCache.Delete(nonces[j])
if lruNonceCache.Exists(nonces[j]) {
t.Errorf("Delete #%d (%s) entry %d exists", i,
test.name, nonces[j])
continue testLoop
}
}
}
}
// TestLruNonceCacheStringer tests the stringized output for the lruNonceCache type.
func TestLruNonceCacheStringer(t *testing.T) {
// Create a couple of fake nonces to use in testing the lru nonce
// stringer code.
nonce1 := uint64(10)
nonce2 := uint64(20)
// Create new lru nonce cache and add the nonces.
lruNonceCache := newLruNonceCache(uint(2))
lruNonceCache.Add(nonce1)
lruNonceCache.Add(nonce2)
// Ensure the stringer gives the expected result. Since cache iteration
// is not ordered, either entry could be first, so account for both
// cases.
wantStr1 := fmt.Sprintf("<%d>[%d, %d]", 2, nonce1, nonce2)
wantStr2 := fmt.Sprintf("<%d>[%d, %d]", 2, nonce2, nonce1)
gotStr := lruNonceCache.String()
if gotStr != wantStr1 && gotStr != wantStr2 {
t.Fatalf("unexpected string representation - got %q, want %q "+
"or %q", gotStr, wantStr1, wantStr2)
}
}
// BenchmarkLruNonceList performs basic benchmarks on the most recently used
// nonce handling.
func BenchmarkLruNonceList(b *testing.B) {
// Create a bunch of fake nonces to use in benchmarking the lru nonce
// code.
b.StopTimer()
numNonces := 100000
nonces := make([]uint64, 0, numNonces)
for i := 0; i < numNonces; i++ {
nonces = append(nonces, uint64(i))
}
b.StartTimer()
// Benchmark the add plus evicition code.
limit := 20000
lruNonceCache := newLruNonceCache(uint(limit))
for i := 0; i < b.N; i++ {
lruNonceCache.Add(nonces[i%numNonces])
}
}

View File

@ -79,7 +79,7 @@ var (
// sentNonces houses the unique nonces that are generated when pushing
// version messages that are used to detect self connections.
sentNonces = newLruNonceCache(50)
sentNonces = lru.NewCache(50)
// allowSelfConns is only used to allow the tests to bypass the self
// connection detecting and disconnect logic since they intentionally
@ -1805,7 +1805,7 @@ func (p *Peer) readRemoteVersionMsg() error {
}
// Detect self connections.
if !allowSelfConns && sentNonces.Exists(msg.Nonce) {
if !allowSelfConns && sentNonces.Contains(msg.Nonce) {
return errors.New("disconnecting peer connected to self")
}