-
Notifications
You must be signed in to change notification settings - Fork 810
feat: add in memory implementation of HeightIndex Database #4212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
6eb6764
5cf1948
d8d93e1
c5e218c
7645992
9997910
0ce9c20
cde510e
be762aa
edeb8ff
e70e213
88f2394
0f9a5d9
1769a5c
5454574
344b0e1
bc432c3
bbeb41f
ac5ca8f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -95,17 +95,29 @@ type Database interface { | |
health.Checker | ||
} | ||
|
||
// HeightIndex defines the interface for storing and retrieving entries by height. | ||
// HeightIndex defines the interface for storing and retrieving values by height. | ||
type HeightIndex interface { | ||
// Put inserts the entry into the store at the given height. | ||
Put(height uint64, bytes []byte) error | ||
// Put inserts the value into the database at the given height. | ||
// | ||
// A nil or empty slice for [value] is allowed and will be returned as nil | ||
// when retrieved via Get. | ||
// | ||
// Note: [value] is safe to read and modify after calling Put. | ||
Put(height uint64, value []byte) error | ||
|
||
// Get retrieves an entry by its height. | ||
// Get retrieves a value by its height. | ||
// Returns ErrNotFound if the key is not present in the database. | ||
DracoLi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
// | ||
// Note: Get always returns a new copy of the data. | ||
DracoLi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
Get(height uint64) ([]byte, error) | ||
|
||
// Has checks if an entry exists at the given height. | ||
// Has checks if a value exists at the given height. | ||
// | ||
// Return true even if the stored value is nil, or empty. | ||
|
||
Has(height uint64) (bool, error) | ||
|
||
// Close closes the database. | ||
// | ||
// Note: Calling Close after Close returns ErrClosed. | ||
DracoLi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
io.Closer | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,250 @@ | ||
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. | ||
// See the file LICENSE for licensing terms. | ||
|
||
package dbtest | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/ava-labs/avalanchego/database" | ||
) | ||
|
||
// Tests is a list of all database tests | ||
var Tests = map[string]func(t *testing.T, newDB func() database.HeightIndex){ | ||
|
||
"PutGet": TestPutGet, | ||
"Has": TestHas, | ||
"CloseAndPut": TestCloseAndPut, | ||
"CloseAndGet": TestCloseAndGet, | ||
"CloseAndHas": TestCloseAndHas, | ||
"Close": TestClose, | ||
"ModifyValueAfterPut": TestModifyValueAfterPut, | ||
"ModifyValueAfterGet": TestModifyValueAfterGet, | ||
} | ||
DracoLi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
type putArgs struct { | ||
height uint64 | ||
data []byte | ||
} | ||
|
||
func TestPutGet(t *testing.T, newDB func() database.HeightIndex) { | ||
tests := []struct { | ||
name string | ||
puts []putArgs | ||
queryHeight uint64 | ||
expected []byte | ||
expectedErr error | ||
DracoLi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
}{ | ||
{ | ||
name: "normal operation", | ||
puts: []putArgs{ | ||
{1, []byte("test data 1")}, | ||
}, | ||
queryHeight: 1, | ||
expected: []byte("test data 1"), | ||
}, | ||
{ | ||
name: "not found error when getting on non-existing height", | ||
puts: []putArgs{ | ||
{1, []byte("test data")}, | ||
}, | ||
queryHeight: 2, | ||
expected: nil, | ||
expectedErr: database.ErrNotFound, | ||
}, | ||
{ | ||
name: "overwriting data on same height", | ||
puts: []putArgs{ | ||
{1, []byte("original data")}, | ||
{1, []byte("overwritten data")}, | ||
}, | ||
queryHeight: 1, | ||
expected: []byte("overwritten data"), | ||
}, | ||
{ | ||
name: "put and get nil data", | ||
puts: []putArgs{ | ||
{1, nil}, | ||
}, | ||
queryHeight: 1, | ||
expected: nil, | ||
}, | ||
{ | ||
name: "put and get empty data", | ||
puts: []putArgs{ | ||
{1, []byte{}}, | ||
}, | ||
queryHeight: 1, | ||
expected: nil, | ||
DracoLi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
}, | ||
{ | ||
name: "put and get large data", | ||
puts: []putArgs{ | ||
{1, make([]byte, 1000)}, | ||
}, | ||
queryHeight: 1, | ||
expected: make([]byte, 1000), | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
db := newDB() | ||
defer db.Close() | ||
DracoLi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
// Perform all puts | ||
for _, write := range tt.puts { | ||
require.NoError(t, db.Put(write.height, write.data)) | ||
} | ||
|
||
// Query the specific height | ||
retrievedData, err := db.Get(tt.queryHeight) | ||
if tt.expectedErr != nil { | ||
require.ErrorIs(t, err, tt.expectedErr) | ||
} else { | ||
require.NoError(t, err) | ||
require.Equal(t, tt.expected, retrievedData) | ||
} | ||
DracoLi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
}) | ||
} | ||
} | ||
|
||
func TestHas(t *testing.T, newDB func() database.HeightIndex) { | ||
tests := []struct { | ||
name string | ||
puts []putArgs | ||
queryHeight uint64 | ||
expected bool | ||
}{ | ||
{ | ||
name: "non-existent item", | ||
puts: []putArgs{}, | ||
DracoLi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
queryHeight: 1, | ||
expected: false, | ||
DracoLi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
}, | ||
{ | ||
name: "existing item with data", | ||
puts: []putArgs{{1, []byte("test data")}}, | ||
queryHeight: 1, | ||
expected: true, | ||
}, | ||
{ | ||
name: "existing item with nil data", | ||
puts: []putArgs{{1, nil}}, | ||
queryHeight: 1, | ||
expected: true, | ||
}, | ||
{ | ||
name: "existing item with empty bytes", | ||
puts: []putArgs{{1, []byte{}}}, | ||
queryHeight: 1, | ||
expected: true, | ||
}, | ||
{ | ||
name: "has returns true on overridden height", | ||
puts: []putArgs{ | ||
{1, []byte("original data")}, | ||
{1, []byte("overridden data")}, | ||
}, | ||
queryHeight: 1, | ||
expected: true, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
db := newDB() | ||
defer db.Close() | ||
|
||
// Perform all puts | ||
for _, write := range tt.puts { | ||
require.NoError(t, db.Put(write.height, write.data)) | ||
} | ||
|
||
exists, err := db.Has(tt.queryHeight) | ||
DracoLi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
require.NoError(t, err) | ||
require.Equal(t, tt.expected, exists) | ||
}) | ||
} | ||
} | ||
|
||
func TestCloseAndPut(t *testing.T, newDB func() database.HeightIndex) { | ||
db := newDB() | ||
require.NoError(t, db.Close()) | ||
|
||
// Try to put after close - should return error | ||
err := db.Put(1, []byte("test")) | ||
require.ErrorIs(t, err, database.ErrClosed) | ||
} | ||
|
||
func TestCloseAndGet(t *testing.T, newDB func() database.HeightIndex) { | ||
db := newDB() | ||
require.NoError(t, db.Close()) | ||
|
||
// Try to get after close - should return error | ||
_, err := db.Get(1) | ||
require.ErrorIs(t, err, database.ErrClosed) | ||
} | ||
|
||
func TestCloseAndHas(t *testing.T, newDB func() database.HeightIndex) { | ||
db := newDB() | ||
require.NoError(t, db.Close()) | ||
|
||
// Try to has after close - should return error | ||
_, err := db.Has(1) | ||
require.ErrorIs(t, err, database.ErrClosed) | ||
} | ||
|
||
func TestModifyValueAfterPut(t *testing.T, newDB func() database.HeightIndex) { | ||
db := newDB() | ||
defer db.Close() | ||
|
||
originalData := []byte("original data") | ||
modifiedData := []byte("modified data") | ||
|
||
// Put original data | ||
require.NoError(t, db.Put(1, originalData)) | ||
|
||
// Modify the original data slice | ||
copy(originalData, modifiedData) | ||
|
||
// Get the data back - should still be the original data, not the modified slice | ||
retrievedData, err := db.Get(1) | ||
DracoLi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
require.NoError(t, err) | ||
require.Equal(t, []byte("original data"), retrievedData) | ||
require.NotEqual(t, modifiedData, retrievedData) | ||
} | ||
|
||
func TestModifyValueAfterGet(t *testing.T, newDB func() database.HeightIndex) { | ||
DracoLi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
db := newDB() | ||
defer db.Close() | ||
|
||
originalData := []byte("original data") | ||
|
||
// Put original data | ||
require.NoError(t, db.Put(1, originalData)) | ||
|
||
// Get the data | ||
retrievedData, err := db.Get(1) | ||
require.NoError(t, err) | ||
require.Equal(t, originalData, retrievedData) | ||
|
||
// Modify the retrieved data | ||
copy(retrievedData, []byte("modified data")) | ||
|
||
// Get the data again - should still be the original data | ||
retrievedData2, err := db.Get(1) | ||
require.NoError(t, err) | ||
require.Equal(t, originalData, retrievedData2) | ||
require.NotEqual(t, retrievedData, retrievedData2) | ||
} | ||
|
||
func TestClose(t *testing.T, newDB func() database.HeightIndex) { | ||
db := newDB() | ||
require.NoError(t, db.Close()) | ||
|
||
// Second close should return error | ||
err := db.Close() | ||
require.ErrorIs(t, err, database.ErrClosed) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. | ||
// See the file LICENSE for licensing terms. | ||
|
||
package memdb | ||
|
||
import ( | ||
"slices" | ||
"sync" | ||
|
||
"github.com/ava-labs/avalanchego/database" | ||
) | ||
|
||
var _ database.HeightIndex = (*Database)(nil) | ||
|
||
// Database is an in-memory implementation of database.HeightIndex | ||
type Database struct { | ||
mu sync.RWMutex | ||
data map[uint64][]byte | ||
closed bool | ||
} | ||
|
||
func New() *Database { | ||
return &Database{ | ||
data: make(map[uint64][]byte), | ||
} | ||
} | ||
DracoLi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
// Put stores data in memory at the given height | ||
DracoLi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
func (db *Database) Put(height uint64, data []byte) error { | ||
db.mu.Lock() | ||
defer db.mu.Unlock() | ||
|
||
if db.closed { | ||
return database.ErrClosed | ||
} | ||
|
||
if db.data == nil { | ||
db.data = make(map[uint64][]byte) | ||
} | ||
|
||
if len(data) == 0 { | ||
// don't save empty slice if data is nil or empty | ||
db.data[height] = nil | ||
} else { | ||
db.data[height] = slices.Clone(data) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Get retrieves data at the given height | ||
func (db *Database) Get(height uint64) ([]byte, error) { | ||
db.mu.RLock() | ||
defer db.mu.RUnlock() | ||
|
||
if db.closed { | ||
return nil, database.ErrClosed | ||
} | ||
|
||
data, ok := db.data[height] | ||
if !ok { | ||
return nil, database.ErrNotFound | ||
} | ||
|
||
// don't return empty slice if data is nil or empty | ||
if data == nil { | ||
return nil, nil | ||
} | ||
DracoLi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
return slices.Clone(data), nil | ||
} | ||
|
||
// Has checks if data exists at the given height | ||
func (db *Database) Has(height uint64) (bool, error) { | ||
db.mu.RLock() | ||
defer db.mu.RUnlock() | ||
|
||
if db.closed { | ||
return false, database.ErrClosed | ||
} | ||
|
||
_, ok := db.data[height] | ||
return ok, nil | ||
} | ||
|
||
// Close closes the in-memory database | ||
func (db *Database) Close() error { | ||
db.mu.Lock() | ||
defer db.mu.Unlock() | ||
|
||
if db.closed { | ||
return database.ErrClosed | ||
} | ||
|
||
db.closed = true | ||
db.data = nil | ||
return nil | ||
} |
Uh oh!
There was an error while loading. Please reload this page.