leveldown

repository·master·Indexed 20 days ago

https://github.com/level/leveldown

A high-performance, low-level Node.js binding for LevelDB that provides an ordered mapping from string keys to string values. It supports atomic batches, snapshots, and custom comparison functions. Note that leveldown is superseded by classic-level, and the use of levelup is generally recommended for improved usability and safety.

Tokens
9.1K
Snippets
33
Records
51
Agent score
73%

What's inside leveldown

  1. What is LevelDB?

    master

    LevelDB is a fast key-value storage library developed by Google. It provides an ordered mapping from string keys to string values using arbitrary byte arrays.

    Core Features:

    • Ordered Storage: Data is stored sorted by key. Users can provide a custom comparison function to override the default sort order.
    • Atomic Batches: Multiple changes can be applied in a single atomic batch.
    • Snapshots: Users can create transient snapshots to obtain a consistent view of the data.
    • Iteration: Supports both forward and backward iteration over the data.
    • Compression: Data is automatically compressed using the Snappy compression library.
    • Extensible Environment: External activity (like file system operations) is handled through a virtual interface, allowing users to customize OS interactions.
  2. Important: leveldown is superseded by classic-level

    master

    Warning

    leveldown is superseded by classic-level.

    It is strongly recommended to use levelup in preference to leveldown unless you have specific, measurable performance reasons to do so. levelup is optimized for usability and safety. leveldown can still cause Node.js process crashes if operations are not performed correctly.

  3. Iterate through the database with an iterator

    master

    An iterator provides a snapshot of the store at the time it was created. You can consume it using for await...of or manually via .next().

    Range Options

    • gt, gte: Lower bound (greater than / greater than or equal).
    • lt, lte: Upper bound (less than / less than or equal).
    • reverse (boolean, default: false): Iterates in reverse order.
    • limit (number, default: -1): Maximum number of entries to collect.

    Iterator Options

    • keys (boolean, default: true): Whether to return keys.
    • values (boolean, default: true): Whether to return values.
    • keyAsBuffer (boolean, default: true): Return keys as Buffer or string.
    • valueAsBuffer (boolean, default: true): Return values as Buffer or string.
    • fillCache (boolean, default: false): Whether to fill the LRU cache.

    Important: If using manual .next(), you must call .end() to free resources. for await...of handles this automatically.

    // Using for await...of
    try {
      for await (const [key, value] of db.iterator({ gt: 'a', lt: 'z' })) {
        console.log(key, value);
      }
    } catch (err) {
      console.error(err);
    }
  4. Understand the LevelDB file structure

    master

    A LevelDB database is represented by a set of files stored in a single directory. The key components are:

    • Log files (*.log): Store a sequence of recent updates. When a log file reaches its size threshold (default ~4MB), it is converted into a sorted table and a new log is started.
    • Sorted tables (*.ldb): Store entries sorted by key (values or deletion markers). These are organized into levels.
    • MANIFEST: A log file that lists the sorted tables in each level, their key ranges, and other metadata. A new MANIFEST is created whenever the database is reopened.
    • CURRENT: A text file containing the name of the latest MANIFEST file.
    • Info logs (LOG, LOG.old): Contain informational messages.
    • Miscellaneous (LOCK, *.dbtmp): Files used for locking and temporary operations.
  5. How LevelDB levels and compaction work

    master

    LevelDB uses a tiered storage structure to manage data efficiently:

    Level 0 (Young Level)

    • Contains sorted tables generated directly from log files.
    • Note: Files in Level 0 may contain overlapping key ranges.
    • When the number of Level 0 files exceeds a threshold (currently 4), they are merged with overlapping Level 1 files to create new Level 1 files.

    Higher Levels (Level 1 and above)

    • Files in these levels have distinct, non-overlapping key ranges.
    • Compaction Trigger: When the combined size of files in level $L$ exceeds $10^L$ MB (e.g., 10MB for Level 1, 100MB for Level 2), a compaction is triggered.
    • Compaction Process: A background thread picks one file from level $L$ and all overlapping files from level $L+1$. These are merged into new files for level $L+1$.
    • Benefits: Compaction gradually migrates updates to larger levels using bulk reads/writes, minimizing expensive seeks and dropping obsolete values or deletion markers.
  6. Concurrency model in LevelDB

    master

    LevelDB has specific concurrency constraints:

    • Process Level: A database can only be opened by one process at a time. LevelDB uses OS-level locks to enforce this.
    • Thread Level: A single leveldb::DB object can be safely shared across multiple threads without external synchronization. Threads can call Get, Put, Delete, or create iterators concurrently.
    • Object Level: Objects like leveldb::Iterator and leveldb::WriteBatch are not thread-safe. If multiple threads must access the same iterator or batch, you must provide your own external synchronization (e.g., mutexes).
  7. Understand the LevelDB Log Format

    master

    LevelDB log files are composed of a sequence of 32KB blocks. The final block in a file may be a partial block. Each block contains a sequence of records and an optional trailer.

    Record Structure

    Each record is composed of the following fields (all little-endian):

    • checksum: uint32 (CRC32C of the type and data[])
    • length: uint16 (The length of the data field)
    • type: uint8 (One of FULL, FIRST, MIDDLE, or LAST)
    • data: uint8[length] (The actual payload)

    Record Types

    Records are categorized by how they handle user data, especially when data spans across block boundaries:

    • FULL (1): The record contains the entire contents of a user record.
    • FIRST (2): The first fragment of a user record that has been split across blocks.
    • MIDDLE (3): An interior fragment of a user record.
    • LAST (4): The final fragment of a user record.

    Block Boundaries and Trailers

    A record will never start within the last six bytes of a block to ensure it can fit. Any remaining bytes at the end of a block (up to 6 bytes) form a trailer, which must consist entirely of zero bytes and should be skipped by readers.

    block := record* trailer?
    record :=
      checksum: uint32     // crc32c of type and data[] ; little-endian
      length: uint16       // little-endian
      type: uint8          // One of FULL, FIRST, MIDDLE, LAST
      data: uint8[length]
  8. Understand LevelDB Snapshots in leveldown

    master

    leveldown supports LevelDB snapshots. A snapshot captures the state of the database at a specific point in time.

    When using a snapshot (for example, by creating a createReadStream and a createWriteStream simultaneously), any data modified by the write stream will not affect the data emitted from the read stream. The snapshot allows you to iterate or read data without seeing subsequent writes. Any read operation not explicitly performed on a snapshot will implicitly use the latest state of the database.

  9. Understand LevelDB limitations

    master

    Before using LevelDB, be aware of the following constraints:

    • Not a SQL Database: It does not have a relational data model, does not support SQL queries, and has no support for indexes.
    • Single Process Access: Only a single process (which may be multi-threaded) can access a particular database instance at a time.
    • No Built-in Client-Server Support: LevelDB is a library, not a standalone server. If your application requires client-server architecture, you must wrap the library in your own server implementation.
  10. Perform atomic updates with WriteBatch

    master

    To prevent partial updates (e.g., if a process crashes between a Put and a Delete), use the leveldb::WriteBatch class. A batch allows you to group multiple edits that are applied atomically. This also improves performance for bulk updates.

    Note: When performing operations like moving a value from key1 to key2, call Delete(key1) before Put(key2, value) within the batch to avoid data loss if the keys are identical.

    #include "leveldb/write_batch.h"
    ... 
    std::string value;
    leveldb::Status s = db->Get(leveldb::ReadOptions(), key1, &value);
    if (s.ok()) {
      leveldb::WriteBatch batch;
      batch.Delete(key1);
      batch.Put(key2, value);
      s = db->Write(leveldb::WriteOptions(), &batch);
    }
  11. How LevelDB performs recovery

    master

    When a database is opened, LevelDB follows these steps to ensure consistency:

    1. Read the CURRENT file to find the name of the latest committed MANIFEST.
    2. Read that MANIFEST file.
    3. Clean up stale files (garbage collection).
    4. Convert the existing log chunk into a new Level 0 sorted table.
    5. Start directing new writes to a new log file using the recovered sequence number.
  12. Understand leveldb::Slice

    master

    A leveldb::Slice is a lightweight structure containing a pointer and a length. It is used for keys and values to avoid expensive memory copies of large byte arrays.

    Key Behaviors:

    • Conversion: You can convert std::string to Slice and Slice to std::string via .ToString().
    • Safety Warning: A Slice does not own the underlying data. You must ensure the source buffer (e.g., a std::string) remains in scope and is not destroyed while the Slice is being used. Using a Slice that points to a local variable that goes out of scope is a common source of bugs.
    // String to Slice
    leveldb::Slice s1 = "hello";
    std::string str("world");
    leveldb::Slice s2 = str;
    
    // Slice to String
    std::string str_from_s1 = s1.ToString();