RocksDbSharp

repository·master·Indexed 42 days ago

https://github.com/curiosity-ai/rocksdb-sharp

C# bindings for Facebook's RocksDB, a high-performance log-structured-merge key-value database. It provides managed wrappers around the native C++ library and includes a transport-agnostic Raft implementation for building fault-tolerant, multi-node clusters using the RocksDB WAL as the log.

Tokens
5K
Snippets
9
Records
20
Agent score
53%

What's inside RocksDbSharp

  1. How the cluster lifecycle works: Bootstrap to Cluster mode

    master

    The cluster moves through three distinct phases:

    1. Bootstrap (initialization)

    • Starts with one designated primary (RaftConfig.BootstrapPrimary = true).
    • The primary is forced into the Leader role immediately without an election.
    • Other nodes pull a full checkpoint via SyncInitialStateAsync and then follow the WAL stream.
    • No elections are allowed during this phase to prevent split-brain before data is replicated.

    2. Promotion to Cluster mode

    • The leader monitors followers using ReportLastSyncSequenceNumber.
    • When a sufficient number of followers (defined by BootstrapPromotionQuorum) are within the BootstrapInSyncLagThreshold of the leader's latestSeq, the leader promotes itself to Cluster mode.
    • This mode change is broadcast via heartbeats (HeartbeatPayload.LeaderMode).
    • Once a node enters Cluster mode, it cannot return to Bootstrap mode.

    3. Steady-state Cluster mode

    • Full Raft protocol is enforced.
    • Leader: Sends heartbeats every HeartbeatIntervalMs (default 250-300ms).
    • Follower: Resets its election timer on every heartbeat. If the randomized timer (default [1500, 3000] ms) expires, the follower becomes a Candidate.
  2. Understand the RocksDbSharp Clustering lifecycle

    master

    RocksDbSharp clustering follows a specific lifecycle to transition from a standalone node to a replicated cluster. The end-to-end flow involves:

    1. Bootstrap (initialization): A primary node is initialized.
    2. Promotion to Cluster mode: The primary node synchronizes with a follower via SyncUpdatesAsync (streaming the WAL). The follower uses ReportLastSyncSequenceNumber to advance the matchSeq and update the bootSync count. Once criteria are met, the node enters ModeChanged: Cluster.
    3. Steady-state Cluster mode: The leader maintains authority via periodic HeartbeatAsync calls, which communicate the current term, leaderCommit, and leaderMode to followers.
    4. Leader Failure & Election: If the leader fails, followers' election timers fire, triggering RequestVoteAsync to peers. Upon reaching a quorum, a node executes BecomeLeader and starts a new heartbeat with an incremented term++.
    +--------+          snapshot           +----------+
    | primary| ----------------------------> follower |
    | (boot) |
    +--------+                               +----------+
         |                                       |
         |  SyncUpdatesAsync (WAL stream)        |
         |-------------------------------------->|
         |                                       |
         |    ReportLastSyncSequenceNumber       |
         |<--------------------------------------|
         |  (matchSeq advances, bootSync count  |
         |  updates, promotion check fires)     |
         |                                       |
         +--- ModeChanged: Cluster --------------+
         |                                       |
         |   periodic HeartbeatAsync ------>     |
         |   (term, leaderCommit, leaderMode)   |
         |                                       |
         +-- on leader failure -----------------+
                                           election timer fires
                                                  |
                                           RequestVoteAsync to peers
                                                  |
                                           quorum reached -> BecomeLeader
                                                  |
                                           new heartbeat: term++
  3. Understand the limitations of the RocksDbSharp Raft implementation

    master

    The current implementation focuses on core Raft safety and does not include the following extensions:

    • Pre-vote: Partitioned followers may briefly bump their term and trigger an election before stabilizing.
    • Dynamic membership changes: The peer list is static for the lifetime of a process. Adding or removing nodes requires a configuration update and a restart.
    • Log compaction inside Raft: Compaction is handled by piggybacking on RocksDB's native compaction and pruning WAL files via RocksDbWalInspector.GetFirstSequenceNumbers once followers acknowledge them.
    • Linearizable reads from followers: Reads are only linearizable when queried from the leader. Followers do not currently implement ReadIndex.
    • Batched / pipelined AppendEntries: Heartbeats are sent individually, though the data plane (SyncUpdatesAsync) uses gRPC pipelining.
  4. Understand the RocksDbSharp Clustering architecture

    master

    RocksDbSharp Clustering is a transport-agnostic Raft implementation that converts a single-primary/single-replica WAL (Write Ahead Log) streaming setup into a fault-tolerant, multi-node cluster.

    Instead of maintaining a separate Raft log, it reuses the existing RocksDB WAL as the log. The mapping is as follows:

    • Raft log index: The RocksDB sequence number (db.GetLatestSequenceNumber()).
    • Log replication: Uses the existing ReplicationSource.GetPooledWalUpdates(seq) stream.
    • Catch-up: Uses ReplicationSource.GetInitialState for checkpoint transfers.

    To associate terms with entries without modifying RocksDB, the library maintains a side-log of term ranges in raft.terms. Each row is (startSeq, term), allowing a node to determine an entry's term by finding the largest startSeq <= seqNo via binary search.

  5. Verify Cluster Safety and Consistency

    master

    When building or testing applications using RocksDbSharp clustering, ensure your implementation respects the following observable Raft properties:

    • Election Safety: No two nodes should ever report themselves as the leader of the same term, and a node's term must never decrease.
    • Leader Completeness / Durability: A WriteAsync call only acknowledges once the entry is committed (replicated to a quorum). After a leader failure (e.g., a crash or hang), every acknowledged key must remain readable on the new leader.
    • State Machine Safety: After a period of activity, all replicas should converge to the same log tail where commitSeq == latestSeq. You can verify database consistency across nodes by comparing whole-database checksums using ChecksumAsync.
  6. Understand the RocksDb binding layers

    master

    The rocksdb-sharp library is a multi-level binding that wraps the native RocksDB C++ library via the RocksDB C API. It provides three levels of abstraction:

    1. Low Level: Direct access to the native C API functions.
    2. Mid Level: Helper wrappers to assist with marshaling and exception handling.
    3. High Level: An idiomatic C# class hierarchy designed for ease of use.
  7. How the election protocol works

    master

    When a follower's election timer expires, it initiates an election following these steps:

    1. Transition: The node becomes a Candidate, increments currentTerm, votes for itself, and resets its election timer.
    2. Request Votes: It sends RequestVote(term, candidateId, lastLogSeq, lastLogTerm) to all peers.
    3. Voting Criteria: A peer grants a vote only if:
      • The request's term >= currentTerm.
      • The node is in Cluster mode (not Bootstrap).
      • The node hasn't already voted for someone else in this term.
      • The candidate's log is at least as up-to-date as the receiver's (lexicographic comparison of lastLogTerm and lastLogSeq).
    4. Winning: If the candidate receives a Quorum of votes:
      • It calls BecomeLeader().
      • It records a new term-range row.
      • It resets followers' nextSeq to latestSeq + 1 and matchSeq to 0.
      • It writes a no-op entry (RaftClusterNode.LeaderNoOpKey) to allow the commit index to advance.
      • It starts the heartbeat loop.

    Persistence: To ensure safety during crashes, currentTerm, votedFor, and the new term-range row are all fsync'd (using atomic temp-file renames) before any network responses are sent.

  8. How log divergence and full resync work

    master

    RocksDbSharp handles log divergence by rebuilding the entire replica from a leader snapshot rather than truncating the RocksDB WAL (which is not possible). Divergence is detected through three layers:

    1. Attach-time consistency check: Before starting/restarting a WAL stream, a follower calls CheckLogConsistencyAsync(latestSeq, termAtLatest) on the leader. If the leader's (seq, term) pair doesn't match, a resync is triggered.
    2. Per-batch sequence verification: Followers require every incoming batch to have a sequence number exactly equal to localLatestSeq + 1. Gaps or overlaps trigger a resync.
    3. Heartbeat prev-term check: HandleAppendEntries compares prevLogTerm against the local term map; a mismatch triggers RaftClusterNode.ResyncRequired.

    The Resync Process (ClusterNodeHost.PerformResyncAsync):

    • The node enters a BeginResync() state: it pauses election timers, refuses votes, and answers heartbeats with "resyncing".
    • The local DB is disposed.
    • A per-file delta transfer occurs: The node and leader exchange identity signatures for .sst and .blob files using GetFileHashesAsync. The signature hashes the file length and the last 1 MiB (containing the RocksDB footer/metadata) to ensure byte-identity.
    • The leader streams only new or changed files. Verified files are reused.
    • The node deletes its journal/WAL directory and all unconfirmed local files, keeping only the raft/ state directory.
    • CURRENT is transferred last to ensure a crash mid-restore results in a clean re-pull.
    • Once CompleteResync(newDb) is called, the database is swapped in and the election timer is re-armed.
  9. Build RocksDB native libraries for Linux

    master

    Use the build-rocksdb-linux.sh script to build RocksDB for linux-x64 and linux-arm64 architectures, supporting both glibc and musl libc flavors.

    Requirements:

    • make, cmake, a C++ toolchain (GCC 11+ or Clang 13+ required for C++20 support)
    • git, curl
    • libjemalloc-dev (if building the jemalloc flavor)
    • For arm64 cross-compilation: g++-aarch64-linux-gnu must be on PATH.

    Output:

    • librocksdb.so (Standard library)
    • librocksdb-jemalloc.so (Linked against jemalloc; requires the process to have jemalloc mapped, e.g., via LD_PRELOAD=libjemalloc.so.2).

    Note: RocksDbSharp probes for the jemalloc version first. If jemalloc is not present in the process, it falls back to the standard librocksdb.so.

    ./build-rocksdb-linux.sh [--arch x64|arm64] [--libc glibc|musl] [--no-jemalloc]
  10. Build RocksDB native libraries using Docker (Linux)

    master

    If you need to build specific Linux flavors that your current host cannot build directly (e.g., specific glibc/musl combinations on a non-Linux host), use the Docker wrapper script. This ensures the toolchain and libc versions are pinned and consistent.

    Usage Examples:

    ./build-rocksdb-linux-docker.sh --arch x64   --libc glibc
    ./build-rocksdb-linux-docker.sh --arch x64   --libc musl
    ./build-rocksdb-linux-docker.sh --arch arm64 --libc glibc
    ./build-rocksdb-linux-docker.sh --arch arm64 --libc musl
    ./build-rocksdb-linux-docker.sh --arch x64   --libc glibc
  11. Install RocksDb via NuGet

    master

    To use RocksDb in your C# project, install the RocksDb NuGet package. The package includes both the managed library and the appropriate unmanaged native library for Windows, Linux, and MacOS based on your operating system.

    Note that the versioning follows the official RocksDB release version, followed by a build number (e.g., 6.7.3.6120 corresponds to RocksDB v6.7.3).

    install-package RocksDb
  12. Build RocksDB native libraries for macOS

    master

    Use the build-rocksdb-macos.sh script to build RocksDB for macOS. The script supports cross-compilation for both arm64 and x64 architectures from your current machine.

    Requirements:

    • Xcode command line tools.

    Usage:

    • Use --arch arm64, --arch x64, or --arch all (default) to specify the target architecture.
    ./build-rocksdb-macos.sh [--arch arm64|x64|all]