diff --git a/Cargo.toml b/Cargo.toml index f9f6ff27..9ef143e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ exclude = [ "examples/rocksdb-cluster", "examples/sled-cluster", "examples/three-nodes-cluster", + "examples/single-node-expansion", "benches/d-engine-bench", ] diff --git a/d-engine-core/src/election/election_handler.rs b/d-engine-core/src/election/election_handler.rs index 963b26aa..475d7cfd 100644 --- a/d-engine-core/src/election/election_handler.rs +++ b/d-engine-core/src/election/election_handler.rs @@ -48,17 +48,26 @@ where ) -> Result<()> { debug!("broadcast_vote_requests..."); + // Single-node cluster: no peers to vote, automatically win election + if membership.is_single_node_cluster().await { + debug!( + "Single-node cluster detected (node_id={}): automatically winning election", + self.my_id + ); + return Ok(()); + } + let members = membership.voters().await; if members.is_empty() { - error!("my(id={}) peers is empty.", self.my_id); + error!("No voting members found for node {}", self.my_id); return Err(ElectionError::NoVotingMemberFound { candidate_id: self.my_id, } .into()); - } else { - debug!("going to send_vote_requests to: {:?}", &members); } + debug!("Sending vote requests to peers: {:?}", &members); + let LogId { index: last_log_index, term: last_log_term, diff --git a/d-engine-core/src/election/election_handler_test.rs b/d-engine-core/src/election/election_handler_test.rs index 63a408fb..c130fb61 100644 --- a/d-engine-core/src/election/election_handler_test.rs +++ b/d-engine-core/src/election/election_handler_test.rs @@ -576,3 +576,156 @@ async fn test_handle_vote_request_large_term_numbers() { // Assert assert_eq!(state_update.term_update, Some(large_term)); } + +// ================================================================================================ +// Tests for Single-Node Cluster Support (Issue #179) +// ================================================================================================ + +#[cfg(test)] +mod single_node_election_tests { + use super::*; + use crate::ElectionError; + use crate::Error; + use crate::MockMembership; + use crate::MockTransport; + use crate::RaftNodeConfig; + use crate::VoteResult; + use d_engine_proto::server::cluster::NodeMeta; + use d_engine_proto::server::election::VoteResponse; + use std::collections::HashSet; + + #[tokio::test] + async fn test_single_node_auto_wins_election() { + // Arrange + let handler = ElectionHandler::::new(1); + let mut mock_membership = MockMembership::new(); + + // Mock: is_single_node_cluster() returns true for single-node + mock_membership.expect_is_single_node_cluster().times(1).returning(|| true); + + // voters() should NOT be called (early return before this check) + mock_membership.expect_voters().times(0); + + let membership = Arc::new(mock_membership); + let raft_log = Arc::new(create_mock_raft_log(None)); + let mock_transport = MockTransport::new(); + let transport = Arc::new(mock_transport); + let settings = Arc::new(RaftNodeConfig::default()); + + // Act + let result = handler + .broadcast_vote_requests(1, membership, &raft_log, &transport, &settings) + .await; + + // Assert + assert!( + result.is_ok(), + "Single-node should automatically win election" + ); + } + + #[tokio::test] + async fn test_three_node_cluster_goes_through_normal_election() { + // Arrange + let handler = ElectionHandler::::new(1); + let mut mock_membership = MockMembership::new(); + + // Mock: is_single_node_cluster() returns false for multi-node + mock_membership.expect_is_single_node_cluster().times(1).returning(|| false); + + // Mock: voters() returns 2 peers + mock_membership.expect_voters().times(1).returning(|| { + vec![ + NodeMeta { + id: 2, + address: "127.0.0.1:9082".to_string(), + role: 0, + status: 2, + }, + NodeMeta { + id: 3, + address: "127.0.0.1:9083".to_string(), + role: 0, + status: 2, + }, + ] + }); + + let membership = Arc::new(mock_membership); + let raft_log = Arc::new(create_mock_raft_log(None)); + + let mut mock_transport = MockTransport::new(); + // Mock transport to return majority votes + mock_transport.expect_send_vote_requests().times(1).returning( + |_req, _retry, _membership| { + Ok(VoteResult { + peer_ids: HashSet::from([2, 3]), + responses: vec![ + Ok(VoteResponse { + term: 1, + vote_granted: true, + last_log_index: 0, + last_log_term: 0, + }), + Ok(VoteResponse { + term: 1, + vote_granted: true, + last_log_index: 0, + last_log_term: 0, + }), + ], + }) + }, + ); + + let transport = Arc::new(mock_transport); + let settings = Arc::new(RaftNodeConfig::default()); + + // Act + let result = handler + .broadcast_vote_requests(1, membership, &raft_log, &transport, &settings) + .await; + + // Assert + assert!( + result.is_ok(), + "Three-node cluster should complete normal election" + ); + } + + #[tokio::test] + async fn test_network_partition_with_empty_voters_still_reports_error() { + // Arrange + let handler = ElectionHandler::::new(1); + let mut mock_membership = MockMembership::new(); + + // Mock: is_single_node_cluster() returns false for multi-node (network partition scenario) + mock_membership.expect_is_single_node_cluster().times(1).returning(|| false); + + // Mock: voters() returns empty (network partition) + mock_membership.expect_voters().times(1).returning(Vec::new); + + let membership = Arc::new(mock_membership); + let raft_log = Arc::new(create_mock_raft_log(None)); + let mock_transport = MockTransport::new(); + let transport = Arc::new(mock_transport); + let settings = Arc::new(RaftNodeConfig::default()); + + // Act + let result = handler + .broadcast_vote_requests(1, membership, &raft_log, &transport, &settings) + .await; + + // Assert + assert!(result.is_err(), "Network partition should return error"); + assert!( + matches!( + result.unwrap_err(), + Error::Consensus(crate::ConsensusError::Election( + ElectionError::NoVotingMemberFound { .. } + )) + ), + "Should return NoVotingMemberFound error" + ); + } +} diff --git a/d-engine-core/src/membership.rs b/d-engine-core/src/membership.rs index deefde01..a0056a7b 100644 --- a/d-engine-core/src/membership.rs +++ b/d-engine-core/src/membership.rs @@ -49,6 +49,40 @@ where /// All non-self nodes in Active state async fn voters(&self) -> Vec; + /// Get the initial cluster size from configuration + /// + /// This value is determined at node startup from the `initial_cluster` configuration + /// and remains constant throughout the node's lifetime. It represents the designed + /// cluster size, not the current runtime membership state. + /// + /// Used for: + /// - Quorum calculations + /// - Cluster topology decisions + /// + /// # Safety + /// This method is safe to use for cluster topology decisions as it's based on + /// immutable configuration rather than runtime state that could be affected by + /// network partitions or bugs. + async fn initial_cluster_size(&self) -> usize; + + /// Check if this is a single-node cluster + /// + /// Returns `true` if the initial cluster size is 1, indicating this node + /// was configured to run in standalone mode without any peers. + /// + /// This is a convenience method equivalent to `initial_cluster_size() == 1`. + /// + /// # Use Cases + /// - Skip Raft election in single-node mode (no peers to vote) + /// - Skip log replication in single-node mode (no peers to replicate to) + /// - Optimize performance by avoiding unnecessary network operations + /// + /// # Safety + /// Safe for all cluster topology decisions as it's based on immutable configuration. + async fn is_single_node_cluster(&self) -> bool { + self.initial_cluster_size().await == 1 + } + /// All pending active nodes in Active state #[allow(unused)] async fn nodes_with_status( diff --git a/d-engine-core/src/raft_role/leader_state.rs b/d-engine-core/src/raft_role/leader_state.rs index c3360bd6..74382f65 100644 --- a/d-engine-core/src/raft_role/leader_state.rs +++ b/d-engine-core/src/raft_role/leader_state.rs @@ -1501,6 +1501,18 @@ impl LeaderState { if !ready_learners.is_empty() { debug!("Ready learners: {:?}", ready_learners); + + // Print promotion messages for each ready learner (Plan B) + for learner_id in &ready_learners { + let match_index = learner_progress.get(learner_id).and_then(|mi| *mi).unwrap_or(0); + crate::utils::cluster_printer::print_leader_promoting_learner( + self.node_id(), + *learner_id, + match_index, + leader_commit_index, + ); + } + // Add to pending queue let promotions = ready_learners .into_iter() @@ -1867,6 +1879,15 @@ impl LeaderState { "Node {} ({}) successfully added as learner", node_id, address ); + + // Print leader accepting new node message (Plan B) + crate::utils::cluster_printer::print_leader_accepting_new_node( + self.node_id(), + node_id, + address, + d_engine_proto::common::NodeRole::Learner as i32, + ); + Ok(()) } diff --git a/d-engine-core/src/raft_role/learner_state.rs b/d-engine-core/src/raft_role/learner_state.rs index 9494deda..285ab782 100644 --- a/d-engine-core/src/raft_role/learner_state.rs +++ b/d-engine-core/src/raft_role/learner_state.rs @@ -126,6 +126,10 @@ impl RaftRoleState for LearnerState { self.node_id(), self.current_term(), ); + + // Print promotion message (Plan B) + crate::utils::cluster_printer::print_learner_promoted_to_voter(self.node_id()); + Ok(RaftRole::Follower(Box::new(self.into()))) } fn become_learner(&self) -> Result> { @@ -485,6 +489,12 @@ impl RaftRoleState for LearnerState { // 4. mark leader_id membership.mark_leader_id(leader_id).await?; + // Print join success message (Plan B) + crate::utils::cluster_printer::print_learner_join_success( + self.shared_state.node_id, + leader_id, + ); + Ok(()) } diff --git a/d-engine-core/src/replication/replication_handler.rs b/d-engine-core/src/replication/replication_handler.rs index 4ddc288e..221d17fd 100644 --- a/d-engine-core/src/replication/replication_handler.rs +++ b/d-engine-core/src/replication/replication_handler.rs @@ -73,6 +73,20 @@ where // Phase 1: Pre-Checks // ---------------------- let membership = ctx.membership(); + + // Single-node cluster: no replication needed, quorum automatically achieved + if membership.is_single_node_cluster().await { + debug!( + "Single-node cluster (leader={}): no replication needed, quorum automatically achieved", + self.my_id + ); + return Ok(AppendResults { + commit_quorum_achieved: true, + peer_updates: HashMap::new(), + learner_progress: HashMap::new(), + }); + } + let replication_targets = membership.replication_peers().await; if replication_targets.is_empty() { warn!("no peer found for leader({})", self.my_id); diff --git a/d-engine-core/src/test_utils/mock/mock_raft_builder.rs b/d-engine-core/src/test_utils/mock/mock_raft_builder.rs index a97fc0ef..cbe7ace4 100644 --- a/d-engine-core/src/test_utils/mock/mock_raft_builder.rs +++ b/d-engine-core/src/test_utils/mock/mock_raft_builder.rs @@ -396,6 +396,9 @@ pub fn mock_membership() -> MockMembership { membership.expect_get_peers_id_with_condition().returning(|_| vec![]); // Mock methods used by create_not_leader_response() membership.expect_current_leader_id().returning(|| None); + // Mock single-node cluster detection (default to multi-node with no peers) + membership.expect_is_single_node_cluster().returning(|| false); + membership.expect_initial_cluster_size().returning(|| 3); membership } diff --git a/d-engine-core/src/utils/cluster_printer.rs b/d-engine-core/src/utils/cluster_printer.rs new file mode 100644 index 00000000..7a103851 --- /dev/null +++ b/d-engine-core/src/utils/cluster_printer.rs @@ -0,0 +1,339 @@ +// Cluster Status Printer Module +// +// This module provides utilities for printing clear, user-friendly cluster status +// information to help administrators understand the cluster state at a glance. +// +// Implements: +// - Plan A: Enhanced startup output with cluster membership table +// - Plan B: Clear role transition messages +// +// Context for Plan D (CLI tool): +// - These formatting functions can be reused by the future CLI tool +// - The data structures used here match what the CLI will need to query +// - Emoji and color codes can be made configurable for different terminals + +use crate::{Membership, TypeConfig}; +use d_engine_proto::common::NodeRole; + +/// Get emoji representation for node role +/// +/// Maps NodeRole enum values to emoji: +/// - Follower (0): ๐Ÿ“ฎ +/// - Candidate (1): ๐Ÿ—ณ๏ธ +/// - Leader (2): ๐Ÿ‘‘ +/// - Learner (3): ๐ŸŽ“ +pub fn role_emoji(role: i32) -> &'static str { + match role { + 0 => "๐Ÿ“ฎ", // Follower + 1 => "๐Ÿ—ณ๏ธ", // Candidate + 2 => "๐Ÿ‘‘", // Leader + 3 => "๐ŸŽ“", // Learner + _ => "โ“", // Unknown/Invalid + } +} + +/// Get human-readable role name +/// +/// Maps NodeRole enum values to display names: +/// - Follower (0) +/// - Candidate (1) +/// - Leader (2) +/// - Learner (3) +pub fn role_name(role: i32) -> &'static str { + match role { + 0 => "Follower", + 1 => "Candidate", + 2 => "Leader", + 3 => "Learner", + _ => "Unknown", + } +} + +/// Get emoji representation for node status +pub fn status_emoji(status: i32) -> &'static str { + match status { + 0 => "๐Ÿ”„", // Joining + 1 => "๐Ÿ”„", // Syncing + 2 => "โœ…", // Active + 3 => "โŒ", // Inactive + _ => "โ“", + } +} + +/// Get human-readable status name +pub fn status_name(status: i32) -> &'static str { + match status { + 0 => "Joining", + 1 => "Syncing", + 2 => "Active", + 3 => "Inactive", + _ => "Unknown", + } +} + +/// Print the cluster membership table +/// +/// Example output: +/// ```text +/// ================================================================================ +/// CLUSTER MEMBERSHIP: +/// Node ID Address Role Status +/// ------------------------------------------------------------ +/// 1 0.0.0.0:9081 ๐Ÿ‘‘ Leader โœ… Active +/// 2 0.0.0.0:9082 ๐ŸŽ“ Learner ๐Ÿ”„ Syncing +/// ================================================================================ +/// ``` +pub async fn print_cluster_membership_table>(membership: &M) { + let members = membership.get_all_nodes().await; + + println!("\n{}", "=".repeat(80)); + println!(" CLUSTER MEMBERSHIP:"); + println!( + " {:<8} {:<20} {:<18} {:<15}", + "Node ID", "Address", "Role", "Status" + ); + println!(" {}", "-".repeat(80)); + + for member in members { + println!( + " {:<8} {:<20} {:<18} {:<15}", + member.id, + member.address, + format!("{} {}", role_emoji(member.role), role_name(member.role)), + format!( + "{} {}", + status_emoji(member.status), + status_name(member.status) + ) + ); + } + + println!("{}", "=".repeat(80)); +} + +/// Print node startup banner with full status +/// +/// This is the main entry point for Plan A: Enhanced startup output +/// +/// Example output: +/// ```text +/// ================================================================================ +/// D-ENGINE NODE STARTED +/// ================================================================================ +/// Node ID: 2 +/// Listen Address: 0.0.0.0:9082 +/// Current Role: ๐ŸŽ“ Learner +/// Current Term: 2 +/// Leader ID: 1 +/// ================================================================================ +/// CLUSTER MEMBERSHIP: +/// ...membership table... +/// ================================================================================ +/// ``` +pub async fn print_node_startup_banner>( + node_id: u32, + listen_addr: &str, + current_role: i32, + current_term: u64, + leader_id: Option, + membership: &M, +) { + println!("\n{}", "=".repeat(80)); + println!(" D-ENGINE NODE STARTED"); + println!("{}", "=".repeat(80)); + println!(" Node ID: {node_id}"); + println!(" Listen Address: {listen_addr}"); + println!( + " Current Role: {} {}", + role_emoji(current_role), + role_name(current_role) + ); + println!(" Current Term: {current_term}"); + println!( + " Leader ID: {}", + leader_id.map(|id| id.to_string()).unwrap_or_else(|| "None".to_string()) + ); + println!("{}", "=".repeat(80)); + + print_cluster_membership_table(membership).await; +} + +/// Print learner join success message +/// +/// Part of Plan B: Clear role transition messages +/// +/// Example output: +/// ```text +/// ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰ +/// โœ… NODE 2 SUCCESSFULLY JOINED CLUSTER +/// Role: ๐ŸŽ“ Learner โ†’ Syncing data from Leader 1 +/// Next: Will auto-promote to Voter when caught up +/// ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰ +/// ``` +pub fn print_learner_join_success( + node_id: u32, + leader_id: u32, +) { + println!("\n{}", "๐ŸŽ‰".repeat(40)); + println!(" โœ… NODE {node_id} SUCCESSFULLY JOINED CLUSTER"); + println!( + " Role: {} Learner โ†’ Syncing data from Leader {leader_id}", + role_emoji(NodeRole::Learner as i32) + ); + println!(" Next: Will auto-promote to Voter when caught up"); + println!("{}\n", "๐ŸŽ‰".repeat(40)); +} + +/// Print learner promotion success message +/// +/// Part of Plan B: Clear role transition messages +/// +/// Example output: +/// ```text +/// ๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€ +/// ๐ŸŽŠ NODE 2 PROMOTED TO VOTER! +/// Old Role: ๐ŸŽ“ Learner +/// New Role: ๐Ÿ“ฎ Follower (Voter) +/// Status: Can now participate in voting and consensus +/// ๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€ +/// ``` +pub fn print_learner_promoted_to_voter(node_id: u32) { + println!("\n{}", "๐Ÿš€".repeat(40)); + println!(" ๐ŸŽŠ NODE {node_id} PROMOTED TO VOTER!"); + println!( + " Old Role: {} Learner", + role_emoji(NodeRole::Learner as i32) + ); + println!( + " New Role: {} Follower (Voter)", + role_emoji(NodeRole::Follower as i32) + ); + println!(" Status: Can now participate in voting and consensus"); + println!("{}\n", "๐Ÿš€".repeat(40)); +} + +/// Print role transition message +/// +/// Part of Plan B: Clear role transition messages +/// +/// This is called whenever a node changes roles (Followerโ†’Candidate, Candidateโ†’Leader, etc.) +/// +/// Example output: +/// ```text +/// ================================================================================ +/// ๐Ÿ”„ ROLE TRANSITION +/// ================================================================================ +/// Node ID: 1 +/// From Role: ๐Ÿ“ฎ Follower +/// To Role: ๐Ÿ‘‘ Leader +/// Term: 2 +/// Reason: Won election with majority votes +/// ================================================================================ +/// ``` +pub fn print_role_transition( + node_id: u32, + from_role: i32, + to_role: i32, + term: u64, + reason: &str, +) { + println!("\n{}", "=".repeat(80)); + println!(" ๐Ÿ”„ ROLE TRANSITION"); + println!("{}", "=".repeat(80)); + println!(" Node ID: {node_id}"); + println!( + " From Role: {} {}", + role_emoji(from_role), + role_name(from_role) + ); + println!( + " To Role: {} {}", + role_emoji(to_role), + role_name(to_role) + ); + println!(" Term: {term}"); + println!(" Reason: {reason}"); + println!("{}", "=".repeat(80)); +} + +/// Print leader accepting new node message +/// +/// Part of Plan B: Clear role transition messages +/// +/// Called on the leader when a new node joins +/// +/// Example output: +/// ```text +/// ================================================================================ +/// โœ… LEADER: ACCEPTING NEW NODE +/// ================================================================================ +/// Leader ID: 1 +/// New Node ID: 2 +/// New Node Addr: 0.0.0.0:9082 +/// Role: ๐ŸŽ“ Learner +/// Action: AddNode config change committed +/// ================================================================================ +/// ``` +pub fn print_leader_accepting_new_node( + leader_id: u32, + new_node_id: u32, + new_node_addr: &str, + role: i32, +) { + println!("\n{}", "=".repeat(80)); + println!(" โœ… LEADER: ACCEPTING NEW NODE"); + println!("{}", "=".repeat(80)); + println!(" Leader ID: {leader_id}"); + println!(" New Node ID: {new_node_id}"); + println!(" New Node Addr: {new_node_addr}"); + println!( + " Role: {} {}", + role_emoji(role), + role_name(role) + ); + println!(" Action: AddNode config change committed"); + println!("{}", "=".repeat(80)); +} + +/// Print leader triggering learner promotion message +/// +/// Part of Plan B: Clear role transition messages +/// +/// Called on the leader when it decides to promote a learner +/// +/// Example output: +/// ```text +/// ================================================================================ +/// ๐Ÿš€ LEADER: PROMOTING LEARNER +/// ================================================================================ +/// Leader ID: 1 +/// Learner ID: 2 +/// Learner Match Index: 150 +/// Leader Commit Index: 150 +/// Progress: 100.0% +/// Action: Scheduling PromoteToVoter config change +/// ================================================================================ +/// ``` +pub fn print_leader_promoting_learner( + leader_id: u32, + learner_id: u32, + match_index: u64, + commit_index: u64, +) { + let progress = if commit_index > 0 { + (match_index as f64 / commit_index as f64) * 100.0 + } else { + 100.0 + }; + + println!("\n{}", "=".repeat(80)); + println!(" ๐Ÿš€ LEADER: PROMOTING LEARNER"); + println!("{}", "=".repeat(80)); + println!(" Leader ID: {leader_id}"); + println!(" Learner ID: {learner_id}"); + println!(" Learner Match Index: {match_index}"); + println!(" Leader Commit Index: {commit_index}"); + println!(" Progress: {progress:.1}%"); + println!(" Action: Scheduling PromoteToVoter config change"); + println!("{}", "=".repeat(80)); +} diff --git a/d-engine-core/src/utils/cluster_printer_test.rs b/d-engine-core/src/utils/cluster_printer_test.rs new file mode 100644 index 00000000..02d5a955 --- /dev/null +++ b/d-engine-core/src/utils/cluster_printer_test.rs @@ -0,0 +1,96 @@ +use super::cluster_printer::*; +use d_engine_proto::common::NodeRole; + +#[test] +fn test_role_emoji_all_variants() { + assert_eq!(role_emoji(NodeRole::Follower as i32), "๐Ÿ“ฎ"); + assert_eq!(role_emoji(NodeRole::Candidate as i32), "๐Ÿ—ณ๏ธ"); + assert_eq!(role_emoji(NodeRole::Leader as i32), "๐Ÿ‘‘"); + assert_eq!(role_emoji(NodeRole::Learner as i32), "๐ŸŽ“"); + assert_eq!(role_emoji(999), "โ“"); // Invalid role +} + +#[test] +fn test_role_name_all_variants() { + assert_eq!(role_name(NodeRole::Follower as i32), "Follower"); + assert_eq!(role_name(NodeRole::Candidate as i32), "Candidate"); + assert_eq!(role_name(NodeRole::Leader as i32), "Leader"); + assert_eq!(role_name(NodeRole::Learner as i32), "Learner"); + assert_eq!(role_name(999), "Unknown"); // Invalid role +} + +#[test] +fn test_status_emoji_all_variants() { + assert_eq!(status_emoji(0), "๐Ÿ”„"); // Joining + assert_eq!(status_emoji(1), "๐Ÿ”„"); // Syncing + assert_eq!(status_emoji(2), "โœ…"); // Active + assert_eq!(status_emoji(3), "โŒ"); // Inactive + assert_eq!(status_emoji(999), "โ“"); // Invalid status +} + +#[test] +fn test_status_name_all_variants() { + assert_eq!(status_name(0), "Joining"); + assert_eq!(status_name(1), "Syncing"); + assert_eq!(status_name(2), "Active"); + assert_eq!(status_name(3), "Inactive"); + assert_eq!(status_name(999), "Unknown"); // Invalid status +} + +#[test] +fn test_role_emoji_consistency_with_proto() { + // Ensure our emoji mapping matches proto enum values + assert_eq!(NodeRole::Follower as i32, 0); + assert_eq!(NodeRole::Candidate as i32, 1); + assert_eq!(NodeRole::Leader as i32, 2); + assert_eq!(NodeRole::Learner as i32, 3); +} + +#[test] +fn test_role_and_emoji_pairing() { + let roles = [ + (NodeRole::Leader as i32, "๐Ÿ‘‘", "Leader"), + (NodeRole::Follower as i32, "๐Ÿ“ฎ", "Follower"), + (NodeRole::Learner as i32, "๐ŸŽ“", "Learner"), + ]; + + for (role_val, expected_emoji, expected_name) in roles { + assert_eq!(role_emoji(role_val), expected_emoji); + assert_eq!(role_name(role_val), expected_name); + } +} + +#[test] +fn test_status_progression() { + // Test typical status progression: Joining -> Syncing -> Active + let statuses = [(0, "Joining"), (1, "Syncing"), (2, "Active")]; + + for (status_val, expected_name) in statuses { + assert_eq!(status_name(status_val), expected_name); + // Joining and Syncing both use spinning emoji + if status_val < 2 { + assert_eq!(status_emoji(status_val), "๐Ÿ”„"); + } + } +} + +#[test] +fn test_edge_cases() { + // Test negative values (shouldn't happen but should handle gracefully) + assert_eq!(role_emoji(-1), "โ“"); + assert_eq!(role_name(-1), "Unknown"); + assert_eq!(status_emoji(-1), "โ“"); + assert_eq!(status_name(-1), "Unknown"); +} + +#[test] +fn test_output_formatting_consistency() { + // Ensure emoji strings are single characters (for alignment) + let emojis = ["โ“", "๐Ÿ“ฎ", "๐Ÿ‘‘", "๐ŸŽ“", "๐Ÿ”„", "โœ…", "โŒ"]; + for emoji in emojis { + // Each emoji should be non-empty + assert!(!emoji.is_empty()); + // Emojis are multi-byte but should be single grapheme + assert_eq!(emoji.chars().count(), 1); + } +} diff --git a/d-engine-core/src/utils/mod.rs b/d-engine-core/src/utils/mod.rs index aa0f4b85..5d4d86b0 100644 --- a/d-engine-core/src/utils/mod.rs +++ b/d-engine-core/src/utils/mod.rs @@ -1,10 +1,13 @@ pub mod cluster; +pub mod cluster_printer; pub mod convert; pub mod file_io; pub mod scoped_timer; pub mod stream; pub mod time; +#[cfg(test)] +mod cluster_printer_test; #[cfg(test)] mod convert_test; #[cfg(test)] diff --git a/d-engine-server/src/membership/raft_membership.rs b/d-engine-server/src/membership/raft_membership.rs index 23bc92de..017db63e 100644 --- a/d-engine-server/src/membership/raft_membership.rs +++ b/d-engine-server/src/membership/raft_membership.rs @@ -59,6 +59,8 @@ where node_id: u32, membership: MembershipGuard, config: RaftNodeConfig, + /// Initial cluster size from configuration (immutable after startup) + initial_cluster_size: usize, pub(super) health_monitor: RaftHealthMonitor, pub(super) connection_cache: ConnectionCache, _phantom: PhantomData, @@ -116,6 +118,10 @@ where .await } + async fn initial_cluster_size(&self) -> usize { + self.initial_cluster_size + } + async fn nodes_with_status( &self, status: NodeStatus, @@ -776,10 +782,12 @@ where ) -> Self { let zombie_threshold = config.raft.membership.zombie.threshold; let connection_cache = ConnectionCache::new(config.network.clone()); + let initial_cluster_size = initial_nodes.len(); Self { node_id, membership: MembershipGuard::new(initial_nodes, 0), config, + initial_cluster_size, _phantom: PhantomData, health_monitor: RaftHealthMonitor::new(zombie_threshold), connection_cache, diff --git a/d-engine-server/src/membership/raft_membership_test.rs b/d-engine-server/src/membership/raft_membership_test.rs index 0fc7f169..cd512066 100644 --- a/d-engine-server/src/membership/raft_membership_test.rs +++ b/d-engine-server/src/membership/raft_membership_test.rs @@ -1291,3 +1291,244 @@ mod pre_warm_connections_tests { assert!(logs_contain("No replication peers found")); } } + +// ================================================================================================ +// Tests for Single-Node Cluster Support (Issue #179) +// ================================================================================================ + +#[cfg(test)] +mod single_node_tests { + use super::*; + + /// Helper: Create single-node membership + fn create_single_node_membership() + -> RaftMembership> { + let initial_cluster = vec![NodeMeta { + id: 1, + address: "127.0.0.1:9081".to_string(), + role: Follower.into(), + status: NodeStatus::Active.into(), + }]; + RaftMembership::new(1, initial_cluster, RaftNodeConfig::default()) + } + + /// Helper: Create 3-node membership + fn create_three_node_membership() + -> RaftMembership> { + let initial_cluster = vec![ + NodeMeta { + id: 1, + address: "127.0.0.1:9081".to_string(), + role: Leader.into(), + status: NodeStatus::Active.into(), + }, + NodeMeta { + id: 2, + address: "127.0.0.1:9082".to_string(), + role: Follower.into(), + status: NodeStatus::Active.into(), + }, + NodeMeta { + id: 3, + address: "127.0.0.1:9083".to_string(), + role: Follower.into(), + status: NodeStatus::Active.into(), + }, + ]; + RaftMembership::new(1, initial_cluster, RaftNodeConfig::default()) + } + + /// Helper: Create 5-node membership + fn create_five_node_membership() + -> RaftMembership> { + let initial_cluster = vec![ + NodeMeta { + id: 1, + address: "127.0.0.1:9081".to_string(), + role: Leader.into(), + status: NodeStatus::Active.into(), + }, + NodeMeta { + id: 2, + address: "127.0.0.1:9082".to_string(), + role: Follower.into(), + status: NodeStatus::Active.into(), + }, + NodeMeta { + id: 3, + address: "127.0.0.1:9083".to_string(), + role: Follower.into(), + status: NodeStatus::Active.into(), + }, + NodeMeta { + id: 4, + address: "127.0.0.1:9084".to_string(), + role: Follower.into(), + status: NodeStatus::Active.into(), + }, + NodeMeta { + id: 5, + address: "127.0.0.1:9085".to_string(), + role: Follower.into(), + status: NodeStatus::Active.into(), + }, + ]; + RaftMembership::new(1, initial_cluster, RaftNodeConfig::default()) + } + + #[tokio::test] + async fn test_initial_cluster_size_single_node() { + let membership = create_single_node_membership(); + + let size = membership.initial_cluster_size().await; + + assert_eq!(size, 1, "Single-node cluster should report size 1"); + } + + #[tokio::test] + async fn test_initial_cluster_size_three_nodes() { + let membership = create_three_node_membership(); + + let size = membership.initial_cluster_size().await; + + assert_eq!(size, 3, "Three-node cluster should report size 3"); + } + + #[tokio::test] + async fn test_initial_cluster_size_five_nodes() { + let membership = create_five_node_membership(); + + let size = membership.initial_cluster_size().await; + + assert_eq!(size, 5, "Five-node cluster should report size 5"); + } + + #[tokio::test] + async fn test_is_single_node_cluster_true() { + let membership = create_single_node_membership(); + + let is_single = membership.is_single_node_cluster().await; + + assert!(is_single, "Single-node cluster should return true"); + } + + #[tokio::test] + async fn test_is_single_node_cluster_false_three_nodes() { + let membership = create_three_node_membership(); + + let is_single = membership.is_single_node_cluster().await; + + assert!(!is_single, "Three-node cluster should return false"); + } + + #[tokio::test] + async fn test_is_single_node_cluster_false_five_nodes() { + let membership = create_five_node_membership(); + + let is_single = membership.is_single_node_cluster().await; + + assert!(!is_single, "Five-node cluster should return false"); + } + + #[tokio::test] + async fn test_initial_cluster_size_immutable_after_add_learner() { + let membership = create_three_node_membership(); + + // Initial size should be 3 + assert_eq!(membership.initial_cluster_size().await, 3); + + // Add a learner (simulate cluster expansion) + membership + .add_learner(4, "127.0.0.1:9084".to_string()) + .await + .expect("Should succeed"); + + // Initial cluster size should still be 3 (immutable) + assert_eq!( + membership.initial_cluster_size().await, + 3, + "initial_cluster_size should remain unchanged after adding learner" + ); + + // Verify the learner was actually added + assert!(membership.contains_node(4).await, "Node 4 should exist"); + } + + #[tokio::test] + async fn test_initial_cluster_size_immutable_after_remove_node() { + let membership = create_three_node_membership(); + + // Initial size should be 3 + assert_eq!(membership.initial_cluster_size().await, 3); + + // Remove node 3 (simulate node failure) + membership.force_remove_node(3).await.expect("Should succeed"); + + // Initial cluster size should still be 3 (immutable) + assert_eq!( + membership.initial_cluster_size().await, + 3, + "initial_cluster_size should remain unchanged after removing node" + ); + + // Verify node was actually removed + assert!( + !membership.contains_node(3).await, + "Node 3 should be removed" + ); + } + + #[tokio::test] + async fn test_voters_empty_in_single_node_but_cluster_size_correct() { + let membership = create_single_node_membership(); + + // voters() should return empty (excludes self) + let voters = membership.voters().await; + assert!( + voters.is_empty(), + "voters() should be empty for single-node (excludes self)" + ); + + // But initial_cluster_size() should still be 1 + assert_eq!( + membership.initial_cluster_size().await, + 1, + "initial_cluster_size should be 1 even when voters() is empty" + ); + + // is_single_node_cluster() should be true + assert!( + membership.is_single_node_cluster().await, + "is_single_node_cluster should be true" + ); + } + + #[tokio::test] + async fn test_network_partition_does_not_affect_cluster_size() { + let membership = create_three_node_membership(); + + // Simulate network partition by removing all other nodes + membership.force_remove_node(2).await.expect("Should succeed"); + membership.force_remove_node(3).await.expect("Should succeed"); + + // voters() should be empty now (simulating partition) + let voters = membership.voters().await; + assert!( + voters.is_empty(), + "voters() should be empty after partition" + ); + + // But initial_cluster_size should still be 3 + assert_eq!( + membership.initial_cluster_size().await, + 3, + "initial_cluster_size should remain 3 even in network partition" + ); + + // is_single_node_cluster() should still return false + assert!( + !membership.is_single_node_cluster().await, + "is_single_node_cluster should return false (was 3-node cluster)" + ); + } +} diff --git a/d-engine-server/tests/components/election/election_handler_test.rs b/d-engine-server/tests/components/election/election_handler_test.rs index 342550c7..a0e5b0e6 100644 --- a/d-engine-server/tests/components/election/election_handler_test.rs +++ b/d-engine-server/tests/components/election/election_handler_test.rs @@ -179,6 +179,8 @@ async fn test_broadcast_vote_requests_case3() { let term = 1; let mut membership = MockMembership::new(); + membership.expect_is_single_node_cluster().returning(|| false); + membership.expect_initial_cluster_size().returning(|| 2); membership.expect_voters().returning(move || { vec![NodeMeta { id: 2, diff --git a/d-engine-server/tests/components/replication/replication_handler_test.rs b/d-engine-server/tests/components/replication/replication_handler_test.rs index 00464336..f402e473 100644 --- a/d-engine-server/tests/components/replication/replication_handler_test.rs +++ b/d-engine-server/tests/components/replication/replication_handler_test.rs @@ -54,6 +54,14 @@ use d_engine_server::node::RaftTypeConfig; use d_engine_server::test_utils::setup_raft_components; use d_engine_server::test_utils::simulate_insert_command; +// Helper function to create MockMembership with single-node cluster detection mocked +fn create_mock_membership_multi_node() -> MockMembership { + let mut membership = MockMembership::new(); + membership.expect_is_single_node_cluster().returning(|| false); + membership.expect_initial_cluster_size().returning(|| 3); + membership +} + /// # Case 1: The peer3's next_index is equal to /// the end of the leader's old log, /// and only the new log is sent @@ -938,7 +946,7 @@ mod handle_raft_request_in_batch_test { noop_log_id: None, }; - let mut membership = MockMembership::new(); + let mut membership = create_mock_membership_multi_node(); membership.expect_replication_peers().returning(move || { vec![NodeMeta { id: peer2_id, @@ -1035,7 +1043,7 @@ mod handle_raft_request_in_batch_test { context.storage.raft_log = Arc::new(raft_log); context.transport = Arc::new(transport); - let mut membership = MockMembership::new(); + let mut membership = create_mock_membership_multi_node(); membership.expect_replication_peers().returning(Vec::new); context.membership = Arc::new(membership); @@ -1103,7 +1111,7 @@ mod handle_raft_request_in_batch_test { context.storage.raft_log = Arc::new(raft_log); context.transport = Arc::new(transport); - let mut membership = MockMembership::new(); + let mut membership = create_mock_membership_multi_node(); membership.expect_replication_peers().returning(move || { vec![NodeMeta { id: peer2_id, @@ -1171,7 +1179,7 @@ mod handle_raft_request_in_batch_test { context.storage.raft_log = Arc::new(raft_log); context.transport = Arc::new(transport); - let mut membership = MockMembership::new(); + let mut membership = create_mock_membership_multi_node(); membership.expect_replication_peers().returning(move || { vec![NodeMeta { id: peer2_id, @@ -1263,7 +1271,7 @@ mod handle_raft_request_in_batch_test { // ---------------------- //Call the function to be tested // ---------------------- - let mut membership = MockMembership::new(); + let mut membership = create_mock_membership_multi_node(); membership.expect_replication_peers().returning(move || { vec![NodeMeta { id: peer2_id, @@ -1435,7 +1443,7 @@ mod handle_raft_request_in_batch_test { .collect(); let members = futures::future::join_all(futures).await; - let mut membership = MockMembership::new(); + let mut membership = create_mock_membership_multi_node(); membership.expect_replication_peers().returning(move || members.clone()); context.membership = Arc::new(membership); context.storage.raft_log = Arc::new(raft_log); @@ -1618,7 +1626,7 @@ mod handle_raft_request_in_batch_test { // ---------------------- context.storage.raft_log = Arc::new(raft_log); context.transport = Arc::new(transport); - let mut membership = MockMembership::new(); + let mut membership = create_mock_membership_multi_node(); membership.expect_replication_peers().returning(move || { vec![ NodeMeta { @@ -1771,7 +1779,7 @@ mod handle_raft_request_in_batch_test { // ---------------------- context.storage.raft_log = Arc::new(raft_log); context.transport = Arc::new(transport); - let mut membership = MockMembership::new(); + let mut membership = create_mock_membership_multi_node(); membership.expect_replication_peers().returning(move || { vec![ NodeMeta { @@ -1901,7 +1909,7 @@ mod handle_raft_request_in_batch_test { // ---------------------- context.storage.raft_log = Arc::new(raft_log); context.transport = Arc::new(transport); - let mut membership = MockMembership::new(); + let mut membership = create_mock_membership_multi_node(); membership.expect_replication_peers().returning(move || { vec![ NodeMeta { @@ -2022,7 +2030,7 @@ mod handle_raft_request_in_batch_test { // ---------------------- context.storage.raft_log = Arc::new(raft_log); context.transport = Arc::new(transport); - let mut membership = MockMembership::new(); + let mut membership = create_mock_membership_multi_node(); membership.expect_replication_peers().returning(move || { vec![ NodeMeta { @@ -2229,7 +2237,7 @@ mod handle_raft_request_in_batch_test { // ---------------------- context.storage.raft_log = Arc::new(raft_log); context.transport = Arc::new(transport); - let mut membership = MockMembership::new(); + let mut membership = create_mock_membership_multi_node(); membership.expect_replication_peers().returning(move || { vec![NodeMeta { id: peer2_id, @@ -2279,7 +2287,7 @@ mod handle_raft_request_in_batch_test { let handler = ReplicationHandler::::new(my_id); // Setup membership: one voter, one learner - let mut membership = MockMembership::new(); + let mut membership = create_mock_membership_multi_node(); membership.expect_replication_peers().returning(move || { vec![ NodeMeta { @@ -2384,7 +2392,7 @@ mod handle_raft_request_in_batch_test { let handler = ReplicationHandler::::new(my_id); // Setup membership: one voter, one pending active - let mut membership = MockMembership::new(); + let mut membership = create_mock_membership_multi_node(); membership.expect_replication_peers().returning(move || { vec![ NodeMeta { @@ -2488,7 +2496,7 @@ mod handle_raft_request_in_batch_test { let handler = ReplicationHandler::::new(my_id); // Setup membership: only learners - let mut membership = MockMembership::new(); + let mut membership = create_mock_membership_multi_node(); membership.expect_replication_peers().returning(move || { vec![ NodeMeta { @@ -2590,8 +2598,10 @@ mod handle_raft_request_in_batch_test { let my_id = 1; let handler = ReplicationHandler::::new(my_id); - // Membership: no other peers - let mut membership = MockMembership::new(); + // Membership: no other peers (simulating multi-node cluster with network partition) + let mut membership = create_mock_membership_multi_node(); + membership.expect_is_single_node_cluster().returning(|| false); // Multi-node cluster + membership.expect_initial_cluster_size().returning(|| 3); // 3-node cluster membership.expect_replication_peers().returning(Vec::new); membership.expect_voters().returning(Vec::new); let context = { @@ -2642,7 +2652,7 @@ mod handle_raft_request_in_batch_test { let handler = ReplicationHandler::::new(my_id); // Setup membership: 1 voter, 1 joining, 1 syncing - let mut membership = MockMembership::new(); + let mut membership = create_mock_membership_multi_node(); membership.expect_replication_peers().returning(move || { vec![ NodeMeta { @@ -2756,7 +2766,7 @@ mod handle_raft_request_in_batch_test { // Setup membership: 2 voters, 1 joining, 1 syncing let pending_id = 2; let voter_id = 3; - let mut membership = MockMembership::new(); + let mut membership = create_mock_membership_multi_node(); membership.expect_replication_peers().returning(move || { vec![ NodeMeta { @@ -2861,7 +2871,7 @@ mod handle_raft_request_in_batch_test { let leader_commit_index = 100; // Membership: one learner - let mut membership = MockMembership::new(); + let mut membership = create_mock_membership_multi_node(); membership.expect_replication_peers().returning(move || { vec![NodeMeta { id: learner_id, @@ -2946,7 +2956,7 @@ mod handle_raft_request_in_batch_test { let handler = ReplicationHandler::::new(my_id); // Setup membership: only learners (0 voters) - let mut membership = MockMembership::new(); + let mut membership = create_mock_membership_multi_node(); membership.expect_replication_peers().returning(move || { vec![NodeMeta { id: learner_id, @@ -3025,3 +3035,260 @@ mod handle_raft_request_in_batch_test { ); } } + +// ============================================================================ +// Single-Node Cluster Replication Tests +// Tests for issue #179: Support single-node to multi-node dynamic expansion +// ============================================================================ + +#[cfg(test)] +mod single_node_cluster_replication_tests { + use super::*; + + /// Test: Single-node cluster should skip replication and automatically achieve quorum + /// This verifies the fix for issue #179 where single-node replication would fail + #[tokio::test] + #[traced_test] + async fn test_single_node_auto_achieves_quorum() { + let (_graceful_tx, graceful_rx) = watch::channel(()); + let mut context = mock_raft_context( + "/tmp/test_single_node_auto_achieves_quorum", + graceful_rx, + None, + ); + + let my_id = 1; + let handler = ReplicationHandler::::new(my_id); + + // Setup: Single-node cluster membership + let mut membership = MockMembership::new(); + membership.expect_is_single_node_cluster().returning(|| true); + membership.expect_initial_cluster_size().returning(|| 1); + // No need to expect replication_peers() as it shouldn't be called + context.membership = Arc::new(membership); + + // Execute: Try to replicate entries + let commands = vec![]; + let state_snapshot = StateSnapshot { + current_term: 1, + voted_for: None, + commit_index: 0, + role: Leader.into(), + }; + let leader_state_snapshot = LeaderStateSnapshot { + next_index: HashMap::new(), + match_index: HashMap::new(), + noop_log_id: None, + }; + + let result = handler + .handle_raft_request_in_batch(commands, state_snapshot, leader_state_snapshot, &context) + .await; + + // Verify: Should succeed with quorum achieved, no peer updates + assert!(result.is_ok(), "Single-node replication should succeed"); + let append_results = result.unwrap(); + assert!( + append_results.commit_quorum_achieved, + "Single-node should automatically achieve quorum" + ); + assert_eq!( + append_results.peer_updates.len(), + 0, + "Single-node should have no peer updates" + ); + assert_eq!( + append_results.learner_progress.len(), + 0, + "Single-node should have no learner progress" + ); + } + + /// Test: Three-node cluster should go through normal replication flow + /// This ensures the single-node optimization doesn't affect multi-node clusters + #[tokio::test] + #[traced_test] + async fn test_three_node_cluster_normal_replication() { + let (_graceful_tx, graceful_rx) = watch::channel(()); + let mut context = mock_raft_context( + "/tmp/test_three_node_cluster_normal_replication", + graceful_rx, + None, + ); + + let my_id = 1; + let peer1_id = 2; + let peer2_id = 3; + let handler = ReplicationHandler::::new(my_id); + + // Setup: Three-node cluster membership + let mut membership = create_mock_membership_multi_node(); + membership.expect_is_single_node_cluster().returning(|| false); + membership.expect_initial_cluster_size().returning(|| 3); + membership.expect_replication_peers().returning(move || { + vec![ + NodeMeta { + id: peer1_id, + address: "http://127.0.0.1:55001".to_string(), + role: Follower.into(), + status: NodeStatus::Active.into(), + }, + NodeMeta { + id: peer2_id, + address: "http://127.0.0.1:55002".to_string(), + role: Follower.into(), + status: NodeStatus::Active.into(), + }, + ] + }); + membership.expect_voters().returning(move || { + vec![ + NodeMeta { + id: peer1_id, + address: "http://127.0.0.1:55001".to_string(), + role: Follower.into(), + status: NodeStatus::Active.into(), + }, + NodeMeta { + id: peer2_id, + address: "http://127.0.0.1:55002".to_string(), + role: Follower.into(), + status: NodeStatus::Active.into(), + }, + ] + }); + context.membership = Arc::new(membership); + + // Mock log + let mut raft_log = MockRaftLog::new(); + raft_log.expect_last_entry_id().returning(|| 1); + raft_log.expect_get_entries_range().returning(|_| Ok(vec![])); + raft_log.expect_entry_term().returning(|_| None); + context.storage.raft_log = Arc::new(raft_log); + + // Mock transport with successful responses from both peers + let mut transport = MockTransport::new(); + transport.expect_send_append_requests().return_once(move |_, _, _, _| { + Ok(AppendResult { + peer_ids: vec![peer1_id, peer2_id].into_iter().collect(), + responses: vec![ + Ok(AppendEntriesResponse::success( + peer1_id, + 1, + Some(LogId { term: 1, index: 2 }), + )), + Ok(AppendEntriesResponse::success( + peer2_id, + 1, + Some(LogId { term: 1, index: 2 }), + )), + ], + }) + }); + context.transport = Arc::new(transport); + + // Execute: Replicate entries + let commands = vec![]; + let state_snapshot = StateSnapshot { + current_term: 1, + voted_for: None, + commit_index: 1, + role: Leader.into(), + }; + let leader_state_snapshot = LeaderStateSnapshot { + next_index: HashMap::from([(peer1_id, 2), (peer2_id, 2)]), + match_index: HashMap::new(), + noop_log_id: None, + }; + + let result = handler + .handle_raft_request_in_batch(commands, state_snapshot, leader_state_snapshot, &context) + .await; + + // Verify: Should succeed with peer updates + assert!(result.is_ok(), "Three-node replication should succeed"); + let append_results = result.unwrap(); + assert!( + append_results.commit_quorum_achieved, + "Should achieve quorum with majority" + ); + assert_eq!( + append_results.peer_updates.len(), + 2, + "Should have updates for both peers" + ); + } + + /// Test: Three-node cluster with network partition shouldn't be treated as single-node + /// This verifies that we use config-based detection, not runtime state + #[tokio::test] + #[traced_test] + async fn test_network_partition_not_treated_as_single_node() { + let (_graceful_tx, graceful_rx) = watch::channel(()); + let mut context = mock_raft_context( + "/tmp/test_network_partition_not_treated_as_single_node", + graceful_rx, + None, + ); + + let my_id = 1; + let handler = ReplicationHandler::::new(my_id); + + // Setup: Three-node cluster (initial_cluster_size=3) + // But network partition means replication_peers() returns empty + let mut membership = create_mock_membership_multi_node(); + membership.expect_is_single_node_cluster().returning(|| false); // Config says 3 nodes + membership.expect_initial_cluster_size().returning(|| 3); + membership.expect_replication_peers().returning(Vec::new); // Network partition: no reachable peers + context.membership = Arc::new(membership); + + // Mock log + let mut raft_log = MockRaftLog::new(); + raft_log.expect_last_entry_id().returning(|| 1); + raft_log.expect_get_entries_range().returning(|_| Ok(vec![])); + raft_log.expect_entry_term().returning(|_| None); + context.storage.raft_log = Arc::new(raft_log); + + // Mock transport - should error due to empty peer list + let mut transport = MockTransport::new(); + transport.expect_send_append_requests().returning(move |_, _, _, _| { + Err(NetworkError::EmptyPeerList { + request_type: "send_append_requests", + } + .into()) + }); + context.transport = Arc::new(transport); + + // Execute: Try to replicate + let commands = vec![]; + let state_snapshot = StateSnapshot { + current_term: 1, + voted_for: None, + commit_index: 1, + role: Leader.into(), + }; + let leader_state_snapshot = LeaderStateSnapshot { + next_index: HashMap::new(), + match_index: HashMap::new(), + noop_log_id: None, + }; + + let result = handler + .handle_raft_request_in_batch(commands, state_snapshot, leader_state_snapshot, &context) + .await; + + // Verify: Should get NoPeerFound error (not treated as single-node) + // In a 3-node cluster with network partition, we should error, not auto-achieve quorum + assert!(result.is_err(), "Network partition should cause error"); + let err = result.unwrap_err(); + assert!( + matches!( + err, + Error::Consensus(ConsensusError::Replication( + ReplicationError::NoPeerFound { .. } + )) + ), + "Should return NoPeerFound error, got: {err:?}" + ); + } +} diff --git a/examples/single-node-expansion/Cargo.lock b/examples/single-node-expansion/Cargo.lock new file mode 100644 index 00000000..26b715e0 --- /dev/null +++ b/examples/single-node-expansion/Cargo.lock @@ -0,0 +1,2828 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "astral-tokio-tar" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec179a06c1769b1e42e1e2cbe74c7dcdb3d6383c838454d063eaac5bbb7ebbe5" +dependencies = [ + "filetime", + "futures-core", + "libc", + "portable-atomic", + "rustc-hash", + "tokio", + "tokio-stream", + "xattr", +] + +[[package]] +name = "async-compression" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e86f6d3dc9dc4352edeea6b8e499e13e3f5dc3b964d7ca5fd411415a3498473" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b5ce75405893cd713f9ab8e297d8e438f624dde7d706108285f7e17a25a180f" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "179c3777a8b5e70e90ea426114ffc565b2c1a9f82f6c4a0c5a34aa6ef5e781b6" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +dependencies = [ + "serde", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "cc" +version = "1.2.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + +[[package]] +name = "compression-codecs" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302266479cb963552d11bd042013a58ef1adc56768016c8b82b4199488f2d4ad" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + +[[package]] +name = "config" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf" +dependencies = [ + "nom", + "pathdiff", + "serde", + "toml", +] + +[[package]] +name = "console-api" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8030735ecb0d128428b64cd379809817e620a40e5001c54465b99ec5feec2857" +dependencies = [ + "futures-core", + "prost", + "prost-types", + "tonic", + "tracing-core", +] + +[[package]] +name = "console-subscriber" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6539aa9c6a4cd31f4b1c040f860a1eac9aa80e7df6b05d506a6e7179936d6a01" +dependencies = [ + "console-api", + "crossbeam-channel", + "crossbeam-utils", + "futures-task", + "hdrhistogram", + "humantime", + "hyper-util", + "prost", + "prost-types", + "serde", + "serde_json", + "thread_local", + "tokio", + "tokio-stream", + "tonic", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-skiplist" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df29de440c58ca2cc6e587ec3d22347551a32435fbde9d2bff64e78a9ffa151b" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "d-engine" +version = "0.2.0" +dependencies = [ + "d-engine-server", +] + +[[package]] +name = "d-engine-core" +version = "0.1.0" +dependencies = [ + "astral-tokio-tar", + "async-compression", + "async-stream", + "bincode", + "bytes", + "config", + "crc32fast", + "crossbeam", + "crossbeam-channel", + "crossbeam-skiplist", + "d-engine-proto", + "dashmap", + "futures", + "http-body", + "http-body-util", + "lru", + "memmap2", + "metrics", + "nanoid", + "prost", + "rand 0.8.5", + "serde", + "sha2", + "tempfile", + "thiserror 1.0.69", + "tokio", + "tokio-stream", + "tonic", + "tracing", +] + +[[package]] +name = "d-engine-proto" +version = "0.2.0" +dependencies = [ + "bytes", + "prost", + "serde", + "tonic", + "tonic-build", + "tracing", + "vergen", +] + +[[package]] +name = "d-engine-server" +version = "0.2.0" +dependencies = [ + "arc-swap", + "bincode", + "bytes", + "config", + "crc32fast", + "crossbeam", + "crossbeam-skiplist", + "d-engine-core", + "d-engine-proto", + "dashmap", + "futures", + "http-body", + "http-body-util", + "metrics", + "parking_lot", + "prost", + "rcgen", + "rocksdb", + "serde", + "tempfile", + "tokio", + "tokio-stream", + "tokio-util", + "tonic", + "tonic-health", + "tracing", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "demo" +version = "0.1.4" +dependencies = [ + "console-subscriber", + "d-engine", + "metrics", + "metrics-exporter-prometheus", + "tokio", + "tokio-metrics", + "tracing", + "tracing-appender", + "tracing-subscriber", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.12.1", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "base64 0.21.7", + "byteorder", + "flate2", + "nom", + "num-traits", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2 0.6.1", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "librocksdb-sys" +version = "0.17.3+10.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef2a00ee60fe526157c9023edab23943fae1ce2ab6f4abb2a807c1746835de9" +dependencies = [ + "bindgen", + "bzip2-sys", + "cc", + "libc", + "libz-sys", + "lz4-sys", + "zstd-sys", +] + +[[package]] +name = "libz-sys" +version = "1.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "lru" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0281c2e25e62316a5c9d98f2d2e9e95a37841afdaf4383c177dbb5c1dfab0568" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "lz4-sys" +version = "1.11.1+lz4-1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memmap2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +dependencies = [ + "libc", +] + +[[package]] +name = "metrics" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dea7ac8057892855ec285c440160265225438c3c45072613c25a4b26e98ef5" +dependencies = [ + "ahash", + "portable-atomic", +] + +[[package]] +name = "metrics-exporter-prometheus" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b166dea96003ee2531cf14833efedced545751d800f03535801d833313f8c15" +dependencies = [ + "base64 0.22.1", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "indexmap 2.12.1", + "ipnet", + "metrics", + "metrics-util", + "quanta", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "metrics-util" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe8db7a05415d0f919ffb905afa37784f71901c9a773188876984b4f769ab986" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", + "hashbrown 0.15.5", + "metrics", + "quanta", + "rand 0.9.2", + "rand_xoshiro", + "sketches-ddsketch", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "nanoid" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" +dependencies = [ + "rand 0.8.5", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap 2.12.1", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +dependencies = [ + "heck", + "itertools 0.14.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core 0.9.3", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rocksdb" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddb7af00d2b17dbd07d82c0063e25411959748ff03e8d4f96134c2ff41fce34f" +dependencies = [ + "libc", + "librocksdb-sys", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "sketches-ddsketch" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.1", + "tokio-macros", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-metrics" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01bbf7db0b3f5eee8930a119fe99bfa1438de1095fe3d26b25e652a5933ef3f" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.12.1", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "flate2", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "rustls-pemfile", + "socket2 0.5.10", + "tokio", + "tokio-rustls", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn", +] + +[[package]] +name = "tonic-health" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1eaf34ddb812120f5c601162d5429933c9b527d901ab0e7f930d3147e33a09b2" +dependencies = [ + "async-stream", + "prost", + "tokio", + "tokio-stream", + "tonic", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand 0.8.5", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vergen" +version = "8.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990d9ea5967266ea0ccf413a4aa5c42a93dbcfda9cb49a97de6931726b12566" +dependencies = [ + "anyhow", + "cfg-if", + "rustversion", + "time", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "zerocopy" +version = "0.8.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea879c944afe8a2b25fef16bb4ba234f47c694565e97383b36f3a878219065c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf955aa904d6040f70dc8e9384444cb1030aed272ba3cb09bbc4ab9e7c1f34f5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/examples/single-node-expansion/Cargo.toml b/examples/single-node-expansion/Cargo.toml new file mode 100644 index 00000000..b7953eba --- /dev/null +++ b/examples/single-node-expansion/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "demo" +version = "0.1.4" +edition = "2021" + +[dependencies] +d-engine = { path = "../../d-engine", features = ["server", "rocksdb"] } +# d-engine = "0.1.1" +# d-engine = { git = "https://github.com/deventlab/d-engine.git", branch = "feature/79-snapshot" } + +metrics-exporter-prometheus = "0.17.2" +tokio = { version = "1.4", features = ["rt-multi-thread"] } +tracing = { version = "0.1" } +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } +tracing-appender = "0.2" +tokio-metrics = { version = "0.4.5"} +metrics = "0.24" + +# For console support +console-subscriber = "0.4" diff --git a/examples/single-node-expansion/Makefile b/examples/single-node-expansion/Makefile new file mode 100644 index 00000000..0fd0b289 --- /dev/null +++ b/examples/single-node-expansion/Makefile @@ -0,0 +1,137 @@ +# Makefile for Single-Node to Multi-Node Dynamic Expansion Testing + +.PHONY: build start-node1 join-node2 join-node3 clean clean-logs help +.DEFAULT_GOAL := help + +# =============================== +# Global Variables +# =============================== +LOG_LEVEL ?= debug + +# =============================== +# Build Targets +# =============================== +build: + @echo "Building release binary..." + cargo build --release --jobs 4 + +# =============================== +# Single Node Bootstrap +# =============================== +start-node1: + @echo "=== Starting Node 1 (Single-Node Bootstrap) ===" + @echo "This node will start as a standalone leader (initial_cluster_size=1)" + @echo "" + CONFIG_PATH=config/n1 \ + DB_PATH="./db/1" \ + LOG_DIR="./logs/1" \ + METRICS_PORT=8081 \ + RUST_LOG=demo=$(LOG_LEVEL),d_engine=$(LOG_LEVEL),timing=$(LOG_LEVEL) \ + RUST_BACKTRACE=1 \ + ./target/release/demo + +# =============================== +# Dynamic Expansion - Add Node 2 +# =============================== +join-node2: + @echo "=== Joining Node 2 (Dynamic Expansion) ===" + @echo "This node will join as Learner and auto-promote to Voter when caught up" + @echo "Expected flow:" + @echo " 1. Node 2 discovers Node 1 (leader)" + @echo " 2. Node 2 sends JoinRequest to Node 1" + @echo " 3. Node 1 commits AddNode config change" + @echo " 4. Node 2 syncs via AppendEntries (raft log replication)" + @echo " 5. Node 2 auto-promotes to Voter when match_index catches up" + @echo "" + CONFIG_PATH=config/n2 \ + DB_PATH="./db/2" \ + LOG_DIR="./logs/2" \ + METRICS_PORT=8082 \ + RUST_LOG=demo=$(LOG_LEVEL),d_engine=$(LOG_LEVEL),timing=$(LOG_LEVEL) \ + RUST_BACKTRACE=1 \ + ./target/release/demo + +# =============================== +# Dynamic Expansion - Add Node 3 +# =============================== +join-node3: + @echo "=== Joining Node 3 (Dynamic Expansion) ===" + @echo "This node will join after Node 2 has been promoted" + @echo "Expected flow:" + @echo " 1. Node 3 discovers Node 1 (leader)" + @echo " 2. Node 3 sends JoinRequest to Node 1" + @echo " 3. Node 1 commits AddNode config change (requires 2/3 quorum)" + @echo " 4. Node 3 syncs via AppendEntries" + @echo " 5. Node 3 auto-promotes to Voter when caught up" + @echo "" + CONFIG_PATH=config/n3 \ + DB_PATH="./db/3" \ + LOG_DIR="./logs/3" \ + METRICS_PORT=8083 \ + RUST_LOG=demo=$(LOG_LEVEL),d_engine=$(LOG_LEVEL),timing=$(LOG_LEVEL) \ + RUST_BACKTRACE=1 \ + ./target/release/demo + +# =============================== +# Log Monitoring +# =============================== +tail-node1: + @echo "Watching Node 1 logs..." + tail -f logs/1/d.log + +tail-node2: + @echo "Watching Node 2 logs..." + tail -f logs/2/d.log + +tail-node3: + @echo "Watching Node 3 logs..." + tail -f logs/3/d.log + +# =============================== +# Utilities +# =============================== +clean: + @echo "Cleaning all artifacts..." + cargo clean + rm -rf logs/* + rm -rf db/* + +clean-logs: + @echo "Cleaning logs and database only..." + rm -rf logs/* + rm -rf db/* + +help: + @echo "Single-Node Dynamic Expansion Testing Commands:" + @echo "" + @echo "Build:" + @echo " make build - Build release binary" + @echo "" + @echo "Testing Workflow (run in separate terminals):" + @echo " make start-node1 - Step 1: Start single-node cluster (leader)" + @echo " make join-node2 - Step 2: Join node 2 as learner (auto-promote)" + @echo " make join-node3 - Step 3: Join node 3 as learner (auto-promote)" + @echo "" + @echo "Log Monitoring:" + @echo " make tail-node1 - Watch node 1 logs" + @echo " make tail-node2 - Watch node 2 logs" + @echo " make tail-node3 - Watch node 3 logs" + @echo "" + @echo "Cleanup:" + @echo " make clean - Remove all build artifacts and data" + @echo " make clean-logs - Remove only logs and database" + @echo "" + @echo "Testing Steps:" + @echo " 1. Open 3 terminal windows" + @echo " 2. Terminal 1: make build && make start-node1" + @echo " 3. Wait for Node 1 to become leader (check logs)" + @echo " 4. Terminal 2: make join-node2" + @echo " 5. Watch Node 2 join as Learner and promote to Voter" + @echo " 6. Terminal 3: make join-node3" + @echo " 7. Watch Node 3 join and promote" + @echo "" + @echo "Key Log Messages to Watch:" + @echo " - Node 1: 'Node X successfully added as learner'" + @echo " - Node 2/3: 'Learner state joined cluster successfully'" + @echo " - Node 1: 'Promoting learner X to voter'" + @echo " - Node 2/3: 'Transitioned from Learner to Follower'" diff --git a/examples/single-node-expansion/README.md b/examples/single-node-expansion/README.md new file mode 100644 index 00000000..93bd8aa6 --- /dev/null +++ b/examples/single-node-expansion/README.md @@ -0,0 +1,382 @@ +# Single-Node Dynamic Expansion Example + +This example demonstrates **dynamic cluster expansion** from a single-node cluster to a three-node cluster without downtime, testing the implementation of [Issue #179](https://github.com/your-repo/d-engine/issues/179). + +## Overview + +This test validates the following workflow: + +1. **Phase 1**: Start a single node that bootstraps as leader (no election) +2. **Phase 2**: Dynamically add a second node (Learner โ†’ Voter promotion) +3. **Phase 3**: Dynamically add a third node (Learner โ†’ Voter promotion) + +The entire process happens **without restarting the original node** and **without data loss**. + +## Architecture + +``` +Initial State: After Node 2 Joins: After Node 3 Joins: +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Node 1 โ”‚ โ”‚ Node 1 โ”‚ โ”‚ Node 1 โ”‚ +โ”‚ Leader โ”‚ โ”‚ Leader โ”‚ โ”‚ Leader โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ +Single node cluster โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ” +(initial_cluster_size=1) โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ + โ”‚ Node 2 โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Voter โ”‚ โ”‚ Node 2 โ”‚ โ”‚ Node 3 โ”‚ + โ”‚ โ”‚ โ”‚ Voter โ”‚ โ”‚ Voter โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + 2-node cluster 3-node cluster + (quorum = 2) (quorum = 2) +``` + +## Key Technical Concepts + +### Single-Node Bootstrap + +- Node 1 starts with `initial_cluster_size = 1` +- Automatically becomes leader **without election** +- Can accept writes immediately +- Designed for development or budget-constrained deployments + +### Dynamic Node Join + +- New nodes (2 and 3) start with `role = 3` (Learner), `status = 0` (Joining) +- Learner discovery flow: + 1. Learner discovers leader (via `initial_cluster` config or broadcast) + 2. Learner sends `JoinRequest` to leader via gRPC + 3. Leader commits `AddNode` configuration change via Raft + 4. Learner syncs data via `AppendEntries` RPC (raft log replication) + 5. Learner may fetch snapshot (state machine state) if needed + 6. Leader monitors learner's `match_index` progress + 7. When `match_index` catches up to `commit_index`, leader auto-promotes learner to Voter + +### Snapshot vs Log Replication + +- **Raft Log Replication**: New nodes sync via `AppendEntries` RPC +- **Snapshot Transfer**: For state machine data (NOT raft log) + - Helps new nodes catch up without replaying all historical log entries + - Transferred via streaming RPC + - Applied to state machine, not raft log + +### Learner Promotion + +- Automatic promotion triggered by leader when learner catches up +- Promotion requires committing a `PromoteToVoter` configuration change +- After promotion, the node participates in voting and leader election + +## Configuration Files + +### n1.toml - Single Node Bootstrap + +```toml +[cluster] +node_id = 1 +listen_address = "0.0.0.0:9081" +initial_cluster = [ + { id = 1, address = "0.0.0.0:9081", role = 2, status = 2 }, # Self as Leader +] +initial_cluster_size = 1 # KEY: Triggers single-node mode +``` + +**Key Points**: +- `initial_cluster_size = 1` is **IMMUTABLE** and triggers single-node bootstrap +- `role = 2` (Leader) - node expects to become leader immediately +- `status = 2` (Active) - node is active from the start + +### n2.toml - Joining as Learner + +```toml +[cluster] +node_id = 2 +listen_address = "0.0.0.0:9082" +initial_cluster = [ + { id = 1, address = "0.0.0.0:9081", role = 2, status = 2 }, # Existing leader + { id = 2, address = "0.0.0.0:9082", role = 3, status = 0 }, # Self: Learner, Joining +] +initial_cluster_size = 1 # Must match Node 1 +``` + +**Key Points**: +- Lists ALL nodes (existing + self) +- Self has `role = 3` (Learner), `status = 0` (Joining) +- `initial_cluster_size = 1` matches Node 1 (immutable) + +### n3.toml - Joining After Node 2 + +```toml +[cluster] +node_id = 3 +listen_address = "0.0.0.0:9083" +initial_cluster = [ + { id = 1, address = "0.0.0.0:9081", role = 2, status = 2 }, # Leader + { id = 2, address = "0.0.0.0:9082", role = 1, status = 2 }, # Promoted voter + { id = 3, address = "0.0.0.0:9083", role = 3, status = 0 }, # Self: Learner +] +initial_cluster_size = 1 # Must match Node 1 +``` + +**Note**: Node 2 is listed as `role = 1` (Follower) assuming it's already promoted. + +## Testing Instructions + +### Prerequisites + +```bash +# Ensure you have Rust and cargo installed +rustc --version + +# Navigate to example directory +cd examples/single-node-expansion +``` + +### Build + +```bash +make build +``` + +### Testing Steps + +#### Terminal 1: Start Node 1 (Single-Node Bootstrap) + +```bash +make start-node1 +``` + +**Expected logs**: +``` +Single-node mode detected (initial_cluster_size=1) +Node 1 transitioning directly to Leader role +Leader state initialized successfully +``` + +**What to observe**: +- Node 1 becomes leader immediately (no election) +- Can accept writes immediately + +#### Terminal 2: Join Node 2 + +Wait for Node 1 to be fully initialized, then: + +```bash +make join-node2 +``` + +**Expected logs**: + +Node 2: +``` +Learner state initialized +Discovering leader from initial_cluster +Sending JoinRequest to Node 1 (0.0.0.0:9081) +Join successful, snapshot_metadata: ... +Syncing raft logs via AppendEntries +``` + +Node 1: +``` +Received JoinRequest from Node 2 +Committing AddNode configuration change +Node 2 (0.0.0.0:9082) successfully added as learner +Learner progress: node_id=2, match_index=X, commit_index=Y +Promoting learner 2 to voter (caught up) +``` + +Node 2: +``` +Received promotion to Voter +Transitioned from Learner to Follower +``` + +**What to observe**: +- Node 2 joins as Learner +- Node 2 syncs data via AppendEntries +- Node 1 monitors Node 2's progress +- Node 2 auto-promotes to Voter when caught up + +#### Terminal 3: Join Node 3 + +Wait for Node 2 to be promoted, then: + +```bash +make join-node3 +``` + +**Expected logs**: Similar to Node 2's join flow. + +**What to observe**: +- Node 3 joins as Learner +- Configuration change now requires 2/3 quorum (Node 1 + Node 2) +- Node 3 syncs and promotes to Voter + +### Log Monitoring + +In additional terminal windows, you can monitor logs: + +```bash +# Watch Node 1 logs +make tail-node1 + +# Watch Node 2 logs +make tail-node2 + +# Watch Node 3 logs +make tail-node3 +``` + +### Cleanup + +```bash +# Clean all build artifacts and data +make clean + +# Or just clean logs and database +make clean-logs +``` + +## Key Log Messages to Watch + +### Successful Join Flow + +| Stage | Node | Log Message | +|-------|------|-------------| +| 1. Join Request | Node 2/3 | `Sending JoinRequest to Node 1` | +| 2. Leader Accepts | Node 1 | `Node X successfully added as learner` | +| 3. Config Change | Node 1 | `Committing AddNode configuration change` | +| 4. Learner Syncing | Node 2/3 | `Syncing raft logs via AppendEntries` | +| 5. Leader Monitors | Node 1 | `Learner progress: node_id=X, match_index=Y` | +| 6. Promotion Triggered | Node 1 | `Promoting learner X to voter` | +| 7. Learner Promoted | Node 2/3 | `Transitioned from Learner to Follower` | + +### Error Scenarios + +| Error | Possible Cause | Solution | +|-------|----------------|----------| +| `NoLeader` | Node 1 not started | Start Node 1 first | +| `NodeAlreadyExists` | Duplicate node_id | Use unique node_ids | +| `CommitTimeout` | Quorum not available | Ensure majority of voters are running | +| `JoinClusterFailed` | Network issue | Check addresses in config | + +## Testing Checklist + +- [ ] Node 1 starts as leader without election +- [ ] Node 1 can accept writes before any joins +- [ ] Node 2 successfully joins as Learner +- [ ] Node 2 syncs data from Node 1 +- [ ] Node 2 auto-promotes to Voter +- [ ] Node 3 successfully joins as Learner +- [ ] Config change for Node 3 requires 2/3 quorum +- [ ] Node 3 auto-promotes to Voter +- [ ] Final cluster has 3 active voters +- [ ] Original Node 1 never restarted +- [ ] No data loss during expansion + +## Troubleshooting + +### Node 2/3 Cannot Find Leader + +**Symptom**: `NoLeader` error in logs + +**Solution**: +1. Verify Node 1 is running: `ps aux | grep demo` +2. Check Node 1 logs for "Leader state initialized" +3. Verify addresses in config files match + +### Join Request Times Out + +**Symptom**: `CommitTimeout` or `JoinClusterFailed` + +**Solution**: +1. Check network connectivity: `curl http://0.0.0.0:9081` (should fail but proves port is open) +2. Verify `initial_cluster` in joining node's config lists correct leader address +3. Check Node 1 logs for error messages + +### Learner Never Promotes + +**Symptom**: Node stays in Learner role indefinitely + +**Solution**: +1. Check Node 1 logs for "Learner progress" messages +2. Verify `match_index` is increasing (data is syncing) +3. Check for errors in AppendEntries replication +4. Verify `learner_catchup_threshold` is reasonable (default) + +### Promotion Requires Quorum But Node 2 Not Voting Yet + +**Symptom**: Node 3 join fails because only 1 voter exists + +**Solution**: +1. Wait for Node 2 to promote to Voter first +2. Check Node 2's role: should see "Transitioned from Learner to Follower" +3. Retry Node 3 join after Node 2 is promoted + +## Advanced Topics + +### Manual Testing Without Makefile + +**Node 1**: +```bash +CONFIG_PATH=config/n1 \ +DB_PATH="./db/1" \ +LOG_DIR="./logs/1" \ +METRICS_PORT=8081 \ +RUST_LOG=demo=debug,d_engine=debug \ +RUST_BACKTRACE=1 \ +./target/release/demo +``` + +**Node 2**: +```bash +CONFIG_PATH=config/n2 \ +DB_PATH="./db/2" \ +LOG_DIR="./logs/2" \ +METRICS_PORT=8082 \ +RUST_LOG=demo=debug,d_engine=debug \ +RUST_BACKTRACE=1 \ +./target/release/demo +``` + +### Testing with Writes During Expansion + +You can send write requests to Node 1 while nodes are joining to verify: +- Leader continues accepting writes +- New nodes sync all committed entries +- No data loss during expansion + +### Testing Failure Scenarios + +1. **Kill Node 1 during Node 2 join**: Node 2 should handle gracefully +2. **Kill Node 2 during sync**: Should be able to restart and rejoin +3. **Network partition**: Test split-brain prevention + +## References + +- [Issue #179: Single-node cluster support](https://github.com/your-repo/d-engine/issues/179) +- [Raft Paper: Section 6 - Cluster Membership Changes](https://raft.github.io/raft.pdf) +- [etcd Learner Documentation](https://etcd.io/docs/v3.5/learning/design-learner/) +- [openraft Membership Changes](https://docs.rs/openraft/latest/openraft/docs/cluster_control/index.html) + +## Next Steps + +After successful testing, consider: + +1. **Performance Testing**: Measure join latency and sync throughput +2. **Failure Testing**: Test node failures during expansion +3. **Multi-Stage Expansion**: Test adding more than 3 nodes +4. **Snapshot Testing**: Enable snapshots and test join with large state +5. **Production Validation**: Test with real workloads + +## Contributing + +If you find issues during testing: + +1. Check logs in `logs/1/`, `logs/2/`, `logs/3/` +2. Open an issue with: + - Configuration files used + - Full log output + - Steps to reproduce +3. Link to [Issue #179](https://github.com/your-repo/d-engine/issues/179) diff --git a/examples/single-node-expansion/config/n1.toml b/examples/single-node-expansion/config/n1.toml new file mode 100644 index 00000000..de5e4b31 --- /dev/null +++ b/examples/single-node-expansion/config/n1.toml @@ -0,0 +1,87 @@ +# Node 1 Configuration - Single Node Bootstrap +# This node starts as a standalone single-node cluster and becomes leader automatically + +[cluster] +node_id = 1 +listen_address = "0.0.0.0:9081" + +# Single node startup: only this node in initial_cluster +initial_cluster = [ + { id = 1, address = "0.0.0.0:9081", role = 2, status = 2 }, # role=2 (Leader), status=2 (Active) +] + +# KEY: initial_cluster_size = 1 triggers single-node mode +# This is IMMUTABLE and tells the node to bootstrap without election +initial_cluster_size = 1 + +db_root_dir = "./db" +log_dir = "./logs" + + +[raft] +general_raft_timeout_duration_in_ms = 100 + +[raft.commit_handler] +batch_size_threshold = 500 +process_interval_ms = 10 +max_entries_per_chunk = 100 + +[raft.election] +election_timeout_min = 1000 +election_timeout_max = 2000 + +[raft.read_consistency] +default_policy = "LeaseRead" +lease_duration_ms = 500 + +[raft.replication] +rpc_append_entries_in_batch_threshold = 5000 +rpc_append_entries_batch_process_delay_in_ms = 0 + +[raft.persistence] +strategy = "MemFirst" +flush_policy = { Batch = { threshold = 100, interval_ms = 20 } } +max_buffered_entries = 10000 +flush_workers = 4 +channel_capacity = 1000 + +[raft.snapshot] +enable = false +max_log_entries_before_snapshot = 10000 +snapshot_cool_down_since_last_check = { secs = 60 } +snapshots_dir = "./snapshots/" +retained_log_entries = 3 + +# == Network Control Plane == +[network.control] +concurrency_limit = 50 +max_concurrent_streams = 1024 +connection_window_size = 4_194_304 +initial_stream_window_size = 2_097_152 +enable_connect_protocol = true + +tcp_keepalive_in_secs = 60 +http2_keep_alive_interval_in_secs = 15 +http2_keep_alive_timeout_in_secs = 10 +max_frame_size = 16_777_215 + +http2_max_pending_accept_reset_streams = 1000 +http2_max_send_buffer_size = 8_388_608 + +# == Network Data Plane == +[network.data] +connect_timeout_in_ms = 100 +request_timeout_in_ms = 300 +concurrency_limit = 100 +max_concurrent_streams = 4096 +connection_window_size = 8_388_608 +initial_stream_window_size = 4_194_304 + +tcp_keepalive_in_secs = 60 +http2_keep_alive_interval_in_secs = 15 +http2_keep_alive_timeout_in_secs = 10 +max_frame_size = 16_777_215 + +http2_adaptive_window = true +http2_max_pending_accept_reset_streams = 2000 +http2_max_send_buffer_size = 16_777_216 diff --git a/examples/single-node-expansion/config/n2.toml b/examples/single-node-expansion/config/n2.toml new file mode 100644 index 00000000..6cb84a6b --- /dev/null +++ b/examples/single-node-expansion/config/n2.toml @@ -0,0 +1,89 @@ +# Node 2 Configuration - Joining Node (Dynamic Expansion) +# This node joins an existing single-node cluster as a Learner + +[cluster] +node_id = 2 +listen_address = "0.0.0.0:9082" + +# List ALL nodes in the cluster (existing + self) +# Node 1: existing leader (role=2, status=2) +# Node 2: joining as learner (role=3, status=0) +initial_cluster = [ + { id = 1, address = "0.0.0.0:9081", role = 2, status = 2 }, # Existing leader + { id = 2, address = "0.0.0.0:9082", role = 3, status = 0 }, # Self: Learner, Joining +] + +# initial_cluster_size is IMMUTABLE - must match node 1's config +initial_cluster_size = 1 + +db_root_dir = "./db" +log_dir = "./logs" + + +[raft] +general_raft_timeout_duration_in_ms = 100 + +[raft.commit_handler] +batch_size_threshold = 500 +process_interval_ms = 10 +max_entries_per_chunk = 100 + +[raft.election] +election_timeout_min = 1000 +election_timeout_max = 2000 + +[raft.read_consistency] +default_policy = "LeaseRead" +lease_duration_ms = 500 + +[raft.replication] +rpc_append_entries_in_batch_threshold = 5000 +rpc_append_entries_batch_process_delay_in_ms = 0 + +[raft.persistence] +strategy = "MemFirst" +flush_policy = { Batch = { threshold = 100, interval_ms = 20 } } +max_buffered_entries = 10000 +flush_workers = 4 +channel_capacity = 1000 + +[raft.snapshot] +enable = false +max_log_entries_before_snapshot = 10000 +snapshot_cool_down_since_last_check = { secs = 60 } +snapshots_dir = "./snapshots/" +retained_log_entries = 3 + +# == Network Control Plane == +[network.control] +concurrency_limit = 50 +max_concurrent_streams = 1024 +connection_window_size = 4_194_304 +initial_stream_window_size = 2_097_152 +enable_connect_protocol = true + +tcp_keepalive_in_secs = 60 +http2_keep_alive_interval_in_secs = 15 +http2_keep_alive_timeout_in_secs = 10 +max_frame_size = 16_777_215 + +http2_max_pending_accept_reset_streams = 1000 +http2_max_send_buffer_size = 8_388_608 + +# == Network Data Plane == +[network.data] +connect_timeout_in_ms = 100 +request_timeout_in_ms = 300 +concurrency_limit = 100 +max_concurrent_streams = 4096 +connection_window_size = 8_388_608 +initial_stream_window_size = 4_194_304 + +tcp_keepalive_in_secs = 60 +http2_keep_alive_interval_in_secs = 15 +http2_keep_alive_timeout_in_secs = 10 +max_frame_size = 16_777_215 + +http2_adaptive_window = true +http2_max_pending_accept_reset_streams = 2000 +http2_max_send_buffer_size = 16_777_216 diff --git a/examples/single-node-expansion/config/n3.toml b/examples/single-node-expansion/config/n3.toml new file mode 100644 index 00000000..a1bf26a8 --- /dev/null +++ b/examples/single-node-expansion/config/n3.toml @@ -0,0 +1,91 @@ +# Node 3 Configuration - Joining Node (Dynamic Expansion) +# This node joins after node 2 has joined + +[cluster] +node_id = 3 +listen_address = "0.0.0.0:9083" + +# List ALL nodes in the cluster (existing + self) +# Node 1: existing leader (role=2, status=2) +# Node 2: existing member (role=1, status=2) - assumes node 2 is already promoted +# Node 3: joining as learner (role=3, status=0) +initial_cluster = [ + { id = 1, address = "0.0.0.0:9081", role = 2, status = 2 }, # Existing leader + { id = 2, address = "0.0.0.0:9082", role = 1, status = 2 }, # Existing follower (promoted) + { id = 3, address = "0.0.0.0:9083", role = 3, status = 0 }, # Self: Learner, Joining +] + +# initial_cluster_size is IMMUTABLE - must match node 1's config +initial_cluster_size = 1 + +db_root_dir = "./db" +log_dir = "./logs" + + +[raft] +general_raft_timeout_duration_in_ms = 100 + +[raft.commit_handler] +batch_size_threshold = 500 +process_interval_ms = 10 +max_entries_per_chunk = 100 + +[raft.election] +election_timeout_min = 1000 +election_timeout_max = 2000 + +[raft.read_consistency] +default_policy = "LeaseRead" +lease_duration_ms = 500 + +[raft.replication] +rpc_append_entries_in_batch_threshold = 5000 +rpc_append_entries_batch_process_delay_in_ms = 0 + +[raft.persistence] +strategy = "MemFirst" +flush_policy = { Batch = { threshold = 100, interval_ms = 20 } } +max_buffered_entries = 10000 +flush_workers = 4 +channel_capacity = 1000 + +[raft.snapshot] +enable = false +max_log_entries_before_snapshot = 10000 +snapshot_cool_down_since_last_check = { secs = 60 } +snapshots_dir = "./snapshots/" +retained_log_entries = 3 + +# == Network Control Plane == +[network.control] +concurrency_limit = 50 +max_concurrent_streams = 1024 +connection_window_size = 4_194_304 +initial_stream_window_size = 2_097_152 +enable_connect_protocol = true + +tcp_keepalive_in_secs = 60 +http2_keep_alive_interval_in_secs = 15 +http2_keep_alive_timeout_in_secs = 10 +max_frame_size = 16_777_215 + +http2_max_pending_accept_reset_streams = 1000 +http2_max_send_buffer_size = 8_388_608 + +# == Network Data Plane == +[network.data] +connect_timeout_in_ms = 100 +request_timeout_in_ms = 300 +concurrency_limit = 100 +max_concurrent_streams = 4096 +connection_window_size = 8_388_608 +initial_stream_window_size = 4_194_304 + +tcp_keepalive_in_secs = 60 +http2_keep_alive_interval_in_secs = 15 +http2_keep_alive_timeout_in_secs = 10 +max_frame_size = 16_777_215 + +http2_adaptive_window = true +http2_max_pending_accept_reset_streams = 2000 +http2_max_send_buffer_size = 16_777_216 diff --git a/examples/single-node-expansion/src/main.rs b/examples/single-node-expansion/src/main.rs new file mode 100644 index 00000000..7e9017a6 --- /dev/null +++ b/examples/single-node-expansion/src/main.rs @@ -0,0 +1,202 @@ +use d_engine::NodeBuilder; +// use d_engine::FileStateMachine; +// use d_engine::FileStorageEngine; +use d_engine::RocksDBStateMachine; +use d_engine::RocksDBStorageEngine; +use std::env; +use std::error::Error; +use std::fs::OpenOptions; +use std::path::Path; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; +use tokio::signal::unix::signal; +use tokio::signal::unix::SignalKind; +use tokio::sync::watch; +use tokio_metrics::RuntimeMonitor; +use tracing::error; +use tracing::info; +use tracing_appender::non_blocking::WorkerGuard; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::EnvFilter; +use tracing_subscriber::Layer; + +#[tokio::main(flavor = "multi_thread", worker_threads = 2)] +// #[tokio::main(flavor = "current_thread")] +async fn main() { + let db_path: PathBuf = env::var("DB_PATH") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from("/tmp/db")); + + let log_dir = env::var("LOG_DIR") + .map_err(|_| "LOG_DIR environment variable not set") + .expect("Set log dir successfully."); + + let metrics_port: u16 = env::var("METRICS_PORT") + .map(|v| v.parse::().expect("METRICS_PORT must be a valid port")) + .unwrap_or(9000); // default 9000 if not set + + if env::var("TOKIO_CONSOLE").is_ok() { + let tokio_console_port: u16 = env::var("TOKIO_CONSOLE_PORT") + .map(|v| v.parse::().expect("TOKIO_CONSOLE_PORT must be a valid port")) + .unwrap_or(6669); + + println!("Tokio Console port: {tokio_console_port}"); + + console_subscriber::Builder::default() + .server_addr(([127, 0, 0, 1], tokio_console_port)) + .init(); + + // Your application code here + println!("Application started with Tokio Console monitoring"); + } else { + // Initialize the log system + let _guard = init_observability(log_dir); + } + + // Initializing Shutdown Signal + let (graceful_tx, graceful_rx) = watch::channel(()); + + // Start the server (wait for its initialization to complete) + let server_handler = tokio::spawn(start_dengine_server(db_path, graceful_rx.clone())); + + // Wait for the server to initialize (adjust the waiting time according to the actual logic) + tokio::time::sleep(Duration::from_secs(1)).await; + + // --- Start Prometheus metrics server --- + let metrics_handle = tokio::spawn(start_metrics_server(metrics_port)); + + // Monitor shutdown signals + let shutdown_handler = tokio::spawn(graceful_shutdown(graceful_tx)); + + // Wait for all tasks to complete (or error) + if env::var("TOKIO_CONSOLE").is_ok() { + // Initialize Tokio metrics monitoring + let handle = tokio::runtime::Handle::current(); + let runtime_monitor = RuntimeMonitor::new(&handle); + // Start the Tokio metrics collection task + let tokio_metrics_handle = + tokio::spawn(collect_tokio_metrics(runtime_monitor, graceful_rx.clone())); + let (_server_result, _metrics_result, _shutdown_result, _tokio_metrics_result) = tokio::join!( + server_handler, + metrics_handle, + shutdown_handler, + tokio_metrics_handle + ); + } else { + let (_server_result, _metrics_result, _shutdown_result) = + tokio::join!(server_handler, metrics_handle, shutdown_handler); + } +} + +async fn start_dengine_server( + db_path: PathBuf, + graceful_rx: watch::Receiver<()>, +) { + // Option 1: RAW FILE + // let storage_engine = Arc::new(FileStorageEngine::new(db_path.join("storage_engine")).unwrap()); + // let state_machine = Arc::new(FileStateMachine::new(db_path.join("state_machine")).await.unwrap()); + + // Option 2: ROCKSDB + let storage_engine = Arc::new(RocksDBStorageEngine::new(db_path.join("storage")).unwrap()); + let state_machine = Arc::new(RocksDBStateMachine::new(db_path.join("state_machine")).unwrap()); + + // Start Node + let node = NodeBuilder::new(None, graceful_rx.clone()) + .storage_engine(storage_engine) + .state_machine(state_machine) + .start_server() + .await + .expect("start node failed."); + + // Start Node + if let Err(e) = node.run().await { + error!("node stops: {:?}", e); + } else { + info!("node stops."); + } + + println!("Exiting program."); +} + +async fn graceful_shutdown(graceful_tx: watch::Sender<()>) { + info!("Monitoring shutdown signal ..."); + let mut sigint = signal(SignalKind::interrupt()).unwrap(); + let mut sigterm = signal(SignalKind::terminate()).unwrap(); + tokio::select! { + _ = sigint.recv() => { + info!("SIGINT detected."); + }, + _ = sigterm.recv() => { + info!("SIGTERM detected."); + }, + _ = tokio::signal::ctrl_c() => { + info!("Ctrl+C detected."); + } + } + + graceful_tx.send(()).unwrap(); + + info!("Shutdown completed"); +} + +pub fn init_observability(log_dir: String) -> Result> { + let log_file = open_file_for_append(Path::new(&log_dir).join("d.log")).unwrap(); + + let (non_blocking, guard) = tracing_appender::non_blocking(log_file); + let base_subscriber = tracing_subscriber::fmt::layer() + .with_writer(non_blocking) + .with_filter(EnvFilter::from_default_env()); + tracing_subscriber::registry().with(base_subscriber).init(); + Ok(guard) +} + +async fn start_metrics_server(port: u16) { + // Start Prometheus exporter with dynamic port + println!("Metrics server will start at http://0.0.0.0:{port}/metrics",); + metrics_exporter_prometheus::PrometheusBuilder::new() + .with_http_listener(([0, 0, 0, 0], port)) + .install() + .expect("failed to start Prometheus metrics exporter"); +} + +// Tokio metrics collection function +async fn collect_tokio_metrics( + monitor: RuntimeMonitor, + mut graceful_rx: watch::Receiver<()>, +) { + let mut interval = tokio::time::interval(Duration::from_secs(1)); + let mut intervals = monitor.intervals(); + loop { + tokio::select! { + _ = interval.tick() => { + if let Some(metrics) = intervals.next() { + metrics::gauge!("tokio.runtime.workers_count").set(metrics.workers_count as f64); + metrics::counter!("tokio.runtime.park_total").absolute(metrics.total_park_count); + metrics::gauge!("tokio.runtime.park_max").set(metrics.max_park_count as f64); + metrics::gauge!("tokio.runtime.park_min").set(metrics.min_park_count as f64); + metrics::histogram!("tokio.runtime.busy_duration_ns") + .record(metrics.total_busy_duration.as_nanos() as f64); + } + } + _ = graceful_rx.changed() => { + info!("Shutting down Tokio metrics collection"); + break; + } + } + } +} + +fn open_file_for_append(path: PathBuf) -> Result> { + // Create parent directories if they don't exist + if let Some(parent) = path.parent() { + if parent != Path::new("") { + std::fs::create_dir_all(parent)?; + } + } + + let log_file = OpenOptions::new().append(true).create(true).open(&path)?; + + Ok(log_file) +} diff --git a/examples/three-nodes-cluster/Makefile b/examples/three-nodes-cluster/Makefile index 1e66fcbf..acd352e8 100644 --- a/examples/three-nodes-cluster/Makefile +++ b/examples/three-nodes-cluster/Makefile @@ -1,6 +1,6 @@ # Makefile for DEngine cluster operations -.PHONY: build start-cluster clean clean-log-db help \ +.PHONY: build start-cluster start-single-node clean clean-log-db help \ start-node1 start-node2 start-node3 join-node4 join-node5 \ perf-node1 perf-node2 perf-node3 perf-cluster \ tokio-console-node1 tokio-console-node2 tokio-console-node3 tokio-console-cluster @@ -21,6 +21,16 @@ build: # =============================== # Cluster Management (Normal Mode) # =============================== +start-single-node: + @echo "Starting Single Node (standalone mode)..." + CONFIG_PATH=config/node \ + DB_PATH="./db/single" \ + LOG_DIR="./logs/single" \ + METRICS_PORT=8081 \ + RUST_LOG=demo=$(LOG_LEVEL),d_engine=$(LOG_LEVEL),timing=$(LOG_LEVEL) \ + RUST_BACKTRACE=1 \ + ./target/release/demo + start-node1: @echo "Starting Node 1..." CONFIG_PATH=config/n1 \ @@ -173,6 +183,7 @@ clean-log-db: help: @echo "DEngine Cluster Management Commands:" @echo " build - Build release binary" + @echo " start-single-node - Start single node in standalone mode" @echo " start-node1..3 - Start individual cluster nodes" @echo " join-node4..5 - Join learner nodes to cluster" @echo " start-cluster - Start all 3 nodes in parallel" diff --git a/examples/three-nodes-cluster/README.md b/examples/three-nodes-cluster/README.md index c26015aa..ec123406 100644 --- a/examples/three-nodes-cluster/README.md +++ b/examples/three-nodes-cluster/README.md @@ -1,9 +1,31 @@ -## Starting a Three-Node Cluster +## Quick Start Guide -### Quick Start with Makefile (Recommended) +### Single-Node Mode (Standalone) + +Perfect for development, testing, or small deployments that don't require high availability. ```bash -# First build (ensure cargo is installed) +# Build and start single node +make build +make start-single-node + +# The node will automatically become leader without election +# Suitable for budget-constrained startups or development environments +``` + +**Use Cases:** + +- Development and testing environments +- Small-scale deployments with budget constraints +- Learning d-engine without cluster complexity +- Later expandable to 3-node cluster without data loss (roadmap #179) + +### Three-Node Cluster Mode (High Availability) + +Production-ready setup with automatic failover and data replication. + +````bash +# Build (ensure cargo is installed) make build # Start full cluster (automatically in parallel) @@ -19,7 +41,21 @@ make clean # Clean all build artifacts and logs For manual control, run these in separate terminal sessions: -### Node 1 +#### Single Node (Standalone) + +```bash +CONFIG_PATH=config/node \ +DB_PATH="./db/single" \ +LOG_DIR="./logs/single" \ +METRICS_PORT=8081 \ +RUST_LOG=demo=debug,d_engine=debug \ +RUST_BACKTRACE=1 \ +target/release/demo +``` + +#### Three-Node Cluster + +**Node 1:** ```bash CONFIG_PATH=config/n1 \ @@ -27,9 +63,9 @@ LOG_DIR="./logs/1" \ RUST_LOG=demo=debug,d_engine=debug \ RUST_BACKTRACE=1 \ target/release/demo -``` +```` -### Node 2 +**Node 2:** ```bash CONFIG_PATH=config/n2 \ @@ -38,7 +74,7 @@ RUST_LOG=demo=debug,d_engine=debug \ target/release/demo ``` -### Node 3 +**Node 3:** ```bash CONFIG_PATH=config/n3 \ @@ -72,13 +108,16 @@ config/ ### Key Makefile Commands -| **Command** | **Description** | -| -------------------- | ------------------------------- | -| `make build` | Build release binary | -| `make start-cluster` | Start 3-node cluster (parallel) | -| `make start-nodeN` | Start individual node (N=1,2,3) | -| `make clean` | Clean build artifacts & logs | -| `make help` | Show all available commands | +| **Command** | **Description** | +| ------------------------ | ----------------------------------- | +| `make build` | Build release binary | +| `make start-single-node` | Start single node (standalone mode) | +| `make start-cluster` | Start 3-node cluster (parallel) | +| `make start-nodeN` | Start individual node (N=1,2,3) | +| `make join-nodeN` | Join learner node (N=4,5) | +| `make clean` | Clean build artifacts & logs | +| `make clean-log-db` | Clean only logs and database files | +| `make help` | Show all available commands | ### Log Monitoring @@ -88,6 +127,16 @@ tail -f logs/1/demo.log# Watch node 1 logstail -f logs/2/demo.log# Watch node 2 ## Important Notes +### Single-Node Mode + +1. **No high availability**: Single point of failure +2. **No data replication**: Data loss if node crashes +3. **Automatic leader election**: Node becomes leader immediately without election +4. **Performance**: Lower latency (no replication overhead) +5. **Use case**: Development, testing, or budget-constrained deployments + +### Three-Node Cluster Mode + 1. Node communication depends on addresses in config files 2. Default log levels: - `debug`: Engine internals @@ -96,8 +145,10 @@ tail -f logs/1/demo.log# Watch node 1 logstail -f logs/2/demo.log# Watch node 2 - Auto-creates log directories - Verifies binary existence - Use Ctrl+C to terminate all nodes -4. Cluster requires all 3 nodes to be operational +4. Cluster requires majority (2/3) nodes to be operational 5. Nodes must run on different ports (pre-configured in example) +6. **High availability**: Automatic failover on node failure +7. **Data safety**: Replicated across all nodes ## Recommended Directory Structure diff --git a/examples/three-nodes-cluster/config/node.toml b/examples/three-nodes-cluster/config/node.toml new file mode 100644 index 00000000..d0cec20b --- /dev/null +++ b/examples/three-nodes-cluster/config/node.toml @@ -0,0 +1,86 @@ +[cluster] +node_id = 1 +listen_address = "0.0.0.0:9081" +initial_cluster = [ + { id = 1, address = "0.0.0.0:9081", role = 0, status = 2 }, +] +db_root_dir = "./db" +log_dir = "./logs" + + +[raft] +general_raft_timeout_duration_in_ms = 100 + +[raft.commit_handler] +batch_size_threshold = 500 +process_interval_ms = 10 +max_entries_per_chunk = 100 + +[raft.election] +election_timeout_min = 1000 +election_timeout_max = 2000 + +[raft.read_consistency] +default_policy = "LeaseRead" +lease_duration_ms = 500 + +[raft.replication] +# Process commands when this number is reached +rpc_append_entries_in_batch_threshold = 5000 +# Ensure the batch is processed within a maximum delay (e.g., 10ms) +rpc_append_entries_batch_process_delay_in_ms = 0 + +[raft.persistence] +# strategy = "DiskFirst" +strategy = "MemFirst" +# flush_policy = "Immediate" +flush_policy = { Batch = { threshold = 100, interval_ms = 20 } } +# Upcoming feature(v0.1.4) +# Maximum number of log entries to buffer in memory +# when using async persistence strategies (MemFirst/Batched) +max_buffered_entries = 10000 +flush_workers = 4 +channel_capacity=1000 + +[raft.snapshot] +enable = false +max_log_entries_before_snapshot = 10000 +snapshot_cool_down_since_last_check = { secs = 60 } +snapshots_dir = "./snapshots/" +retained_log_entries = 3 + +# == Network Control Plane (voting, heartbeat, etc.) == +[network.control] +concurrency_limit = 50 # Increase to 50 to reduce lock contention +max_concurrent_streams = 1024 # Increase significantly to reduce stream creation overhead +connection_window_size = 4_194_304 # Increase to 4MB to reduce flow control blocking +initial_stream_window_size = 2_097_152 # New: initial stream window 2MB +enable_connect_protocol = true # Enable HTTP/2 connect protocol optimization + +tcp_keepalive_in_secs = 60 +http2_keep_alive_interval_in_secs = 15 # Slightly increase to reduce frequent keep-alives +http2_keep_alive_timeout_in_secs = 10 # Increase timeout +max_frame_size = 16_777_215 + +# New performance tuning parameters +http2_max_pending_accept_reset_streams = 1000 # Increase limit for pending streams +http2_max_send_buffer_size = 8_388_608 # Send buffer 8MB + +# == Network Data Plane (append_entries, etc.) == +[network.data] +connect_timeout_in_ms = 100 +request_timeout_in_ms = 300 +concurrency_limit = 100 # Increase significantly to support higher concurrent replication +max_concurrent_streams = 4096 # Increase significantly to support batch replication +connection_window_size = 8_388_608 # Increase to 8MB to support large log entries +initial_stream_window_size = 4_194_304 # Initial stream window 4MB + +tcp_keepalive_in_secs = 60 +http2_keep_alive_interval_in_secs = 15 # Same as control plane +http2_keep_alive_timeout_in_secs = 10 +max_frame_size = 16_777_215 + +# New data plane optimizations +http2_adaptive_window = true # Enable adaptive window +http2_max_pending_accept_reset_streams = 2000 # Higher pending stream limit +http2_max_send_buffer_size = 16_777_216 # 16MB send buffer