Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions crates/ember-cluster/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,12 @@ readme = "README.md"

[dependencies]
thiserror = { workspace = true }
tokio = { workspace = true }
bytes = { workspace = true }
rand = { workspace = true }
tracing = { workspace = true }
serde = { version = "1", features = ["derive"] }
uuid = { version = "1", features = ["v4", "serde"] }

[dev-dependencies]
tokio = { workspace = true, features = ["test-util", "macros"] }
66 changes: 66 additions & 0 deletions crates/ember-cluster/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//! Error types for cluster operations.

use std::net::SocketAddr;

use crate::NodeId;

/// Errors that can occur during cluster operations.
#[derive(Debug, thiserror::Error)]
pub enum ClusterError {
/// The slot is not assigned to any node.
#[error("slot {0} is not assigned to any node")]
SlotNotAssigned(u16),

/// The key belongs to a slot on a different node.
#[error("MOVED {slot} {addr}")]
Moved { slot: u16, addr: SocketAddr },

/// The slot is being migrated; client should retry with ASK.
#[error("ASK {slot} {addr}")]
Ask { slot: u16, addr: SocketAddr },

/// Node not found in the cluster.
#[error("node {0} not found in cluster")]
NodeNotFound(NodeId),

/// Cluster is not in a healthy state.
#[error("cluster is down")]
ClusterDown,

/// Operation requires a different node role.
#[error("operation not supported on {role} node")]
WrongRole { role: String },

/// Cross-slot operation with keys in different slots.
#[error("cross-slot keys not allowed (keys span slots {0} and {1})")]
CrossSlot(u16, u16),

/// Network error during cluster communication.
#[error("cluster communication error: {0}")]
Network(String),

/// Timeout waiting for cluster operation.
#[error("cluster operation timed out")]
Timeout,

/// Configuration error.
#[error("invalid cluster configuration: {0}")]
Configuration(String),
}

impl ClusterError {
/// Returns true if this is a redirect error (MOVED or ASK).
pub fn is_redirect(&self) -> bool {
matches!(self, ClusterError::Moved { .. } | ClusterError::Ask { .. })
}

/// Creates a MOVED error for a slot redirect.
pub fn moved(slot: u16, addr: SocketAddr) -> Self {
ClusterError::Moved { slot, addr }
}

/// Creates an ASK error for a slot migration redirect.
pub fn ask(slot: u16, addr: SocketAddr) -> Self {
ClusterError::Ask { slot, addr }
}
}
40 changes: 37 additions & 3 deletions crates/ember-cluster/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,38 @@
//! ember-cluster: distributed coordination.
//! ember-cluster: distributed coordination for ember.
//!
//! Manages cluster topology, gossip-based failure detection,
//! raft consensus, and live slot migration.
//! This crate provides the building blocks for running ember as a distributed
//! cluster with automatic failover and horizontal scaling.
//!
//! # Architecture
//!
//! The cluster layer sits between the protocol layer and the storage engine,
//! handling:
//!
//! - **Slot management**: 16384 hash slots distributed across nodes
//! - **Topology tracking**: Node membership and health monitoring
//! - **Failure detection**: SWIM gossip protocol for quick detection
//! - **Consensus**: Raft for cluster configuration changes
//! - **Migration**: Live slot resharding without downtime
//!
//! # Quick Start
//!
//! ```rust,ignore
//! use ember_cluster::{ClusterState, ClusterNode, NodeId, key_slot};
//!
//! // Create a single-node cluster
//! let node_id = NodeId::new();
//! let node = ClusterNode::new_primary(node_id, "127.0.0.1:6379".parse().unwrap());
//! let cluster = ClusterState::single_node(node);
//!
//! // Route a key to its slot
//! let slot = key_slot(b"mykey");
//! assert!(cluster.owns_slot(slot));
//! ```

mod error;
mod slots;
mod topology;

pub use error::ClusterError;
pub use slots::{key_slot, SlotMap, SlotRange, SLOT_COUNT};
pub use topology::{ClusterHealth, ClusterNode, ClusterState, NodeFlags, NodeId, NodeRole};
Loading