mirror of
https://github.com/luxfi/database.git
synced 2026-10-30 15:01:18 +00:00
383 lines
10 KiB
Go
383 lines
10 KiB
Go
// Copyright (C) 2020-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
//go:build pebbledb
|
|
|
|
package pebbledb
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"runtime"
|
|
"sync"
|
|
|
|
"github.com/cockroachdb/pebble"
|
|
"github.com/cockroachdb/pebble/bloom"
|
|
database "github.com/luxfi/database"
|
|
"github.com/luxfi/metric"
|
|
)
|
|
|
|
const (
|
|
// pebbleByteOverHead is the number of bytes of constant overhead that
|
|
// should be added to a batch size per operation.
|
|
pebbleByteOverHead = 8
|
|
)
|
|
|
|
var _ database.Database = (*Database)(nil)
|
|
|
|
// Database is a persistent key-value store implementing the Database interface.
|
|
// Apart from basic data storage functionality it also supports batch writes and
|
|
// iterating over the keyspace in binary-alphabetical order.
|
|
type Database struct {
|
|
lock sync.RWMutex
|
|
pebbleDB *pebble.DB
|
|
closed bool
|
|
openIterators set[*iter]
|
|
writeOptions *pebble.WriteOptions
|
|
|
|
// Metrics
|
|
compTimeMeter metric.Summary
|
|
compReadMeter metric.Counter
|
|
compWriteMeter metric.Counter
|
|
writeDelayNMeter metric.Summary
|
|
writeDelayMeter metric.Summary
|
|
diskSizeGauge metric.Gauge
|
|
diskReadMeter metric.Counter
|
|
diskWriteMeter metric.Counter
|
|
memCompGauge metric.Gauge
|
|
level0CompGauge metric.Gauge
|
|
nonlevel0CompGauge metric.Gauge
|
|
seekCompGauge metric.Gauge
|
|
}
|
|
|
|
// New returns a new PebbleDB database.
|
|
func New(path string, cacheSize int, handles int, namespace string, readonly bool) (*Database, error) {
|
|
// Set default options
|
|
opts := &pebble.Options{
|
|
Cache: pebble.NewCache(int64(cacheSize * 1024 * 1024)),
|
|
DisableWAL: false,
|
|
MaxOpenFiles: handles,
|
|
MaxConcurrentCompactions: runtime.NumCPU,
|
|
L0CompactionThreshold: 2,
|
|
L0StopWritesThreshold: 1000,
|
|
LBaseMaxBytes: 64 * 1024 * 1024, // 64 MB
|
|
MaxManifestFileSize: 128 * 1024 * 1024, // 128 MB
|
|
MemTableSize: 4 * 1024 * 1024, // 4 MB
|
|
MemTableStopWritesThreshold: 2,
|
|
ReadOnly: readonly,
|
|
}
|
|
|
|
// Configure bloom filters
|
|
opts.Levels = make([]pebble.LevelOptions, 7)
|
|
for i := 0; i < len(opts.Levels); i++ {
|
|
opts.Levels[i].BlockSize = 32 * 1024 // 32 KB
|
|
opts.Levels[i].IndexBlockSize = 256 * 1024 // 256 KB
|
|
opts.Levels[i].FilterPolicy = bloom.FilterPolicy(10)
|
|
opts.Levels[i].FilterType = pebble.TableFilter
|
|
if i > 0 {
|
|
opts.Levels[i].TargetFileSize = opts.Levels[i-1].TargetFileSize * 2
|
|
}
|
|
}
|
|
opts.Levels[0].TargetFileSize = 2 * 1024 * 1024 // 2 MB
|
|
|
|
// Open database
|
|
db, err := pebble.Open(path, opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create database wrapper
|
|
database := &Database{
|
|
pebbleDB: db,
|
|
openIterators: make(set[*iter]),
|
|
writeOptions: &pebble.WriteOptions{Sync: true},
|
|
}
|
|
|
|
// Configure metrics if namespace is provided
|
|
if namespace != "" {
|
|
database.compTimeMeter = metric.NewSummary(metric.SummaryOpts{
|
|
Namespace: namespace,
|
|
Subsystem: "pebbledb",
|
|
Name: "compact_time",
|
|
Help: "Time spent in compaction",
|
|
})
|
|
database.compReadMeter = metric.NewCounter(metric.CounterOpts{
|
|
Namespace: namespace,
|
|
Subsystem: "pebbledb",
|
|
Name: "compact_read",
|
|
Help: "Bytes read during compaction",
|
|
})
|
|
database.compWriteMeter = metric.NewCounter(metric.CounterOpts{
|
|
Namespace: namespace,
|
|
Subsystem: "pebbledb",
|
|
Name: "compact_write",
|
|
Help: "Bytes written during compaction",
|
|
})
|
|
database.diskSizeGauge = metric.NewGauge(metric.GaugeOpts{
|
|
Namespace: namespace,
|
|
Subsystem: "pebbledb",
|
|
Name: "disk_size",
|
|
Help: "Size of the database on disk",
|
|
})
|
|
database.diskReadMeter = metric.NewCounter(metric.CounterOpts{
|
|
Namespace: namespace,
|
|
Subsystem: "pebbledb",
|
|
Name: "disk_read",
|
|
Help: "Bytes read from disk",
|
|
})
|
|
database.diskWriteMeter = metric.NewCounter(metric.CounterOpts{
|
|
Namespace: namespace,
|
|
Subsystem: "pebbledb",
|
|
Name: "disk_write",
|
|
Help: "Bytes written to disk",
|
|
})
|
|
}
|
|
|
|
return database, nil
|
|
}
|
|
|
|
// Close stops the metrics collection, flushes any pending data to disk and closes
|
|
// all io accesses to the underlying key-value store.
|
|
func (d *Database) Close() error {
|
|
if d == nil {
|
|
return nil
|
|
}
|
|
|
|
d.lock.Lock()
|
|
defer d.lock.Unlock()
|
|
|
|
if d.closed {
|
|
return database.ErrClosed
|
|
}
|
|
|
|
d.closed = true
|
|
|
|
for iter := range d.openIterators {
|
|
iter.lock.Lock()
|
|
iter.release()
|
|
iter.lock.Unlock()
|
|
}
|
|
d.openIterators.Clear()
|
|
|
|
return updateError(d.pebbleDB.Close())
|
|
}
|
|
|
|
// HealthCheck returns nil if the database is healthy, or an error otherwise.
|
|
func (d *Database) HealthCheck(ctx context.Context) (interface{}, error) {
|
|
d.lock.RLock()
|
|
defer d.lock.RUnlock()
|
|
|
|
if d.closed {
|
|
return nil, database.ErrClosed
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (d *Database) Backup(io.Writer, uint64) (uint64, error) {
|
|
return 0, errors.New("backup not supported for pebbledb")
|
|
}
|
|
|
|
func (d *Database) Load(io.Reader) error {
|
|
return errors.New("load not supported for pebbledb")
|
|
}
|
|
|
|
// Has retrieves if a key is present in the key-value store.
|
|
func (d *Database) Has(key []byte) (bool, error) {
|
|
d.lock.RLock()
|
|
defer d.lock.RUnlock()
|
|
|
|
if d.closed {
|
|
return false, database.ErrClosed
|
|
}
|
|
|
|
_, closer, err := d.pebbleDB.Get(key)
|
|
if err == pebble.ErrNotFound {
|
|
return false, nil
|
|
}
|
|
if err != nil {
|
|
return false, updateError(err)
|
|
}
|
|
return true, closer.Close()
|
|
}
|
|
|
|
// Get retrieves the given key if it's present in the key-value store.
|
|
func (d *Database) Get(key []byte) ([]byte, error) {
|
|
d.lock.RLock()
|
|
defer d.lock.RUnlock()
|
|
|
|
if d.closed {
|
|
return nil, database.ErrClosed
|
|
}
|
|
|
|
data, closer, err := d.pebbleDB.Get(key)
|
|
if err != nil {
|
|
return nil, updateError(err)
|
|
}
|
|
ret := make([]byte, len(data))
|
|
copy(ret, data)
|
|
return ret, closer.Close()
|
|
}
|
|
|
|
// Put inserts the given value into the key-value store.
|
|
func (d *Database) Put(key []byte, value []byte) error {
|
|
d.lock.RLock()
|
|
defer d.lock.RUnlock()
|
|
|
|
if d.closed {
|
|
return database.ErrClosed
|
|
}
|
|
|
|
return updateError(d.pebbleDB.Set(key, value, d.writeOptions))
|
|
}
|
|
|
|
// Delete removes the key from the key-value store.
|
|
func (d *Database) Delete(key []byte) error {
|
|
d.lock.RLock()
|
|
defer d.lock.RUnlock()
|
|
|
|
if d.closed {
|
|
return database.ErrClosed
|
|
}
|
|
|
|
return updateError(d.pebbleDB.Delete(key, d.writeOptions))
|
|
}
|
|
|
|
// NewIterator creates a binary-alphabetical iterator over a subset
|
|
// of database content with a particular key prefix, starting at a particular
|
|
// initial key (or after, if it does not exist).
|
|
func (d *Database) NewIterator() database.Iterator {
|
|
return d.NewIteratorWithPrefix(nil)
|
|
}
|
|
|
|
// NewIteratorWithStart creates a binary-alphabetical iterator over a subset of
|
|
// database content starting at a particular initial key (or after, if it does
|
|
// not exist).
|
|
func (d *Database) NewIteratorWithStart(start []byte) database.Iterator {
|
|
return d.NewIteratorWithStartAndPrefix(start, nil)
|
|
}
|
|
|
|
// NewIteratorWithPrefix creates a binary-alphabetical iterator over a subset
|
|
// of database content with a particular key prefix.
|
|
func (d *Database) NewIteratorWithPrefix(prefix []byte) database.Iterator {
|
|
return d.NewIteratorWithStartAndPrefix(nil, prefix)
|
|
}
|
|
|
|
// NewIteratorWithStartAndPrefix creates a binary-alphabetical iterator over a
|
|
// subset of database content with a particular key prefix, starting at a
|
|
// particular initial key (or after, if it does not exist).
|
|
func (d *Database) NewIteratorWithStartAndPrefix(start, prefix []byte) database.Iterator {
|
|
d.lock.Lock()
|
|
defer d.lock.Unlock()
|
|
|
|
if d.closed {
|
|
return &iter{
|
|
db: d,
|
|
closed: true,
|
|
err: database.ErrClosed,
|
|
}
|
|
}
|
|
|
|
it, err := d.pebbleDB.NewIter(keyRange(start, prefix))
|
|
if err != nil {
|
|
return &iter{
|
|
db: d,
|
|
closed: true,
|
|
err: updateError(err),
|
|
}
|
|
}
|
|
|
|
iterator := &iter{
|
|
db: d,
|
|
iter: it,
|
|
}
|
|
d.openIterators.Add(iterator)
|
|
return iterator
|
|
}
|
|
|
|
func keyRange(start, prefix []byte) *pebble.IterOptions {
|
|
opt := &pebble.IterOptions{
|
|
LowerBound: prefix,
|
|
UpperBound: prefixToUpperBound(prefix),
|
|
}
|
|
if pebble.DefaultComparer.Compare(start, prefix) == 1 {
|
|
opt.LowerBound = start
|
|
}
|
|
return opt
|
|
}
|
|
|
|
// Returns an upper bound that stops after all keys with the given [prefix].
|
|
// Assumes the Database uses bytes.Compare for key comparison and not a custom
|
|
// comparer.
|
|
func prefixToUpperBound(prefix []byte) []byte {
|
|
for i := len(prefix) - 1; i >= 0; i-- {
|
|
if prefix[i] != 0xFF {
|
|
upperBound := make([]byte, i+1)
|
|
copy(upperBound, prefix)
|
|
upperBound[i]++
|
|
return upperBound
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Compact flattens the underlying data store for the given key range. In essence,
|
|
// deleted and overwritten versions are discarded, and the data is rearranged to
|
|
// reduce the cost of operations needed to access them.
|
|
//
|
|
// A nil start is treated as a key before all keys in the data store; a nil limit
|
|
// is treated as a key after all keys in the data store. If both are nil then it
|
|
// will compact entire data store.
|
|
func (d *Database) Compact(start []byte, end []byte) error {
|
|
d.lock.RLock()
|
|
defer d.lock.RUnlock()
|
|
|
|
if d.closed {
|
|
return database.ErrClosed
|
|
}
|
|
|
|
if end == nil {
|
|
// The database.Database spec treats a nil [limit] as a key after all
|
|
// keys but pebble treats a nil [limit] as a key before all keys in
|
|
// Compact. Use the greatest key in the database as the [limit] to get
|
|
// the desired behavior.
|
|
it, err := d.pebbleDB.NewIter(&pebble.IterOptions{})
|
|
if err != nil {
|
|
return updateError(err)
|
|
}
|
|
|
|
if !it.Last() {
|
|
// The database is empty.
|
|
return it.Close()
|
|
}
|
|
|
|
end = make([]byte, len(it.Key()))
|
|
copy(end, it.Key())
|
|
if err := it.Close(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if pebble.DefaultComparer.Compare(start, end) >= 1 {
|
|
// pebble requires [start] < [end]
|
|
return nil
|
|
}
|
|
|
|
return updateError(d.pebbleDB.Compact(start, end, true /* parallelize */))
|
|
}
|
|
|
|
// Sync implements the database.Syncer interface
|
|
// It flushes all buffered writes to persistent storage.
|
|
func (d *Database) Sync() error {
|
|
d.lock.RLock()
|
|
defer d.lock.RUnlock()
|
|
|
|
if d.closed {
|
|
return database.ErrClosed
|
|
}
|
|
|
|
// Pebble's LogData with Sync option forces a WAL sync
|
|
// This ensures all written data is persisted to disk
|
|
return updateError(d.pebbleDB.LogData(nil, pebble.Sync))
|
|
}
|