feat: integrate DB with Open/Close/Put/Delete/Get
- 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).
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user