lnd/watchtower/wtdb/range_index_test.go
Elle Mouton 25afc8ad90
watchtower: add RangeIndex and tests
In this commit, a new concept called a RangeIndex is introduced. It
provides an efficient way to keep track of numbers added to a set by
keeping track of various ranges instead of individual numbers.

Notably, it also provides a way to map the contents & diffs applied to
the in memory RangeIndex (which uses a sorted array structure) to a
persisted KV structure.
2023-01-11 13:46:55 +02:00

350 lines
7.5 KiB
Go

package wtdb
import (
"fmt"
"math/rand"
"testing"
"time"
"github.com/stretchr/testify/require"
)
// TestRangeIndex tests that the RangeIndex works as expected.
func TestRangeIndex(t *testing.T) {
t.Parallel()
assertRanges := func(index *RangeIndex, kvStore *mockKVStore,
v map[uint64]uint64) {
require.EqualValues(t, v, index.GetAllRanges())
require.EqualValues(t, v, kvStore.kv)
}
t.Run("test zero value height", func(t *testing.T) {
t.Parallel()
kvStore := newMockKVStore(nil)
index, err := NewRangeIndex(nil)
require.NoError(t, err)
// Since zero values are tricky, assert that the empty index
// does not include zero.
require.False(t, index.IsInIndex(0))
// Now add zero to the index.
_ = index.Add(0, kvStore)
assertRanges(index, kvStore, map[uint64]uint64{0: 0})
require.True(t, index.IsInIndex(0))
})
t.Run("add duplicates", func(t *testing.T) {
t.Parallel()
kvStore := newMockKVStore(nil)
index, err := NewRangeIndex(nil)
require.NoError(t, err)
require.False(t, index.IsInIndex(1))
// Add 1 to the range.
err = index.Add(1, kvStore)
require.NoError(t, err)
assertRanges(index, kvStore, map[uint64]uint64{1: 1})
require.EqualValues(t, 1, index.MaxHeight())
require.True(t, index.IsInIndex(1))
// Add 1 again and assert that nothing has changed.
err = index.Add(1, kvStore)
require.NoError(t, err)
assertRanges(index, kvStore, map[uint64]uint64{1: 1})
require.EqualValues(t, 1, index.MaxHeight())
require.True(t, index.IsInIndex(1))
})
t.Run("extend an existing range", func(t *testing.T) {
t.Parallel()
kvStore := newMockKVStore(nil)
index, err := NewRangeIndex(nil)
require.NoError(t, err)
assertRanges(index, kvStore, map[uint64]uint64{})
// Add 2.
_ = index.Add(2, kvStore)
assertRanges(index, kvStore, map[uint64]uint64{
2: 2,
})
// Add 3, 4 and 5 and assert that these just extend the existing
// range by incrementing its end value.
_ = index.Add(3, kvStore)
_ = index.Add(4, kvStore)
_ = index.Add(5, kvStore)
assertRanges(index, kvStore, map[uint64]uint64{
2: 5,
})
// Now add 1 and 0 and assert that these just extend the
// existing range by decrementing its start value.
_ = index.Add(1, kvStore)
_ = index.Add(0, kvStore)
assertRanges(index, kvStore, map[uint64]uint64{
0: 5,
})
// Assert various other properties of the current range.
require.True(t, index.IsInIndex(3))
require.EqualValues(t, 5, index.MaxHeight())
})
t.Run("add new ranges above and below", func(t *testing.T) {
t.Parallel()
// Initialise the index with an initial range.
initialState := map[uint64]uint64{
4: 10,
}
kvStore := newMockKVStore(initialState)
index, err := NewRangeIndex(initialState)
require.NoError(t, err)
// Add 2 and 12. This should create two new ranges in the index.
_ = index.Add(12, kvStore)
_ = index.Add(2, kvStore)
assertRanges(index, kvStore, map[uint64]uint64{
2: 2,
4: 10,
12: 12,
})
// Assert various other properties of the current range.
require.EqualValues(t, 12, index.MaxHeight())
require.False(t, index.IsInIndex(3))
require.False(t, index.IsInIndex(11))
require.True(t, index.IsInIndex(2))
require.True(t, index.IsInIndex(5))
require.True(t, index.IsInIndex(12))
})
t.Run("merging two ranges", func(t *testing.T) {
t.Parallel()
// Initialise the index with an initial set of ranges.
initialState := map[uint64]uint64{
2: 2,
4: 10,
12: 12,
}
kvStore := newMockKVStore(initialState)
index, err := NewRangeIndex(initialState)
require.NoError(t, err)
// Adding 3 should merge the first and second ranges.
_ = index.Add(3, kvStore)
assertRanges(index, kvStore, map[uint64]uint64{
2: 10,
12: 12,
})
// Adding 11 should merge the first and second ranges.
_ = index.Add(11, kvStore)
assertRanges(index, kvStore, map[uint64]uint64{
2: 12,
})
// Assert various other properties of the current range.
require.EqualValues(t, 12, index.MaxHeight())
require.True(t, index.IsInIndex(2))
require.True(t, index.IsInIndex(5))
require.True(t, index.IsInIndex(12))
require.False(t, index.IsInIndex(1))
})
t.Run("failure applying KV store updates", func(t *testing.T) {
t.Parallel()
kvStore := newMockKVStore(nil)
index, err := NewRangeIndex(nil)
require.NoError(t, err)
assertRanges(index, kvStore, map[uint64]uint64{})
// Ensure that the kv store will return an error when its
// methods are called.
kvStore.setError(fmt.Errorf("db error"))
// Now attempt to add a new item to the range.
err = index.Add(20, kvStore)
require.Error(t, err)
// Assert that the update failed for both the kv store and the
// array store.
assertRanges(index, kvStore, map[uint64]uint64{})
// Now let the kv store again not return an error.
kvStore.setError(nil)
// Again attempt to add a new item to the range.
err = index.Add(20, kvStore)
require.NoError(t, err)
// It should now succeed.
assertRanges(index, kvStore, map[uint64]uint64{
20: 20,
})
})
t.Run("initialising with different ranges", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input map[uint64]uint64
expectErr bool
expectedIndex map[uint64]uint64
}{
{
name: "invalid ranges",
input: map[uint64]uint64{
1: 5,
6: 4,
},
expectErr: true,
},
{
name: "non-overlapping ranges",
input: map[uint64]uint64{
1: 2,
4: 6,
8: 20,
},
expectedIndex: map[uint64]uint64{
1: 2,
4: 6,
8: 20,
},
},
{
name: "merge-able ranges",
input: map[uint64]uint64{
1: 2,
3: 6,
7: 20,
},
expectedIndex: map[uint64]uint64{
1: 20,
},
},
{
name: "overlapping ranges",
input: map[uint64]uint64{
1: 4,
3: 7,
6: 20,
},
expectedIndex: map[uint64]uint64{
1: 20,
},
},
}
for _, test := range tests {
index, err := NewRangeIndex(test.input)
if test.expectErr {
require.Error(t, err)
continue
}
require.NoError(t, err)
require.EqualValues(
t, test.expectedIndex, index.GetAllRanges(),
)
}
})
t.Run("test large number of random inserts", func(t *testing.T) {
t.Parallel()
size := 10000
// Construct an array with values from 0 to size.
arr := make([]uint64, size)
for i := 0; i < size; i++ {
arr[i] = uint64(i)
}
// Shuffle the array so that the values are not added in order.
rand.Seed(time.Now().UnixNano())
rand.Shuffle(len(arr), func(i, j int) {
arr[i], arr[j] = arr[j], arr[i]
})
kvStore := newMockKVStore(nil)
index, err := NewRangeIndex(nil)
require.NoError(t, err)
// Now add each item in the array to the index.
for _, n := range arr {
_ = index.Add(n, kvStore)
}
// Assert that in the end, there is only a single range.
assertRanges(index, kvStore, map[uint64]uint64{
0: uint64(size - 1),
})
require.EqualValues(t, uint64(size-1), index.MaxHeight())
})
}
type mockKVStore struct {
kv map[uint64]uint64
err error
}
func newMockKVStore(initialRanges map[uint64]uint64) *mockKVStore {
if initialRanges != nil {
return &mockKVStore{
kv: initialRanges,
}
}
return &mockKVStore{
kv: make(map[uint64]uint64),
}
}
func (m *mockKVStore) setError(err error) {
m.err = err
}
func (m *mockKVStore) Put(key, value []byte) error {
if m.err != nil {
return m.err
}
k := byteOrder.Uint64(key)
v := byteOrder.Uint64(value)
m.kv[k] = v
return nil
}
func (m *mockKVStore) Delete(key []byte) error {
if m.err != nil {
return m.err
}
k := byteOrder.Uint64(key)
delete(m.kv, k)
return nil
}