Files
dailz 0fe1530e25 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).
2026-06-12 14:10:56 +08:00

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
}