Skip to content

Commit 25603f8

Browse files
committed
Refactor db.Get to scan sstables after memtable key miss
1 parent d0c1c64 commit 25603f8

5 files changed

Lines changed: 1072 additions & 22 deletions

File tree

engine/db.go

Lines changed: 244 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,35 @@ import (
77
"io"
88
"os"
99
"path/filepath"
10+
"slices"
11+
"strconv"
12+
"strings"
1013
"sync"
1114

1215
"github.com/aalhour/beachdb/internal/keys"
1316
"github.com/aalhour/beachdb/internal/memtable"
17+
"github.com/aalhour/beachdb/internal/sstable"
1418
"github.com/aalhour/beachdb/internal/wal"
1519
)
1620

17-
var (
18-
// walFileName specifies the name of the WAL file
21+
const (
22+
// walFileName specifies the name of the WAL file.
1923
walFileName = "beachdb.wal"
24+
25+
// sstableFileExt specifies the extension of SSTable files.
26+
sstableFileExt = ".sst"
2027
)
2128

2229
// DB defines the database struct wrapping the public APIs.
2330
type DB struct {
24-
mu sync.RWMutex // synchronization for safe concurrency.
25-
dir string // path on disk to write data into.
31+
closed bool // Flag indicating whether db is closed or not
32+
mu sync.RWMutex // Synchronization for safe concurrency.
33+
dir string // Path on disk to write data into.
2634
wal *wal.Writer // Writer for WAL file.
2735
mem memtable.Memtable // Memory table data structure
2836
seqno uint64 // Monotonic sequence counter
37+
ssts []*sstable.Reader // Open SSTable readers, newest-first
38+
nextSSTID uint64 // Counter for SST file naming (new files)
2939
syncOnWrite bool // Option: Whether the DB should fsync writes or not.
3040
}
3141

@@ -41,6 +51,7 @@ func Open(dir string, opts ...Option) (*DB, error) {
4151

4252
// Initialize the DB struct
4353
db := &DB{
54+
closed: false,
4455
dir: dir,
4556
mem: memtable.NewSkipList(),
4657
seqno: 0,
@@ -72,6 +83,30 @@ func Open(dir string, opts ...Option) (*DB, error) {
7283
return nil, err
7384
}
7485

86+
// Discover SSTables and create readers for them
87+
sortedFileNames, nextSSTID, err := discoverSSTables(dir)
88+
if err != nil {
89+
_ = db.Close()
90+
return nil, fmt.Errorf("beachdb: error discovering SSTables, %w", err)
91+
}
92+
93+
// Iterate over discovered sstable files and open readers for them
94+
for _, fileName := range sortedFileNames {
95+
fullPath := filepath.Join(dir, fileName)
96+
sstableFile, err := os.Open(fullPath) //nolint:gosec // trusted dir + discovered filename
97+
if err != nil {
98+
_ = db.Close()
99+
return nil, fmt.Errorf("beachdb: opening SSTable %s: %w", fileName, err)
100+
}
101+
sstReader, err := sstable.OpenReader(sstableFile)
102+
if err != nil {
103+
_ = db.Close()
104+
return nil, fmt.Errorf("beachdb: reading SSTable %s: %w", fileName, err)
105+
}
106+
db.ssts = append(db.ssts, sstReader)
107+
}
108+
db.nextSSTID = nextSSTID
109+
75110
return db, nil
76111
}
77112

@@ -80,16 +115,16 @@ func (db *DB) Write(ctx context.Context, b *Batch) error {
80115
db.mu.Lock()
81116
defer db.mu.Unlock()
82117

118+
// Check if the database was closed
119+
if db.closed {
120+
return ErrDBClosed
121+
}
122+
83123
// Check if already canceled before doing any work
84124
if err := ctx.Err(); err != nil {
85125
return fmt.Errorf("beachdb: Write call canceled: %w", err)
86126
}
87127

88-
// Check if the database was closed
89-
if db.wal == nil {
90-
return ErrDBClosed
91-
}
92-
93128
// Encode the Batch and append it to the WAL
94129
if b == nil {
95130
return nil
@@ -124,16 +159,54 @@ func (db *DB) Get(ctx context.Context, key []byte) (value []byte, err error) {
124159
db.mu.RLock()
125160
defer db.mu.RUnlock()
126161

162+
// Check if the database was closed
163+
if db.closed {
164+
return nil, ErrDBClosed
165+
}
166+
127167
// Check if the call was canceled
128168
if err := ctx.Err(); err != nil {
129169
return nil, fmt.Errorf("beachdb: Get call canceled: %w", err)
130170
}
131171

172+
// Search the memtable
132173
value, found := db.mem.Get(key, db.seqno)
133-
if !found {
134-
return nil, ErrKeyNotFound
174+
175+
// Check if the key was found in the membtable
176+
if found {
177+
if value == nil {
178+
// Tombestone (latest update in memtable was a delete operation)
179+
return nil, ErrKeyNotFound
180+
}
181+
return value, nil
135182
}
136-
return value, nil
183+
184+
// Otherwise, search for key in SSTables in reverse order, because
185+
// they are sorted lexicographically from oldest to newest (e.g.: 001 --> 123)
186+
for _, reader := range slices.Backward(db.ssts) {
187+
value, err := reader.Get(key, db.seqno)
188+
189+
// Value found
190+
if err == nil {
191+
return value, nil
192+
}
193+
194+
// Tombstone: key was explicitly deleted at this level, stop searching
195+
if errors.Is(err, sstable.ErrKeyDeleted) {
196+
return nil, ErrKeyNotFound
197+
}
198+
199+
// Key absent from this SSTable, try the next one
200+
if errors.Is(err, sstable.ErrKeyNotFound) {
201+
continue
202+
}
203+
204+
// Real error (corruption, I/O failure)
205+
return nil, fmt.Errorf("beachdb: reading SSTable: %w", err)
206+
}
207+
208+
// Scanned all SSTables and found nothing, return an error.
209+
return nil, ErrKeyNotFound
137210
}
138211

139212
// Put writes the key-value pair in the database.
@@ -161,20 +234,35 @@ func (db *DB) Close() error {
161234
db.mu.Lock()
162235
defer db.mu.Unlock()
163236

164-
// Check if already closed or not
165-
if db.wal == nil {
237+
// Check if the database was closed
238+
if db.closed {
166239
return ErrDBClosed
167240
}
168241

242+
var firstError error
243+
244+
// Close all SSTable readers before closing the WAL writer
245+
for _, reader := range db.ssts {
246+
err := reader.Close()
247+
if err != nil && firstError == nil {
248+
firstError = err
249+
}
250+
}
251+
169252
// Close the WAL writer
170253
err := db.wal.Close()
254+
if err != nil && firstError == nil {
255+
firstError = err
256+
}
171257

172258
// Mark it as closed
259+
db.ssts = nil
173260
db.wal = nil
174261
db.mem = nil
262+
db.closed = true
175263

176-
if err != nil {
177-
return fmt.Errorf("beachdb: closing WAL: %w", err)
264+
if firstError != nil {
265+
return fmt.Errorf("beachdb: error closing DB: %w", firstError)
178266
}
179267
return nil
180268
}
@@ -255,3 +343,143 @@ func replayWAL(db *DB, walFilePath string) error {
255343

256344
return nil
257345
}
346+
347+
// flushMemtable writes the contents in db.mem to a new SST file and replaces the
348+
// memtable with a new one
349+
func (db *DB) flushMemtable() error {
350+
// -----------------------------------
351+
// Phase 1: swap mutable under a lock
352+
// -----------------------------------
353+
db.mu.Lock()
354+
355+
// Check if db was closed
356+
if db.closed {
357+
db.mu.Unlock()
358+
return ErrDBClosed
359+
}
360+
361+
// Grab a pointer to the previous db.mem
362+
immutableMem := db.mem
363+
// Replace the memtable with a new one
364+
db.mem = memtable.NewSkipList()
365+
// Get path of next sstable
366+
sstPath := db.nextSSTPath()
367+
// Increment next SSTID (for future flushes)
368+
db.nextSSTID++
369+
db.mu.Unlock() // release!
370+
371+
// ----------------------------------------------
372+
// Phase 2: operate over (old) immutable memtable
373+
// doesn't require locking
374+
// ----------------------------------------------
375+
// Create the new sstable file
376+
sstFile, err := os.Create(sstPath) //nolint:gosec // path constructed from trusted db.dir + formatted ID
377+
if err != nil {
378+
return ErrCreatingSSTFile
379+
}
380+
defer sstFile.Close()
381+
382+
// Create the sstable writer
383+
writer, err := sstable.NewWriter(sstFile, sstable.WithSync(true))
384+
if err != nil {
385+
_ = os.Remove(sstPath)
386+
return fmt.Errorf("beachdb: creating SSTable writer: %w", err)
387+
}
388+
389+
// Iterate the immutable memtable and write entries to the SSTable
390+
iter := immutableMem.NewIterator()
391+
iter.SeekToFirst()
392+
for iter.Valid() {
393+
if err := writer.Add(iter.Key(), iter.Value()); err != nil {
394+
_ = writer.Close()
395+
_ = iter.Close()
396+
_ = os.Remove(sstPath)
397+
return fmt.Errorf("beachdb: writing entry to SSTable: %w", err)
398+
}
399+
iter.Next()
400+
}
401+
402+
_ = iter.Close()
403+
if err = writer.Close(); err != nil {
404+
_ = os.Remove(sstPath)
405+
return fmt.Errorf("beachdb: closing SSTable writer: %w", err)
406+
}
407+
408+
// Sync parent directory so the new file's directory entry is durable
409+
if err = syncDir(db.dir); err != nil {
410+
return fmt.Errorf("beachdb: syncing directory after flush: %w", err)
411+
}
412+
413+
// Re-open the file for reading
414+
sstFileReadMode, err := os.Open(sstPath) //nolint:gosec // path constructed from trusted db.dir + formatted ID
415+
if err != nil {
416+
return fmt.Errorf("beachdb: opening SSTable for reading: %w", err)
417+
}
418+
sstReader, err := sstable.OpenReader(sstFileReadMode)
419+
if err != nil {
420+
return fmt.Errorf("beachdb: reading flushed SSTable: %w", err)
421+
}
422+
423+
// -------------------------------------------------------
424+
// Phase 3 — Publish the new sstable reader (under a lock)
425+
// -------------------------------------------------------
426+
db.mu.Lock()
427+
db.ssts = append(db.ssts, sstReader)
428+
db.mu.Unlock()
429+
430+
// Synccess! :>
431+
return nil
432+
}
433+
434+
// nextSSTPath returns a full path for the SSTable file from
435+
// the internal nextSSTID
436+
func (db *DB) nextSSTPath() string {
437+
return filepath.Join(db.dir, buildSSTFileName(db.nextSSTID))
438+
}
439+
440+
// Helper function for discovering SSTable files on disk
441+
func discoverSSTables(dir string) ([]string, uint64, error) {
442+
dirEntries, err := os.ReadDir(dir)
443+
if err != nil {
444+
return nil, 0, fmt.Errorf("beachdb: reading directory: %w", err)
445+
}
446+
447+
sstableFiles := make([]string, 0, len(dirEntries))
448+
449+
for _, entry := range dirEntries {
450+
if !entry.Type().IsRegular() {
451+
continue
452+
}
453+
454+
fileName := entry.Name()
455+
if filepath.Ext(fileName) != sstableFileExt {
456+
continue
457+
}
458+
459+
sstableFiles = append(sstableFiles, fileName)
460+
}
461+
462+
slices.Sort(sstableFiles)
463+
464+
var maxID uint64
465+
n := len(sstableFiles)
466+
if n > 0 {
467+
biggestName := sstableFiles[n-1]
468+
strID := strings.TrimSuffix(biggestName, filepath.Ext(biggestName))
469+
470+
parsedID, err := strconv.ParseUint(strID, 10, 64)
471+
if err != nil {
472+
return sstableFiles, 0, fmt.Errorf("beachdb: parsing SSTable ID %q: %w", biggestName, err)
473+
}
474+
475+
maxID = parsedID + 1
476+
}
477+
478+
return sstableFiles, maxID, nil
479+
}
480+
481+
// Helper function for building an SSTable file name from a
482+
// file ID number, e.g.: 1 --> 000001.sst
483+
func buildSSTFileName(id uint64) string {
484+
return fmt.Sprintf("%06d.sst", id)
485+
}

0 commit comments

Comments
 (0)