Skip to content
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

Rkuris/cache read strategy expose to ffi #791

Merged
merged 6 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions ffi/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
A firewood golang interface
# A firewood golang interface

This allows calling into firewood from golang

# Building
## Building

just run make. This builds the dbtest.go executable
First, build the release version (`cargo build --release`). This creates the ffi
interface file "firewood.h" as a side effect.

Then, you can run the tests in go, using `go test .`
119 changes: 98 additions & 21 deletions ffi/firewood.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,112 @@ package firewood
import "C"
import (
"runtime"
"strconv"
"unsafe"
)

const (
// The size of the node cache for firewood
NodeCache = 1000000
// The number of revisions to keep (must be >=2)
Revisions = 100
)

// Firewood is a handle to a Firewood database
type Firewood struct {
Db *C.void
db *C.void
}

// CreateDatabase creates a new Firewood database at the given path.
// Returns a handle that can be used for subsequent database operations.
func CreateDatabase(path string) Firewood {
db := C.fwd_create_db(C.CString(path), C.size_t(NodeCache), C.size_t(Revisions))
ptr := (*C.void)(db)
return Firewood{Db: ptr}
// openConfig is used to configure the database at open time
type openConfig struct {
path string
nodeCacheEntries uintptr
revisions uintptr
readCacheStrategy int8
create bool
}

// OpenOption is a function that configures the database at open time
type OpenOption func(*openConfig)

// WithPath sets the path for the database
func WithPath(path string) OpenOption {
return func(o *openConfig) {
o.path = path
}
}

// WithNodeCacheEntries sets the number of node cache entries
func WithNodeCacheEntries(entries uintptr) OpenOption {
if entries < 1 {
panic("Node cache entries must be >= 1")
}
return func(o *openConfig) {
o.nodeCacheEntries = entries
}
}

// WithRevisions sets the number of revisions to keep
func WithRevisions(revisions uintptr) OpenOption {
if revisions < 2 {
panic("Revisions must be >= 2")
}
return func(o *openConfig) {
o.revisions = revisions
}
}

func OpenDatabase(path string) Firewood {
db := C.fwd_open_db(C.CString(path), C.size_t(NodeCache), C.size_t(Revisions))
// WithReadCacheStrategy sets the read cache strategy
// 0: Only writes are cached
// 1: Branch reads are cached
// 2: All reads are cached
func WithReadCacheStrategy(strategy int8) OpenOption {
if (strategy < 0) || (strategy > 2) {
panic("Invalid read cache strategy " + strconv.Itoa(int(strategy)))
}
return func(o *openConfig) {
o.readCacheStrategy = strategy
}
}

// WithCreate sets whether to create a new database
// If false, the database will be opened
// If true, the database will be created
func WithCreate(create bool) OpenOption {
return func(o *openConfig) {
o.create = create
}
}

// NewDatabase opens or creates a new Firewood database with the given options.
// Returns a handle that can be used for subsequent database operations.
func NewDatabase(options ...OpenOption) Firewood {
opts := &openConfig{
nodeCacheEntries: 1_000_000,
revisions: 100,
readCacheStrategy: 0,
path: "firewood.db",
}

for _, opt := range options {
opt(opts)
}
var db unsafe.Pointer
if opts.create {
db = C.fwd_create_db(C.CString(opts.path), C.size_t(opts.nodeCacheEntries), C.size_t(opts.revisions), C.uint8_t(opts.readCacheStrategy))
} else {
db = C.fwd_open_db(C.CString(opts.path), C.size_t(opts.nodeCacheEntries), C.size_t(opts.revisions), C.uint8_t(opts.readCacheStrategy))
}

ptr := (*C.void)(db)
return Firewood{Db: ptr}
return Firewood{db: ptr}
}

// KeyValue is a key-value pair
type KeyValue struct {
Key []byte
Value []byte
}

// Apply a batch of updates to the database
// Returns the hash of the root node after the batch is applied
// Note that if the Value is empty, the key will be deleted as a prefix
// delete (that is, all children will be deleted)
// WARNING: Calling it with an empty key and value will delete the entire database

func (f *Firewood) Batch(ops []KeyValue) []byte {
var pin runtime.Pinner
defer pin.Unpin()
Expand All @@ -51,25 +124,27 @@ func (f *Firewood) Batch(ops []KeyValue) []byte {
}
}
ptr := (*C.struct_KeyValue)(unsafe.Pointer(&ffi_ops[0]))
hash := C.fwd_batch(unsafe.Pointer(f.Db), C.size_t(len(ops)), ptr)
hash := C.fwd_batch(unsafe.Pointer(f.db), C.size_t(len(ops)), ptr)
hash_bytes := C.GoBytes(unsafe.Pointer(hash.data), C.int(hash.len))
C.fwd_free_value(&hash)
return hash_bytes
}

// Get retrieves the value for the given key.
func (f *Firewood) Get(input_key []byte) ([]byte, error) {
var pin runtime.Pinner
defer pin.Unpin()
ffi_key := make_value(&pin, input_key)

value := C.fwd_get(unsafe.Pointer(f.Db), ffi_key)
value := C.fwd_get(unsafe.Pointer(f.db), ffi_key)
ffi_bytes := C.GoBytes(unsafe.Pointer(value.data), C.int(value.len))
C.fwd_free_value(&value)
if len(ffi_bytes) == 0 {
return nil, nil
}
return ffi_bytes, nil
}

func make_value(pin *runtime.Pinner, data []byte) C.struct_Value {
if len(data) == 0 {
return C.struct_Value{0, nil}
Expand All @@ -79,14 +154,16 @@ func make_value(pin *runtime.Pinner, data []byte) C.struct_Value {
return C.struct_Value{C.size_t(len(data)), ptr}
}

// Root returns the current root hash of the trie.
func (f *Firewood) Root() []byte {
hash := C.fwd_root_hash(unsafe.Pointer(f.Db))
hash := C.fwd_root_hash(unsafe.Pointer(f.db))
hash_bytes := C.GoBytes(unsafe.Pointer(hash.data), C.int(hash.len))
C.fwd_free_value(&hash)
return hash_bytes
}

// Close closes the database and releases all held resources.
func (f *Firewood) Close() error {
C.fwd_close_db(unsafe.Pointer(f.Db))
C.fwd_close_db(unsafe.Pointer(f.db))
return nil
}
9 changes: 6 additions & 3 deletions ffi/firewood.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ struct Value fwd_batch(void *db,
void fwd_close_db(void *db);

/**
* Create a database with the given cache size and maximum number of revisions
* Create a database with the given cache size and maximum number of revisions, as well
* as a specific cache strategy
*
* # Arguments
*
Expand All @@ -76,7 +77,8 @@ void fwd_close_db(void *db);
*/
void *fwd_create_db(const char *path,
size_t cache_size,
size_t revisions);
size_t revisions,
uint8_t strategy);

/**
* Frees the memory associated with a `Value`.
Expand Down Expand Up @@ -127,7 +129,8 @@ struct Value fwd_get(void *db, struct Value key);
*/
void *fwd_open_db(const char *path,
size_t cache_size,
size_t revisions);
size_t revisions,
uint8_t strategy);

/**
* Get the root hash of the latest version of the database
Expand Down
8 changes: 4 additions & 4 deletions ffi/firewood_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
)

func TestInsert(t *testing.T) {
var f Firewood = CreateDatabase("test.db")
var f Firewood = NewDatabase(WithCreate(true), WithPath("test.db"))
defer os.Remove("test.db")
defer f.Close()
f.Batch([]KeyValue{
Expand All @@ -21,7 +21,7 @@ func TestInsert(t *testing.T) {
}

func TestInsert100(t *testing.T) {
var f Firewood = CreateDatabase("test.db")
var f Firewood = NewDatabase(WithCreate(true), WithPath("test.db"))
defer os.Remove("test.db")
defer f.Close()
ops := make([]KeyValue, 100)
Expand Down Expand Up @@ -57,7 +57,7 @@ func TestInsert100(t *testing.T) {

func TestRangeDelete(t *testing.T) {
const N = 100
var f Firewood = CreateDatabase("test.db")
var f Firewood = NewDatabase(WithCreate(true), WithPath("test.db"))
defer os.Remove("test.db")
defer f.Close()
ops := make([]KeyValue, N)
Expand All @@ -84,7 +84,7 @@ func TestRangeDelete(t *testing.T) {
}

func TestInvariants(t *testing.T) {
var f Firewood = CreateDatabase("test.db")
var f Firewood = NewDatabase(WithCreate(true), WithPath("test.db"))
defer os.Remove("test.db")
defer f.Close()

Expand Down
10 changes: 5 additions & 5 deletions ffi/kvbackend.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ package firewood
// this is used for some of the firewood performance tests

// Validate that Firewood implements the KVBackend interface
var _ KVBackend = (*Firewood)(nil)
var _ kVBackend = (*Firewood)(nil)

// Copy of KVBackend from ava-labs/avalanchego
type KVBackend interface {
type kVBackend interface {
// Returns the current root hash of the trie.
// Empty trie must return common.Hash{}.
// Length of the returned slice must be common.HashLength.
Expand Down Expand Up @@ -46,12 +45,13 @@ func (f *Firewood) Prefetch(key []byte) ([]byte, error) {
return nil, nil
}

// / Commit does nothing, since update already persists changes
// Commit does nothing, since update already persists changes
func (f *Firewood) Commit(root []byte) error {
return nil
}

// Update could use some more work, but for now we just batch the keys and values
// Update batches all the keys and values and applies them to the
// database
func (f *Firewood) Update(keys, vals [][]byte) ([]byte, error) {
// batch the keys and values
ops := make([]KeyValue, len(keys))
Expand Down
20 changes: 15 additions & 5 deletions ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::os::unix::ffi::OsStrExt as _;
use std::path::Path;

use firewood::db::{BatchOp as DbBatchOp, Db, DbConfig, DbViewSync as _};
use firewood::manager::RevisionManagerConfig;
use firewood::manager::{CacheReadStrategy, RevisionManagerConfig};

#[derive(Debug)]
#[repr(C)]
Expand Down Expand Up @@ -160,7 +160,8 @@ pub unsafe extern "C" fn fwd_free_value(value: *const Value) {
drop(recreated_box);
}

/// Create a database with the given cache size and maximum number of revisions
/// Create a database with the given cache size and maximum number of revisions, as well
/// as a specific cache strategy
///
/// # Arguments
///
Expand All @@ -184,10 +185,11 @@ pub unsafe extern "C" fn fwd_create_db(
path: *const std::ffi::c_char,
cache_size: usize,
revisions: usize,
strategy: u8,
) -> *mut Db {
let cfg = DbConfig::builder()
.truncate(true)
.manager(manager_config(cache_size, revisions))
.manager(manager_config(cache_size, revisions, strategy))
.build();
common_create(path, cfg)
}
Expand Down Expand Up @@ -216,10 +218,11 @@ pub unsafe extern "C" fn fwd_open_db(
path: *const std::ffi::c_char,
cache_size: usize,
revisions: usize,
strategy: u8,
) -> *mut Db {
let cfg = DbConfig::builder()
.truncate(false)
.manager(manager_config(cache_size, revisions))
.manager(manager_config(cache_size, revisions, strategy))
.build();
common_create(path, cfg)
}
Expand All @@ -232,14 +235,21 @@ unsafe fn common_create(path: *const std::ffi::c_char, cfg: DbConfig) -> *mut Db
))
}

fn manager_config(cache_size: usize, revisions: usize) -> RevisionManagerConfig {
fn manager_config(cache_size: usize, revisions: usize, strategy: u8) -> RevisionManagerConfig {
let cache_read_strategy = match strategy {
0 => CacheReadStrategy::WritesOnly,
1 => CacheReadStrategy::BranchReads,
2 => CacheReadStrategy::All,
_ => panic!("invalid cache strategy"),
};
RevisionManagerConfig::builder()
.node_cache_size(
cache_size
.try_into()
.expect("cache size should always be non-zero"),
)
.max_revisions(revisions)
.cache_read_strategy(cache_read_strategy)
.build()
}

Expand Down