SierraDB Distributed Event Store

repository·main·Indexed 43 days ago

https://github.com/sierra-db/sierradb

A high-performance, distributed event store designed for event sourcing workloads. It features Redis protocol compatibility (RESP3), a distributed architecture similar to Cassandra/ScyllaDB, and a three-tier hierarchy of buckets, partitions, and segmented storage. Key operations include EAPPEND for version-controlled event appending, EMAPPEND for atomic multi-stream transactions, and ESCAN for stream scanning. Includes the seglog crate for managing fixed-size segment files with CRC32C validation.

Tokens
68K
Snippets
179
Records
278
Agent score
53%

What's inside SierraDB

  1. Key features of Kameo

    main

    Kameo is a distributed actor framework built on top of libp2p. Its core features include:

    • Composable Architecture: The Behaviour struct implements libp2p's NetworkBehaviour, allowing integration with existing libp2p applications.
    • Quick Bootstrap: Functions like bootstrap() and bootstrap_on() provide one-line setup for development.
    • Actor Registration & Discovery: Actors are registered under unique names and can be looked up across the network using RemoteActorRef.
    • Reliable Messaging: Uses Kademlia DHT for discovery and request-response protocols for reliable communication.
    • Modular Design: Separates messaging and registry modules to handle different aspects of distributed communication.
  2. Understand the segment module and its re-exports

    main

    The segment module manages the low-level storage components of SierraDB. It provides several key types for reading and writing data segments, which are re-exported for easier access:

    • BucketSegmentIter: Iterator for bucket segments.
    • BucketSegmentReader: Reader for bucket segments.
    • CommitRecord: Represents a commit.
    • CommittedEvents: A collection of committed events.
    • CommittedEventsIntoIter: Iterator over committed events.
    • EventRecord: A single event record.
    • Record: A generic record type.
    • AppendEvent: Used for appending events.
    • BucketSegmentWriter: Writer for bucket segments.
  3. Overview of Remote Actors in Kameo

    main
    The remote module (available via the remote feature flag) enables distributed actor systems. It uses libp2p to allow actors to communicate across a peer-to-peer (P2P) network. Actors can be registered under unique names, allowing you to send messages to actors on different nodes as if they were local.
  4. Understand the seglog record format

    main

    Every record written by seglog follows a specific binary layout to ensure integrity and allow for length-prefixed parsing:

    FieldSize
    Length4 Bytes
    CRC32C4 Bytes
    DataN Bytes

    seglog automatically performs CRC32C validation on every read to detect corruption.

  5. Understand the ClosedIndex enum variants

    main

    The ClosedIndex enum represents the two ways an index can be stored after being closed:

    1. Cache: Uses a std::collections::BTreeMap<uuid::Uuid, u64> for lookups.
    2. Mphf: Uses a boomphf::Mphf<uuid::Uuid> and a records_offset: u64 for high-performance lookups.
  6. How the Distributed Write Protocol works

    main

    SierraDB uses a multi-phase protocol to coordinate writes across a cluster. The process involves a Client, a Coordinator (selected per partition), and multiple Replicas.

    Write Lifecycle Phases

    1. Write Request Routing: A client sends a WriteRequest(transaction, expected_seq?) to any node. If the node is not the coordinator for that partition, it forwards the request to the correct coordinator.
    2. Coordinator Write Preparation: The coordinator validates the request, assigns a partition sequence number, and writes the data locally.
    3. Replica Write Replication: The coordinator sends ReplicateWrite(partition_seq, transaction) to all replicas in parallel. Replicas validate the coordinator and the sequence. If a sequence gap is detected, the replica buffers the write and returns SEQUENCE_GAP.
    4. Quorum Confirmation: Once the coordinator receives enough SUCCESS responses to meet the quorum requirement, it marks the write as confirmed locally.
    5. Write Confirmation: The coordinator notifies replicas via ConfirmWrite(partition_seq), and the final WriteResponse(SUCCESS, partition_seq) is returned to the client.

    Replica Recovery (Catch-up)

    If a replica misses writes, it initiates a catch-up process:

    1. Replica sends PartitionSyncRequest(from_seq) to the coordinator.
    2. Coordinator responds with PartitionSyncResponse(missing_events).
    3. Replica applies the missing events and then processes any writes previously held in its buffer.
    @startuml
    participant Client as "Client/Node"
    participant AnyNode as "Any Replica Node"
    participant Coordinator as "Coordinator Node"
    participant Replica1 as "Replica Node 1"
    participant Replica2 as "Replica Node 2"
    participant Replica3 as "Replica Node 3"
    
    == Phase 1: Write Request Routing ==
    Client -> AnyNode : WriteRequest(transaction, expected_seq?)
    AnyNode -> AnyNode : get_coordinator_for_partition()
    
    alt Current node is coordinator
        AnyNode -> AnyNode : Process as coordinator
    else Forward to coordinator
        AnyNode -> Coordinator : WriteRequest(transaction, expected_seq?)
    end
    
    == Phase 2: Coordinator Write Preparation ==
    Coordinator -> Coordinator : validate_write_request()
    Coordinator -> Coordinator : assign_partition_sequence()
    Coordinator -> Coordinator : write_locally() ✓
    
    == Phase 3: Replica Write Replication (Parallel) ==
    par Replicate to Replica1
        Coordinator -> Replica1 : ReplicateWrite(partition_seq, transaction)
    end
    par Replicate to Replica2
        Coordinator -> Replica2 : ReplicateWrite(partition_seq, transaction)
    end
    par Replicate to Replica3
        Coordinator -> Replica3 : ReplicateWrite(partition_seq, transaction)
    end
    
    == Each replica processes sequentially ==
    
    Replica1 -> Replica1 : validate_coordinator()
    Replica1 -> Replica1 : validate_sequence()
    alt Sequence ready (Replica1)
        Replica1 -> Replica1 : write_locally() ✓
        Replica1 -> Coordinator : ReplicateWriteResponse(SUCCESS)
    else Sequence gap (Replica1)
        Replica1 -> Replica1 : buffer_write()
        Replica1 -> Coordinator : ReplicateWriteResponse(SEQUENCE_GAP)
    end
    
    Replica2 -> Replica2 : validate_coordinator()
    Replica2 -> Replica2 : validate_sequence()
    Replica2 -> Replica2 : write_locally() ✓
    Replica2 -> Coordinator : ReplicateWriteResponse(SUCCESS)
    
    Replica3 -> Replica3 : validate_coordinator()
    Replica3 -> Replica3 : validate_sequence()
    Replica3 -> Replica3 : write_locally() ✓
    Replica3 -> Coordinator : ReplicateWriteResponse(SUCCESS)
    
    == Phase 4: Quorum Confirmation ==
    Coordinator -> Coordinator : count_confirmations()
    Coordinator -> Coordinator : check_quorum_achieved() ✓
    
    == Phase 5: Write Confirmation ==
    Coordinator -> Coordinator : mark_local_confirmed()
    
    par Confirm Replica2
        Coordinator -> Replica2 : ConfirmWrite(partition_seq)
    end
    par Confirm Replica3
        Coordinator -> Replica3 : ConfirmWrite(partition_seq)
    end
    
    Replica2 -> Replica2 : mark_confirmed()
    Replica3 -> Replica3 : mark_confirmed()
    
    alt Direct response (if client connected to coordinator)
        Coordinator -> Client : WriteResponse(SUCCESS, partition_seq)
    else Via forwarding node
        Coordinator -> AnyNode : WriteResponse(SUCCESS, partition_seq)
        AnyNode -> Client : WriteResponse(SUCCESS, partition_seq)
    end
    
    == Background: Failed Replica Recovery ==
    note over Replica1 : Replica1 initiates catch-up
    Replica1 -> Coordinator : PartitionSyncRequest(from_seq)
    Coordinator -> Replica1 : PartitionSyncResponse(missing_events)
    Replica1 -> Replica1 : apply_missing_events()
    Replica1 -> Replica1 : process_buffered_writes()
    
    @enduml
  7. Configure request timeouts in Kameo

    main

    Kameo provides mechanisms to control how long a request to an actor should wait before timing out. You can use the following types to manage this:

    • WithoutRequestTimeout: Represents a request that has no timeout set.
    • WithRequestTimeout: Represents a request with a specific timeout duration.
    • MaybeRequestTimeout: An internal enum used for remote messaging that can be either NoTimeout or Timeout(std::time::Duration).
    // Example of the internal timeout enum structure
    pub enum MaybeRequestTimeout {
        NoTimeout,
        Timeout(std::time::Duration),
    }
  8. Understand the `Reply` trait and error handling

    main

    The Reply trait defines how actors respond to messages.

    Key behaviors:

    • Result Integration: Implementing Reply for Result allows actor handler errors to be communicated back as SendError::HandlerError.
    • DelegatedReply: This type allows the actual reply to be managed asynchronously by another part of the system.
    • Asynchronous Errors: When using tell (asynchronous messaging) and an actor returns an error without a reply expectation, the error is treated as a panic within the actor. This triggers the actor's on_panic hook, which may restart or stop the actor (stopping it is the default behavior).
  9. Understand the SierraDB Write Protocol and Coordination

    main

    SierraDB uses a distributed actor-based model (via libp2p) to manage writes across a cluster. The write lifecycle involves several key stages:

    1. Routing: Write requests are routed via ExecuteTransaction to the appropriate coordinator.
    2. Replication: The coordinator handles replication requests to replicas using ReplicateWrite.
    3. Confirmation: Once replicas have processed the write, the coordinator uses ConfirmTransaction to finalize the write.

    Key safety mechanisms include:

    • Quorum Calculation: Logic to determine the required number of confirmations for a successful write.
    • Watermark Tracking: A system to track confirmed events per partition.
    • Circuit Breaker: Prevents cascading failures during high error rates.
    • Loop Prevention: Request forwarding includes logic to prevent infinite loops.
  10. Kameo Remote Actor Features

    main

    When the remote feature is enabled, Kameo supports distributed actor communication. The following types become available:

    • RemoteActor: An actor that resides on a different node.
    • RemoteActorRef: A reference used to communicate with a RemoteActor.
    • RemoteMessage: A message type specifically designed for transmission over the network.
    • RemoteSendError: Error type for failed remote communications.
  11. Optimize concurrent writes and manage buffers

    main

    To maintain high performance while ensuring sequential processing per partition, SierraDB employs several optimization strategies and buffer management rules.

    Concurrent Write Optimization

    The coordinator can work ahead of the replicas to maximize throughput:

    1. Immediate Sequence Assignment: The coordinator assigns sequence numbers without waiting for previous writes to complete.
    2. Parallel Replication: Replication requests are sent to all replicas simultaneously.
    3. Pipelined Coordination: The coordinator can start replicating write $N+1$ while write $N$ is still undergoing confirmation.
    4. Batch Confirmations: (Planned) Grouping multiple confirmations in a single message.

    Buffer Management

    Replicas use a sequential write buffer to handle out-of-order arrivals (sequence gaps). To prevent memory exhaustion, the following constraints are applied:

    • Max Buffer Size: A limit on the number of buffered writes (e.g., MAX_BUFFER_SIZE = 1000).
    • Buffer Timeout: Writes that stay in the buffer too long are evicted (e.g., BUFFER_TIMEOUT_MS = 5000).
    • Eviction Policy: When the buffer exceeds capacity, the system retains only the writes immediately following the next_expected_seq to ensure continuity.
    const MAX_BUFFER_SIZE: usize = 1000;
    const BUFFER_TIMEOUT_MS: u64 = 5000;
    
    // Evict old buffered writes to prevent memory issues
    fn evict_expired_writes(&mut self, partition_id: PartitionId) {
        let buffer = self.get_buffer_mut(partition_id);
        let now = Instant::now();
        
        buffer.buffered_writes.retain(|_, write| {
            now.duration_since(write.received_at).as_millis() < BUFFER_TIMEOUT_MS as u128
        });
        
        if buffer.buffered_writes.len() > MAX_BUFFER_SIZE {
            // Keep only the next N writes we expect
            let next_seq = buffer.next_expected_seq.load(Ordering::Acquire);
            buffer.buffered_writes = buffer.buffered_writes
                .range(next_seq..next_seq + MAX_BUFFER_SIZE as u64)
                .map(|(k, v)| (*k, v.clone()))
                .collect();
        }
    }
  12. Understand the SierraDB Distributed Write Protocol

    main

    SierraDB uses a three-phase distributed write protocol to ensure events are stored with strong consistency, monotonic versioning, and fault tolerance. The protocol follows these core principles:

    • Single Writer Per Partition: A designated leader serializes all writes for a specific partition.
    • Strong Consistency: Events maintain monotonic, gap-less versioning within both streams and partitions.
    • Quorum-Based Confirmation: Writes are only considered successful once a majority of replicas (defined as (replication_factor / 2) + 1) have confirmed the write.
    • Durability First: Events are written to disk immediately in an "unconfirmed" state before quorum is reached, ensuring data is not lost during node failures.

    The protocol moves through three distinct phases: Leader Selection & Version Assignment, Distributed Replication, and Commitment & Response.