channeldb/reject_cache: add rejectCache w/ randomized eviction
This commit is contained in:
parent
3c46ceec1d
commit
af0ea3590b
95
channeldb/reject_cache.go
Normal file
95
channeldb/reject_cache.go
Normal file
@ -0,0 +1,95 @@
|
||||
package channeldb
|
||||
|
||||
// rejectFlags is a compact representation of various metadata stored by the
|
||||
// reject cache about a particular channel.
|
||||
type rejectFlags uint8
|
||||
|
||||
const (
|
||||
// rejectFlagExists is a flag indicating whether the channel exists,
|
||||
// i.e. the channel is open and has a recent channel update. If this
|
||||
// flag is not set, the channel is either a zombie or unknown.
|
||||
rejectFlagExists rejectFlags = 1 << iota
|
||||
|
||||
// rejectFlagZombie is a flag indicating whether the channel is a
|
||||
// zombie, i.e. the channel is open but has no recent channel updates.
|
||||
rejectFlagZombie
|
||||
)
|
||||
|
||||
// packRejectFlags computes the rejectFlags corresponding to the passed boolean
|
||||
// values indicating whether the edge exists or is a zombie.
|
||||
func packRejectFlags(exists, isZombie bool) rejectFlags {
|
||||
var flags rejectFlags
|
||||
if exists {
|
||||
flags |= rejectFlagExists
|
||||
}
|
||||
if isZombie {
|
||||
flags |= rejectFlagZombie
|
||||
}
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
// unpack returns the booleans packed into the rejectFlags. The first indicates
|
||||
// if the edge exists in our graph, the second indicates if the edge is a
|
||||
// zombie.
|
||||
func (f rejectFlags) unpack() (bool, bool) {
|
||||
return f&rejectFlagExists == rejectFlagExists,
|
||||
f&rejectFlagZombie == rejectFlagZombie
|
||||
}
|
||||
|
||||
// rejectCacheEntry caches frequently accessed information about a channel,
|
||||
// including the timestamps of its latest edge policies and whether or not the
|
||||
// channel exists in the graph.
|
||||
type rejectCacheEntry struct {
|
||||
upd1Time int64
|
||||
upd2Time int64
|
||||
flags rejectFlags
|
||||
}
|
||||
|
||||
// rejectCache is an in-memory cache used to improve the performance of
|
||||
// HasChannelEdge. It caches information about the whether or channel exists, as
|
||||
// well as the most recent timestamps for each policy (if they exists).
|
||||
type rejectCache struct {
|
||||
n int
|
||||
edges map[uint64]rejectCacheEntry
|
||||
}
|
||||
|
||||
// newRejectCache creates a new rejectCache with maximum capacity of n entries.
|
||||
func newRejectCache(n int) *rejectCache {
|
||||
return &rejectCache{
|
||||
n: n,
|
||||
edges: make(map[uint64]rejectCacheEntry, n),
|
||||
}
|
||||
}
|
||||
|
||||
// get returns the entry from the cache for chanid, if it exists.
|
||||
func (c *rejectCache) get(chanid uint64) (rejectCacheEntry, bool) {
|
||||
entry, ok := c.edges[chanid]
|
||||
return entry, ok
|
||||
}
|
||||
|
||||
// insert adds the entry to the reject cache. If an entry for chanid already
|
||||
// exists, it will be replaced with the new entry. If the entry doesn't exists,
|
||||
// it will be inserted to the cache, performing a random eviction if the cache
|
||||
// is at capacity.
|
||||
func (c *rejectCache) insert(chanid uint64, entry rejectCacheEntry) {
|
||||
// If entry exists, replace it.
|
||||
if _, ok := c.edges[chanid]; ok {
|
||||
c.edges[chanid] = entry
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, evict an entry at random and insert.
|
||||
if len(c.edges) == c.n {
|
||||
for id := range c.edges {
|
||||
delete(c.edges, id)
|
||||
break
|
||||
}
|
||||
}
|
||||
c.edges[chanid] = entry
|
||||
}
|
||||
|
||||
// remove deletes an entry for chanid from the cache, if it exists.
|
||||
func (c *rejectCache) remove(chanid uint64) {
|
||||
delete(c.edges, chanid)
|
||||
}
|
107
channeldb/reject_cache_test.go
Normal file
107
channeldb/reject_cache_test.go
Normal file
@ -0,0 +1,107 @@
|
||||
package channeldb
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestRejectCache checks the behavior of the rejectCache with respect to insertion,
|
||||
// eviction, and removal of cache entries.
|
||||
func TestRejectCache(t *testing.T) {
|
||||
const cacheSize = 100
|
||||
|
||||
// Create a new reject cache with the configured max size.
|
||||
c := newRejectCache(cacheSize)
|
||||
|
||||
// As a sanity check, assert that querying the empty cache does not
|
||||
// return an entry.
|
||||
_, ok := c.get(0)
|
||||
if ok {
|
||||
t.Fatalf("reject cache should be empty")
|
||||
}
|
||||
|
||||
// Now, fill up the cache entirely.
|
||||
for i := uint64(0); i < cacheSize; i++ {
|
||||
c.insert(i, entryForInt(i))
|
||||
}
|
||||
|
||||
// Assert that the cache has all of the entries just inserted, since no
|
||||
// eviction should occur until we try to surpass the max size.
|
||||
assertHasEntries(t, c, 0, cacheSize)
|
||||
|
||||
// Now, insert a new element that causes the cache to evict an element.
|
||||
c.insert(cacheSize, entryForInt(cacheSize))
|
||||
|
||||
// Assert that the cache has this last entry, as the cache should evict
|
||||
// some prior element and not the newly inserted one.
|
||||
assertHasEntries(t, c, cacheSize, cacheSize)
|
||||
|
||||
// Iterate over all inserted elements and construct a set of the evicted
|
||||
// elements.
|
||||
evicted := make(map[uint64]struct{})
|
||||
for i := uint64(0); i < cacheSize+1; i++ {
|
||||
_, ok := c.get(i)
|
||||
if !ok {
|
||||
evicted[i] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Assert that exactly one element has been evicted.
|
||||
numEvicted := len(evicted)
|
||||
if numEvicted != 1 {
|
||||
t.Fatalf("expected one evicted entry, got: %d", numEvicted)
|
||||
}
|
||||
|
||||
// Remove the highest item which initially caused the eviction and
|
||||
// reinsert the element that was evicted prior.
|
||||
c.remove(cacheSize)
|
||||
for i := range evicted {
|
||||
c.insert(i, entryForInt(i))
|
||||
}
|
||||
|
||||
// Since the removal created an extra slot, the last insertion should
|
||||
// not have caused an eviction and the entries for all channels in the
|
||||
// original set that filled the cache should be present.
|
||||
assertHasEntries(t, c, 0, cacheSize)
|
||||
|
||||
// Finally, reinsert the existing set back into the cache and test that
|
||||
// the cache still has all the entries. If the randomized eviction were
|
||||
// happening on inserts for existing cache items, we expect this to fail
|
||||
// with high probability.
|
||||
for i := uint64(0); i < cacheSize; i++ {
|
||||
c.insert(i, entryForInt(i))
|
||||
}
|
||||
assertHasEntries(t, c, 0, cacheSize)
|
||||
|
||||
}
|
||||
|
||||
// assertHasEntries queries the reject cache for all channels in the range [start,
|
||||
// end), asserting that they exist and their value matches the entry produced by
|
||||
// entryForInt.
|
||||
func assertHasEntries(t *testing.T, c *rejectCache, start, end uint64) {
|
||||
t.Helper()
|
||||
|
||||
for i := start; i < end; i++ {
|
||||
entry, ok := c.get(i)
|
||||
if !ok {
|
||||
t.Fatalf("reject cache should contain chan %d", i)
|
||||
}
|
||||
|
||||
expEntry := entryForInt(i)
|
||||
if !reflect.DeepEqual(entry, expEntry) {
|
||||
t.Fatalf("entry mismatch, want: %v, got: %v",
|
||||
expEntry, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// entryForInt generates a unique rejectCacheEntry given an integer.
|
||||
func entryForInt(i uint64) rejectCacheEntry {
|
||||
exists := i%2 == 0
|
||||
isZombie := i%3 == 0
|
||||
return rejectCacheEntry{
|
||||
upd1Time: int64(2 * i),
|
||||
upd2Time: int64(2*i + 1),
|
||||
flags: packRejectFlags(exists, isZombie),
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user