- Add db.go with DB struct: Open/Close/Put/Delete/Get/GetDurableSequence/IsWriteStopped - Add db_test.go with lifecycle, put/get, delete, recovery, and corruption tests - Extract sentinel errors to errkit/ leaf package to break import cycle (wal -> go_kv) - Update wal/sequence.go, wal/writer.go to import errkit instead of root package - Root errors.go re-exports from errkit for backward compatibility Phase 1 Wave 4d complete (T19).
161 lines
3.8 KiB
Go
161 lines
3.8 KiB
Go
package go_kv
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/dailz/go-kv/config"
|
|
"github.com/dailz/go-kv/memtable"
|
|
"github.com/dailz/go-kv/wal"
|
|
)
|
|
|
|
// DB is the public database handle for the storage engine.
|
|
type DB struct {
|
|
cfg *config.WalConfig
|
|
dir string
|
|
writer *wal.WalWriter
|
|
recoveryMem *memtable.MemTable
|
|
closed atomic.Bool
|
|
}
|
|
|
|
// Open opens a database directory, recovers durable WAL entries, and starts a
|
|
// fresh WAL writer at the next recovered segment and sequence.
|
|
func Open(dir string, cfg *config.WalConfig) (*DB, error) {
|
|
if cfg == nil {
|
|
defaults := config.Defaults()
|
|
cfg = &defaults
|
|
}
|
|
if err := cfg.Validate(); err != nil {
|
|
return nil, fmt.Errorf("db: validate config: %w", err)
|
|
}
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return nil, fmt.Errorf("db: create directory: %w", err)
|
|
}
|
|
|
|
recoveryMem := memtable.NewMemTable(cfg.MemTableSize)
|
|
replayer := &memTableReplayer{mem: recoveryMem}
|
|
result, err := wal.Recover(dir, replayer)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("db: recover WAL: %w", err)
|
|
}
|
|
if err := replayer.Err(); err != nil {
|
|
return nil, fmt.Errorf("db: replay WAL: %w", err)
|
|
}
|
|
recoveryMem.Publish(^uint64(0))
|
|
|
|
writer, err := wal.NewWalWriter(cfg, dir, result.NextSegmentID, result.NextSequence)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("db: create WAL writer: %w", err)
|
|
}
|
|
|
|
return &DB{
|
|
cfg: cfg,
|
|
dir: dir,
|
|
writer: writer,
|
|
recoveryMem: recoveryMem,
|
|
}, nil
|
|
}
|
|
|
|
// Close drains outstanding writes and releases database resources.
|
|
func (db *DB) Close() error {
|
|
if db.closed.Swap(true) {
|
|
return nil
|
|
}
|
|
if err := db.writer.Close(); err != nil {
|
|
return fmt.Errorf("db: close WAL writer: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Put stores key with value.
|
|
func (db *DB) Put(key, value []byte) error {
|
|
if db.closed.Load() {
|
|
return ErrWriteStopped
|
|
}
|
|
if err := db.writer.Put(key, value); err != nil {
|
|
return fmt.Errorf("db: put: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Delete removes key.
|
|
func (db *DB) Delete(key []byte) error {
|
|
if db.closed.Load() {
|
|
return ErrWriteStopped
|
|
}
|
|
if err := db.writer.Delete(key); err != nil {
|
|
return fmt.Errorf("db: delete: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Get returns the newest visible value for key across recovered and new writes.
|
|
func (db *DB) Get(key []byte) *memtable.GetResult {
|
|
recovered := db.recoveryMem.Get(key)
|
|
current := db.writer.Get(key)
|
|
|
|
if current.Found || current.Sequence > recovered.Sequence {
|
|
return current
|
|
}
|
|
if !recovered.Found {
|
|
return &memtable.GetResult{Found: false}
|
|
}
|
|
if recovered.Value == nil {
|
|
return &memtable.GetResult{Found: false, Sequence: recovered.Sequence}
|
|
}
|
|
return recovered
|
|
}
|
|
|
|
// GetDurableSequence returns the highest sequence durably fsynced to WAL.
|
|
func (db *DB) GetDurableSequence() uint64 {
|
|
return db.writer.GetDurableSequence()
|
|
}
|
|
|
|
// IsWriteStopped reports whether the DB rejects new writes.
|
|
func (db *DB) IsWriteStopped() bool {
|
|
return db.closed.Load() || db.writer.IsWriteStopped()
|
|
}
|
|
|
|
type memTableReplayer struct {
|
|
mem *memtable.MemTable
|
|
mu sync.Mutex
|
|
err error
|
|
}
|
|
|
|
func (r *memTableReplayer) ReplayPut(key, value []byte, sequence uint64) {
|
|
if err := r.mem.PutPending(cloneBytes(key), cloneBytes(value), sequence); err != nil {
|
|
r.setErr(fmt.Errorf("put pending: %w", err))
|
|
}
|
|
}
|
|
|
|
func (r *memTableReplayer) ReplayDelete(key []byte, sequence uint64) {
|
|
if err := r.mem.DeletePending(cloneBytes(key), sequence); err != nil {
|
|
r.setErr(fmt.Errorf("delete pending: %w", err))
|
|
}
|
|
}
|
|
|
|
func (r *memTableReplayer) Err() error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
return r.err
|
|
}
|
|
|
|
func (r *memTableReplayer) setErr(err error) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.err == nil {
|
|
r.err = err
|
|
}
|
|
}
|
|
|
|
func cloneBytes(src []byte) []byte {
|
|
if src == nil {
|
|
return nil
|
|
}
|
|
dst := make([]byte, len(src))
|
|
copy(dst, src)
|
|
return dst
|
|
}
|