memtable

package
v0.0.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 29, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

Documentation

Overview

Package memtable implements BeachDB's in-memory write buffer using a skip list.

What is a memtable?

In an LSM-tree database, writes don't go directly to disk. They land here first: a sorted, in-memory structure that accumulates recent mutations until it's full enough to flush to an SSTable on disk. The memtable is where "fast writes" come from — appending to a sorted structure in RAM is orders of magnitude cheaper than random I/O to stable storage.

Why a skip list?

Skip lists are a probabilistic data structure that give you O(log n) insertion and lookup without the rebalancing headaches of a tree. Think of it as a linked list that learned to skip ahead: each node has a random "height," and taller nodes act as express lanes that let you jump over large sections of the list.

The appeal for a memtable:

  • Sorted iteration is trivial (it's a linked list at level 0)
  • No rotations or rebalancing (insertion is local pointer surgery)
  • Concurrent-friendly (fine-grained locking is possible, though we use a single RWMutex for v1)
  • Simple to implement without getting clever

Redis, LevelDB, and RocksDB all use skip lists for their memtables. It's a battle-tested choice.

Key ordering

This is the most important invariant in the whole package.

Keys are sorted by (user_key ASC, seqno DESC). For the same user key, the entry with the highest sequence number appears first. This ordering means "newest version wins" falls out naturally: when you iterate or search, you hit the freshest version before any older ones.

Example:

Put("foo", "v1") at seqno 5
Put("foo", "v2") at seqno 10
Delete("foo") at seqno 15

The skip list stores these as:

("foo", 15, Delete) -> nil
("foo", 10, Put)    -> "v2"
("foo", 5, Put)     -> "v1"

A Get("foo") at seqno 20 finds the tombstone first and returns "not found." A Get("foo") at seqno 12 skips the tombstone (seqno 15 > 12) and returns "v2."

Concurrency model

The SkipList uses a single sync.RWMutex:

  • Put takes an exclusive lock (we assume single-writer for v1)
  • Get takes a shared lock
  • Iterators acquire a shared lock on first Seek/SeekToFirst and hold it until Close is called

This means iterators block writers. It's a deliberate v1 simplicity choice — the frozen-memtable flush pattern routes new writes to a fresh memtable while the old one drains, so iterator lock contention becomes a non-issue in practice.

IMPORTANT: Callers MUST call Iterator.Close() to release the lock. Forgetting this will deadlock writers indefinitely. The compiler won't save you here.

What the iterator sees

The iterator exposes ALL entries: puts, tombstones, every version of every key. It does not filter, deduplicate, or hide anything. That's intentional.

Filtering (hide old versions, skip tombstones, respect snapshot boundaries) happens at a higher layer — the merge iterator that combines memtable + SSTables during reads. The memtable iterator's job is to produce a sorted stream of internal keys. Nothing more.

Memory accounting

Size() returns an approximate byte count of memory used. It's not exact — we estimate per-node overhead and don't account for allocator fragmentation — but it's good enough for "is this memtable full yet?" decisions.

The p=0.25 choice

Skip list level probability affects the height distribution:

  • p=0.5: Average node height ~2. More memory, faster search.
  • p=0.25: Average node height ~1.33. Less memory, still O(log n).

We use p=0.25 because it's the standard choice (LevelDB, Redis) and keeps memory overhead reasonable. With maxLevel=12 and p=0.25, we can handle billions of entries before the math breaks down.

Package memtable implements the skiplist and other types for the Memtable

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Iterator

type Iterator interface {
	SeekToFirst()          // Position at first key
	Seek(target []byte)    // Position at first key >= target
	Valid() bool           // True if positioned at valid entry
	Next()                 // Advance to next key
	Key() keys.InternalKey // Current key - only valid if Valid() == true
	Value() []byte         // Current value - only valid if Valid() == true
	Close() error          // Release resources
}

Iterator provides forward iteration over memtable entries in sorted order.

type Memtable

type Memtable interface {
	Put(key keys.InternalKey, value []byte) // Insert a key-value pair
	// Get retrieves a value at the given seqno.
	// Returns (nil, false) if the key was not found,
	// and (nil, true) when the newest visible version is a tombstone.
	Get(userKey []byte, seqno uint64) ([]byte, bool)
	NewIterator() Iterator // Create an iterator over entries
	Len() int              // Number of entries
	Size() int64           // Approximate memory usage in bytes
	Empty() bool           // True if no entries
}

Memtable is an in-memory sorted key-value store that buffers writes before flushing to SSTables.

type SkipList

type SkipList struct {
	// contains filtered or unexported fields
}

SkipList is a probabilistic data structure that provides O(log n) insertion and search with lock-friendly concurrent access. It implements the Memtable interface.

func NewSkipList

func NewSkipList() *SkipList

NewSkipList creates a new SkipList struct pointer and returns it

func (*SkipList) Empty

func (sl *SkipList) Empty() bool

Empty returns true if the skip list has no entries.

func (*SkipList) Get

func (sl *SkipList) Get(userKey []byte, seqno uint64) (value []byte, found bool)

Get returns the value for the given userKey at or before the given seqno. Returns (value, true) if found, (nil, false) if not found, and (nil, true) when the newest visible version is a tombstone.

Semantics: finds the newest version of userKey with seqno <= requested seqno. If that version is a tombstone (KindDelete), returns (nil, true).

func (*SkipList) Len

func (sl *SkipList) Len() int

Len returns the number of entries in the skip list.

func (*SkipList) NewIterator

func (sl *SkipList) NewIterator() Iterator

NewIterator returns an iterator over the skip list entries.

func (*SkipList) Put

func (sl *SkipList) Put(key keys.InternalKey, value []byte)

Put inserts a key-value pair into the SkipList. In a memtable, we always insert (never update in place) because different sequence numbers represent different versions of the same user key.

func (*SkipList) Size

func (sl *SkipList) Size() int64

Size returns the approximate memory usage in bytes.

type SkipListIterator

type SkipListIterator struct {
	// contains filtered or unexported fields
}

SkipListIterator provides forward iteration over a SkipList's entries. Thread safety: The iterator holds a read lock from the first Seek/SeekToFirst call until Close. Concurrent writes will block until Close is called. Users MUST call Close to release the lock; failing to do so will deadlock writers.

func NewSkipListIterator

func NewSkipListIterator(list *SkipList) *SkipListIterator

NewSkipListIterator creates a new iterator for the given skip list. The iterator is initially invalid; call SeekToFirst or Seek to position it.

func (*SkipListIterator) Close

func (it *SkipListIterator) Close() error

Close releases any resources held by the iterator. After Close, the iterator must not be used.

func (*SkipListIterator) Key

func (it *SkipListIterator) Key() keys.InternalKey

Key returns the key at the current position. Only valid if Valid() returns true.

func (*SkipListIterator) Next

func (it *SkipListIterator) Next()

Next advances the iterator to the next entry. If the iterator is already at the end, it becomes invalid.

func (*SkipListIterator) Seek

func (it *SkipListIterator) Seek(target []byte)

Seek positions the iterator at the first entry with key >= target. The target is treated as a user key; the iterator will position at the first internal key whose user key >= target (respecting internal key ordering).

func (*SkipListIterator) SeekToFirst

func (it *SkipListIterator) SeekToFirst()

SeekToFirst positions the iterator at the first entry. After this call, Valid() returns true if the list is non-empty.

func (*SkipListIterator) Valid

func (it *SkipListIterator) Valid() bool

Valid returns true if the iterator is positioned at a valid entry.

func (*SkipListIterator) Value

func (it *SkipListIterator) Value() []byte

Value returns the value at the current position. Returns a copy to prevent caller mutation. Only valid if Valid() returns true.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL