Skip to content

Latest commit

 

History

9 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

 ██████ ██████   █████   ██████ ██   ██ ██ ███    ██  ██████        ██      ███████ ███    ███ 
██      ██   ██ ██   ██ ██      ██  ██  ██ ████   ██ ██             ██      ██      ████  ████ 
██      ██████  ███████ ██      █████   ██ ██ ██  ██ ██   ███ █████ ██      ███████ ██ ████ ██ 
██      ██   ██ ██   ██ ██      ██  ██  ██ ██  ██ ██ ██    ██       ██           ██ ██  ██  ██ 
 ██████ ██   ██ ██   ██  ██████ ██   ██ ██ ██   ████  ██████        ███████ ███████ ██      ██ 

cracking-lsm

cracking-lsm is a C++ project for a B-tree memtable and file-resident runs in a tiered Log-Structured Merge-tree (LSM-tree).

The planned write path inserts into an independent Abseil B-tree memtable, with a memtable_bytes flush threshold based on live node allocation bytes. Point and predecessor queries consult the memtable before storage and reconcile visible versions across sources. Memtable flush has a reserved implementation step; its design is deferred.

Run data resides in files. Memtable capacity and Run lifecycle are separate concepts. Typed entries, comparators, codecs, and block views remain available for operations on caller-owned memory. File appends use one aligned block buffer and preserve physical input order, including duplicates.

The shared cracking_lsm::KVEntry<KeyT, KeyOnly> type and its metadata, comparator, and key concepts live in include/cracking-lsm/kv_entry/. Include <cracking-lsm/kv_entry/kv_entry.hpp> to use entries independently of Run. RunState belongs to run/run_state.hpp; entries carry an optional external payload reference and support versions, tombstones, and key-only mode.

Shared operation results live in include/cracking-lsm/engine/op_result/, with one header per result: insertion_result.hpp, lookup_result.hpp, predecessor_result.hpp, and append_result.hpp. Include the specific header, such as <cracking-lsm/engine/op_result/insertion_result.hpp>, as needed. The abstract OpResult interface lives in op_result.hpp, identifies the operation, and has a virtual destructor. InsertionState and QueryState have their own insertion_state.hpp and query_state.hpp headers. InsertionResult, LookupResult<KeyT, KeyOnly>, PredecessorResult<KeyT, KeyOnly>, and AppendResult derive from it and can be returned by value. Query results own optional entry copies; public predecessor results contain a visible value or no entry. These runtime result objects are separate from the persisted entry format. The engine's planned Table interface will coordinate operations across sources.

The current implementation provides the entry and block formats and the internal building-file component cracking_lsm::detail::RunFile<KeyT, KeyOnly>, with Abseil already introduced as a dependency. Memtable<KeyT, KeyComparatorT, KeyOnly> in memtable/memtable.hpp now supports creation, single-entry insertion, snapshot point and predecessor queries, state observation, movement, and destruction. create requires a positive memtable_bytes; size(), empty(), allocated_bytes(), memtable_bytes(), and flush_required() expose its state and configuration. insert(entry) validates the entry kind and physical count before copying an entry into the B-tree. It retains duplicate entries, historical versions, and tombstones in the shared comparator order. An insertion that reaches or crosses the live-byte threshold still returns InsertionState::inserted and sets flush_required=true. Later valid insertions return InsertionState::memtable_full without accepting the entry. Each InsertionResult reports the actual post-operation count, allocated bytes, and flush flag. Invalid input is still rejected by validation when full; validation errors leave existing data, allocation counts, and the flush flag unchanged. A positive threshold smaller than the first node permits the first entry and then requires a flush; this step only reports that requirement.

lookup(key, read_version) returns the latest visible entry for a comparator-equivalent key as a LookupResult: value, tombstone, or not_found. At equal versions, a tombstone takes precedence. predecessor(key, read_version) returns a PredecessorResult containing the greatest visible, undeleted key strictly before the bound in comparator order, or not_found. It resolves each key's latest visible version before checking for deletion, so a tombstone never exposes that key's older value. Both const queries default to EntryMeta<KeyOnly>::MaxVersion, reject larger read versions before searching, and return owned entry copies in both payload-reference and key-only modes. They remain available when full and preserve entry counts, allocated bytes, and the flush flag. Point lookup uses a compound-key B-tree bound in O(log N); predecessor uses one user-key bound and group traversal in O(log N + E), where E counts examined physical entries. Empty queries take O(1), and each query uses O(1) extra space. Returned entries survive later insertion, movement, or destruction.

MemtableImpl<KeyT, KeyComparatorT, KeyOnly> in memtable/memtable_impl.hpp holds the configuration, accounting object, B-tree types and storage, and flush flag. It lives in the cracking_lsm namespace; its validate_insertion(entry) const checks the entry kind and the tree's actual count, and insert_entry(entry) validates before inserting. It also implements entry lookup and predecessor traversal. predecessor_entry directly scans preceding key groups and returns a value or no entry, skipping groups whose latest visible entry is a tombstone. Planned Table reconciliation uses point lookups across sources to check the visible version and deletion state of each possible predecessor. Memtable privately owns the implementation through impl_, a unique_ptr<ImplT>, and handles public interfaces, capacity policy, and operation results. Moving a Memtable transfers its unique_ptr, preserves the accounting object's address, and carries the flush flag with the data. Tree nodes are destroyed before the accounting object. memtable/memory_accounting.hpp defines cracking_lsm::MemoryAccounting and cracking_lsm::CountingAllocator<T>. The allocator wraps std::allocator, shares live request-byte counts across copies and rebinds, and releases memory through the standard allocator. It does not use an arena or enforce the memtable threshold during allocation. Shared fatal-error reporting lives in utils/error.hpp; the allocator reports allocation errors with context before termination. All user key comparators must be callable through a const reference, with non-throwing invocation and conversion of the result to bool. EntryComparator directly provides non-throwing comparisons, and Memtable additionally requires non-throwing comparator copies. Memtable and Run default to std::less<>, whose exception specification follows the key's < operation. Custom comparators should declare operator() as const noexcept. Violating a noexcept promise invokes termination without the former Memtable comparison diagnostic wrapper. Cross-source query routing, public Run lifecycle, sorting, and sealed B+Tree construction are planned in later implementation steps.

About

cracking-lsm is a lightweight implementation of a tiered Log-Structured Merge-tree (LSM-tree) written in C++.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages