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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ resolver = "2"
[workspace.package]
version = "0.0.1"
edition = "2024"
rust-version = "1.91.0"
rust-version = "1.95.0"
license = "MIT"
authors = ["Windsor Nguyen <oss@dedaluslabs.ai>"]
repository = "https://github.com/dedalus-labs/cloud9"
Expand Down
8 changes: 4 additions & 4 deletions clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ disallowed-methods = [
"tokio::fs::rename",
"tokio::fs::write",
"std::os::unix::fs::symlink",
"std::os::windows::fs::symlink_dir",
"std::os::windows::fs::symlink_file",
{ path = "std::os::windows::fs::symlink_dir", allow-invalid = true },
{ path = "std::os::windows::fs::symlink_file", allow-invalid = true },
"tokio::fs::symlink",
"tokio::fs::symlink_dir",
"tokio::fs::symlink_file",
{ path = "tokio::fs::symlink_dir", allow-invalid = true },
{ path = "tokio::fs::symlink_file", allow-invalid = true },
"std::thread::sleep",
]
2 changes: 1 addition & 1 deletion cloud9-node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ workspace = true

[dependencies]
cloud9-core = { workspace = true }
cloud9-consensus = { workspace = true }
cloud9-raft = { workspace = true }
cloud9-storage = { workspace = true }
cloud9-proto = { workspace = true }
tracing = { workspace = true }
Expand Down
10 changes: 2 additions & 8 deletions cloud9-node/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
//! Top-level orchestration for Cloud9 nodes.

use cloud9_consensus::ConsensusConfig;
use cloud9_raft::ConsensusConfig;
use cloud9_storage::StorageOptions;
use tracing::{info, instrument};

/// Runtime configuration derived from CLI flags and config files.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub struct NodeConfig {
pub storage: StorageOptions,
pub consensus: ConsensusConfig,
}

impl Default for NodeConfig {
fn default() -> Self {
Self { storage: StorageOptions::default(), consensus: ConsensusConfig::default() }
}
}

/// Launch the storage and consensus subsystems.
#[instrument(skip_all)]
pub async fn launch(config: NodeConfig) {
Expand Down
2 changes: 1 addition & 1 deletion cloud9/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ workspace = true
[dependencies]
cloud9-core = { workspace = true }
cloud9-storage = { workspace = true }
cloud9-consensus = { workspace = true }
cloud9-raft = { workspace = true }
cloud9-proto = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
Expand Down
8 changes: 4 additions & 4 deletions cloud9/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ fn main() -> Result<()> {
match cli.command {
Command::Start { config } => {
let config_path = config.unwrap_or_else(|| PathBuf::from("cloud9.toml"));
let maybe_config = load_config(config_path.clone())?;
let maybe_config = load_config(&config_path)?;
tracing::info!(path = %config_path.display(), "booting node");
if let Some(config) = maybe_config {
tracing::debug!(contents = %config, "loaded configuration");
Expand All @@ -56,7 +56,7 @@ fn main() -> Result<()> {
}
}
Command::CheckConfig { config } => {
load_config(config.clone())
load_config(&config)
.and_then(|contents| {
contents.ok_or_else(|| miette::miette!("config `{}` missing", config.display()))
})
Expand Down Expand Up @@ -89,9 +89,9 @@ fn init_tracing(verbosity: u8, color_enabled: bool) -> Result<()> {
tracing_subscriber::registry().with(filter).with(fmt_layer).try_init().into_diagnostic()
}

fn load_config(path: PathBuf) -> Result<Option<String>> {
fn load_config(path: &PathBuf) -> Result<Option<String>> {
if path.exists() {
fs::read_to_string(&path)
fs::read_to_string(path)
.into_diagnostic()
.map(Some)
.with_context(|| format!("reading configuration from `{}`", path.display()))
Expand Down
5 changes: 4 additions & 1 deletion consensus/cloud9-raft-io/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! I/O layer for cloud9-raft.
#![cfg_attr(test, allow(clippy::unwrap_used))]
//!
//! This crate provides the integration between the pure Raft state machine
//! (`cloud9-raft`) and the outside world. It defines traits for:
Expand All @@ -24,6 +25,8 @@ mod storage;
mod transport;

pub use read_index::{ReadId, ReadIndexCoordinator, ReadIndexError, ReadIndexResult};
pub use session::{ClientId, ClientSession, DuplicateCheck, SequenceNum, SessionRequest, SessionTracker};
pub use session::{
ClientId, ClientSession, DuplicateCheck, SequenceNum, SessionRequest, SessionTracker,
};
pub use storage::{SnapshotData, Storage, StorageError};
pub use transport::{Transport, TransportError};
14 changes: 4 additions & 10 deletions consensus/cloud9-raft-io/src/read_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
//! 2. Leader records the current commit index as the "read index"
//! 3. Leader sends a heartbeat round and waits for majority acknowledgment
//! 4. Once majority confirms, the leader is guaranteed to still be leader
//! 5. The read can proceed once the state machine has applied up to read_index
//! 5. The read can proceed once the state machine has applied up to `read_index`
//!
//! This module provides `ReadIndexCoordinator` which tracks pending reads and
//! completes them when heartbeat quorum is achieved.
Expand All @@ -33,7 +33,7 @@ impl ReadId {
/// A pending read request awaiting heartbeat confirmation.
#[derive(Debug)]
struct PendingRead {
/// The read index (commit_index when request was made).
/// The read index (`commit_index` when request was made).
read_index: LogIndex,
/// Heartbeat round this read is waiting on.
round: u64,
Expand Down Expand Up @@ -134,13 +134,7 @@ impl ReadIndexCoordinator {
/// The caller is responsible for checking `can_serve_reads()` before calling this.
pub fn request_read(&mut self, read_index: LogIndex) -> ReadId {
let id = self.next_read_id.next();
self.pending.insert(
id,
PendingRead {
read_index,
round: self.current_round,
},
);
self.pending.insert(id, PendingRead { read_index, round: self.current_round });
id
}

Expand All @@ -166,7 +160,7 @@ impl ReadIndexCoordinator {
/// Record a heartbeat acknowledgment from a peer.
///
/// Call this when an `AppendResponse { success: true }` is received
/// from a heartbeat (empty AppendEntries).
/// from a heartbeat (empty `AppendEntries`).
pub fn record_ack(&mut self, from: NodeId) {
if !self.round_acks.contains(&from) && self.voters.contains(&from) {
self.round_acks.push(from);
Expand Down
39 changes: 8 additions & 31 deletions consensus/cloud9-raft-io/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
//!
//! The solution is to track client sessions:
//! 1. Clients register and receive unique IDs
//! 2. Each request carries (client_id, sequence_number)
//! 2. Each request carries (`client_id`, `sequence_number`)
//! 3. The state machine tracks the last completed sequence per client
//! 4. Duplicate requests return cached responses
//!
Expand Down Expand Up @@ -95,10 +95,7 @@ pub struct ClientSession<R> {

impl<R> Default for ClientSession<R> {
fn default() -> Self {
Self {
last_sequence: 0,
last_response: None,
}
Self { last_sequence: 0, last_response: None }
}
}

Expand Down Expand Up @@ -138,10 +135,7 @@ pub struct SessionTracker<R> {
impl<R: Clone> SessionTracker<R> {
/// Create a new session tracker.
pub fn new() -> Self {
Self {
sessions: BTreeMap::new(),
next_client_id: 1,
}
Self { sessions: BTreeMap::new(), next_client_id: 1 }
}

/// Register a new client session.
Expand Down Expand Up @@ -185,12 +179,7 @@ impl<R: Clone> SessionTracker<R> {
///
/// Call this after executing a command. Caches the response for
/// duplicate detection.
pub fn record_completion(
&mut self,
client_id: ClientId,
sequence: SequenceNum,
response: R,
) {
pub fn record_completion(&mut self, client_id: ClientId, sequence: SequenceNum, response: R) {
let session = self.sessions.entry(client_id).or_default();
if sequence > session.last_sequence {
session.last_sequence = sequence;
Expand Down Expand Up @@ -244,10 +233,7 @@ mod tests {
let mut tracker: SessionTracker<String> = SessionTracker::new();
let client = tracker.register_client();

assert!(matches!(
tracker.check_duplicate(client, 1),
DuplicateCheck::New
));
assert!(matches!(tracker.check_duplicate(client, 1), DuplicateCheck::New));
}

#[test]
Expand All @@ -274,10 +260,7 @@ mod tests {
tracker.record_completion(client, 5, "response5".to_string());

// Sequence 3 is stale
assert!(matches!(
tracker.check_duplicate(client, 3),
DuplicateCheck::Stale
));
assert!(matches!(tracker.check_duplicate(client, 3), DuplicateCheck::Stale));
}

#[test]
Expand All @@ -288,10 +271,7 @@ mod tests {
tracker.record_completion(client, 1, "response1".to_string());

// Sequence 2 is new
assert!(matches!(
tracker.check_duplicate(client, 2),
DuplicateCheck::New
));
assert!(matches!(tracker.check_duplicate(client, 2), DuplicateCheck::New));
}

#[test]
Expand All @@ -309,10 +289,7 @@ mod tests {
let tracker: SessionTracker<String> = SessionTracker::new();
let unknown = ClientId(999);

assert!(matches!(
tracker.check_duplicate(unknown, 1),
DuplicateCheck::New
));
assert!(matches!(tracker.check_duplicate(unknown, 1), DuplicateCheck::New));
}

#[test]
Expand Down
17 changes: 13 additions & 4 deletions consensus/cloud9-raft-io/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,28 @@ pub trait Storage: Send + Sync {
///
/// Per §3.8, this must complete before responding to any RPC.
/// If `sync` is true, the implementation should fsync to ensure durability.
fn save(&self, state: &Persistent, sync: bool) -> impl Future<Output = Result<(), StorageError>> + Send;
fn save(
&self,
state: &Persistent,
sync: bool,
) -> impl Future<Output = Result<(), StorageError>> + Send;

/// Load snapshot data for transfer to a slow follower (§5).
///
/// Returns the snapshot data as bytes. The format is opaque to Raft —
/// the state machine defines it.
fn load_snapshot(&self) -> impl Future<Output = Result<Option<SnapshotData>, StorageError>> + Send;
fn load_snapshot(
&self,
) -> impl Future<Output = Result<Option<SnapshotData>, StorageError>> + Send;

/// Save snapshot data received from the leader (§5).
///
/// Called when a follower receives InstallSnapshot. The implementation
/// Called when a follower receives `InstallSnapshot`. The implementation
/// should atomically replace any existing snapshot.
fn save_snapshot(&self, snapshot: SnapshotData) -> impl Future<Output = Result<(), StorageError>> + Send;
fn save_snapshot(
&self,
snapshot: SnapshotData,
) -> impl Future<Output = Result<(), StorageError>> + Send;
}

/// Snapshot data for transfer between nodes (§5).
Expand Down
7 changes: 5 additions & 2 deletions consensus/cloud9-raft-io/src/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

use std::future::Future;

use cloud9_raft::raft::Message;
use cloud9_raft::NodeId;
use cloud9_raft::raft::Message;
use thiserror::Error;

/// Errors from transport operations.
Expand Down Expand Up @@ -58,7 +58,10 @@ pub trait Transport: Send + Sync {
///
/// Called when configuration changes. The transport should establish
/// connections to new nodes and may close connections to removed nodes.
fn update_peers(&self, peers: &[NodeId]) -> impl Future<Output = Result<(), TransportError>> + Send;
fn update_peers(
&self,
peers: &[NodeId],
) -> impl Future<Output = Result<(), TransportError>> + Send;
}

#[cfg(test)]
Expand Down
10 changes: 9 additions & 1 deletion consensus/cloud9-raft/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
#![forbid(unsafe_code)]
#![deny(clippy::unwrap_used, clippy::expect_used)]
#![cfg_attr(any(test, doctest), allow(clippy::unwrap_used, clippy::expect_used))]
#![cfg_attr(
any(test, doctest),
allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::cast_possible_truncation
)
)]

//! Consensus drivers for Cloud9 clusters.
//!
Expand Down
Loading
Loading