[smg][mesh] extract mesh to mesh crate to reduce compile time (#17907)

This commit is contained in:
Simo Lin
2026-01-29 10:58:42 +09:00
committed by GitHub
parent 0368ddf9ea
commit ac16d4450e
28 changed files with 31 additions and 10065 deletions

View File

@@ -84,6 +84,7 @@ wfaas = "1.0.0"
data-connector = "1.0.0"
smg-mcp = "1.0.0"
smg-wasm = "1.0.0"
smg-mesh = "1.0.0"
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
rustls-pemfile = "2.2"
openssl = "0.10.73"

View File

@@ -12,7 +12,6 @@ macro_rules! set_env {
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Rebuild triggers
println!("cargo:rerun-if-changed=src/mesh/proto/gossip.proto");
println!("cargo:rerun-if-changed=src/proto/sglang_scheduler.proto");
println!("cargo:rerun-if-changed=src/proto/vllm_engine.proto");
println!("cargo:rerun-if-changed=Cargo.toml");
@@ -31,13 +30,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
&["src/proto"],
)?;
// Compile gossip protobuf files
tonic_prost_build::configure()
// Generate both client and server code
.build_server(true)
.build_client(true)
.compile_protos(&["src/mesh/proto/gossip.proto"], &["src/mesh/proto"])?;
// Set version info environment variables
let version = read_cargo_version().unwrap_or_else(|_| DEFAULT_VERSION.to_string());
let target = std::env::var("TARGET").unwrap_or_else(|_| get_rustc_host().unwrap_or_default());

View File

@@ -5,7 +5,7 @@ pub mod core;
pub use data_connector;
pub mod grpc_client;
pub use smg_mcp as mcp;
pub mod mesh;
pub use smg_mesh as mesh;
pub mod middleware;
pub mod observability;
pub mod policies;

File diff suppressed because it is too large Load Diff

View File

@@ -1,212 +0,0 @@
//! Consistent hashing for rate-limit ownership
//!
//! Implements consistent hashing ring to determine K owners (K=1-3) for each rate-limit key.
//! Supports ownership transfer on node failures.
use std::{
collections::{hash_map::DefaultHasher, BTreeMap, HashSet},
hash::{Hash, Hasher},
};
/// Number of virtual nodes per physical node (for better distribution)
const VIRTUAL_NODES_PER_NODE: usize = 150;
/// Number of owners (K) for each key
const NUM_OWNERS: usize = 3;
/// Consistent hash ring
#[derive(Debug, Clone)]
pub struct ConsistentHashRing {
/// Ring: hash -> node_name
ring: BTreeMap<u64, String>,
/// Node -> set of virtual node hashes
node_hashes: BTreeMap<String, HashSet<u64>>,
}
impl ConsistentHashRing {
pub fn new() -> Self {
Self {
ring: BTreeMap::new(),
node_hashes: BTreeMap::new(),
}
}
/// Add a node to the ring
pub fn add_node(&mut self, node_name: &str) {
if self.node_hashes.contains_key(node_name) {
// Node already exists
return;
}
let mut hashes = HashSet::new();
for i in 0..VIRTUAL_NODES_PER_NODE {
let virtual_node = format!("{}:{}", node_name, i);
let hash = Self::hash(&virtual_node);
self.ring.insert(hash, node_name.to_string());
hashes.insert(hash);
}
self.node_hashes.insert(node_name.to_string(), hashes);
}
/// Remove a node from the ring
pub fn remove_node(&mut self, node_name: &str) {
if let Some(hashes) = self.node_hashes.remove(node_name) {
for hash in hashes {
self.ring.remove(&hash);
}
}
}
pub fn get_owners(&self, key: &str) -> Vec<&str> {
if self.ring.is_empty() {
return Vec::new();
}
let key_hash = Self::hash(key);
let mut owners = Vec::with_capacity(NUM_OWNERS);
let total_unique_nodes = self.node_hashes.len();
let mut iter = self.ring.range(key_hash..);
while owners.len() < NUM_OWNERS && owners.len() < total_unique_nodes {
if let Some((_, node)) = iter.next() {
if !owners.contains(&node.as_str()) {
owners.push(node.as_str());
}
} else {
iter = self.ring.range(..);
}
}
owners
}
/// Check if a node is an owner of a key
pub fn is_owner(&self, key: &str, node_name: &str) -> bool {
self.get_owners(key).contains(&node_name)
}
/// Get all nodes in the ring
pub fn get_nodes(&self) -> Vec<String> {
self.node_hashes.keys().cloned().collect()
}
/// Check if a node exists in the ring
pub fn has_node(&self, node_name: &str) -> bool {
self.node_hashes.contains_key(node_name)
}
/// Hash a string to u64
fn hash(s: &str) -> u64 {
let mut hasher = DefaultHasher::new();
s.hash(&mut hasher);
hasher.finish()
}
/// Update ring with current membership
pub fn update_membership(&mut self, nodes: &[String]) {
let current_nodes: HashSet<String> = self.node_hashes.keys().cloned().collect();
let new_nodes: HashSet<String> = nodes.iter().cloned().collect();
// Remove nodes that are no longer present
for node in current_nodes.difference(&new_nodes) {
self.remove_node(node);
}
// Add new nodes
for node in new_nodes.difference(&current_nodes) {
self.add_node(node);
}
}
}
impl Default for ConsistentHashRing {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_remove_node() {
let mut ring = ConsistentHashRing::new();
ring.add_node("node1");
assert!(ring.has_node("node1"));
assert_eq!(ring.get_nodes().len(), 1);
ring.add_node("node2");
assert_eq!(ring.get_nodes().len(), 2);
ring.remove_node("node1");
assert!(!ring.has_node("node1"));
assert_eq!(ring.get_nodes().len(), 1);
}
#[test]
fn test_get_owners() {
let mut ring = ConsistentHashRing::new();
ring.add_node("node1");
ring.add_node("node2");
ring.add_node("node3");
let owners = ring.get_owners("test_key");
assert_eq!(owners.len(), NUM_OWNERS);
assert!(owners.iter().all(|n| ring.has_node(n)));
}
#[test]
fn test_is_owner() {
let mut ring = ConsistentHashRing::new();
ring.add_node("node1");
ring.add_node("node2");
ring.add_node("node3");
let owners = ring.get_owners("test_key");
for owner in &owners {
assert!(ring.is_owner("test_key", owner));
}
}
#[test]
fn test_update_membership() {
let mut ring = ConsistentHashRing::new();
ring.add_node("node1");
ring.add_node("node2");
ring.update_membership(&["node2".to_string(), "node3".to_string()]);
assert!(!ring.has_node("node1"));
assert!(ring.has_node("node2"));
assert!(ring.has_node("node3"));
}
#[test]
fn test_get_owners_with_fewer_nodes_than_owners() {
// Test that the loop terminates correctly when there are fewer nodes than NUM_OWNERS
let mut ring = ConsistentHashRing::new();
ring.add_node("node1");
ring.add_node("node2");
// Only 2 nodes, but NUM_OWNERS is 3
let owners = ring.get_owners("test_key");
// Should return all available nodes (2) without infinite loop
assert_eq!(owners.len(), 2);
assert!(owners.contains(&"node1"));
assert!(owners.contains(&"node2"));
}
#[test]
fn test_get_owners_with_single_node() {
// Test with only one node
let mut ring = ConsistentHashRing::new();
ring.add_node("node1");
let owners = ring.get_owners("test_key");
// Should return the single node without infinite loop
assert_eq!(owners.len(), 1);
assert_eq!(owners[0], "node1");
}
}

View File

@@ -1,266 +0,0 @@
use std::{
collections::{BTreeMap, HashMap},
net::SocketAddr,
time::Duration,
};
use anyhow::Result;
use rand::seq::{IndexedRandom, SliceRandom};
use tracing as log;
use tracing::instrument;
use super::{
flow_control::RetryManager,
gossip::{gossip_message, NodeState, NodeStatus, Ping, PingReq, StateSync},
service::{broadcast_node_states, try_ping},
ClusterState,
};
pub struct MeshController {
state: ClusterState,
self_name: String,
self_addr: SocketAddr,
init_peer: Option<SocketAddr>,
}
impl MeshController {
pub fn new(
state: ClusterState,
self_addr: SocketAddr,
self_name: &str,
init_peer: Option<SocketAddr>,
) -> Self {
Self {
state,
self_name: self_name.to_string(),
self_addr,
init_peer,
}
}
#[instrument(fields(name = %self.self_name), skip(self, signal))]
pub async fn event_loop(self, mut signal: tokio::sync::watch::Receiver<()>) -> Result<()> {
let init_state = self.state.clone();
let read_state = self.state.clone();
let mut cnt: u64 = 0;
// Track retry managers for each peer
use std::collections::HashMap;
let mut retry_managers: HashMap<String, RetryManager> = HashMap::new();
loop {
log::info!("Round {} Status:{:?}", cnt, read_state.read());
// Get available peers from cluster state
let mut map = init_state.read().clone();
map.retain(|k, v| {
k.ne(&self.self_name.to_string())
&& v.status != NodeStatus::Down as i32
&& v.status != NodeStatus::Leaving as i32
});
let peer = if cnt == 0 && map.is_empty() {
// Only use init_peer if cluster state is empty (no service discovery)
self.init_peer.map(|init_peer| NodeState {
name: "init_peer".to_string(),
address: init_peer.to_string(),
status: NodeStatus::Suspected as i32,
version: 1,
metadata: HashMap::new(),
})
} else {
// Use nodes from cluster state (from service discovery or gossip)
let random_nodes = get_random_values_refs(&map, 1);
random_nodes.first().map(|&node| node.clone())
};
cnt += 1;
tokio::select! {
_ = signal.changed() => {
log::info!("Gossip app_server {} at {} is shutting down", self.self_name, self.self_addr);
break;
}
_ = tokio::time::sleep(Duration::from_secs(1)) => {
if let Some(peer) = peer {
let peer_name = peer.name.clone();
// Get or create retry manager for this peer
let retry_manager = retry_managers
.entry(peer_name.clone())
.or_default();
// Check if we should retry based on backoff
if retry_manager.should_retry() {
match self.connect_to_peer(peer.clone()).await {
Ok(_) => {
// Success - reset retry state
retry_manager.reset();
log::info!("Successfully connected to peer {}", peer_name);
}
Err(e) => {
// Failure - record attempt and calculate next delay
retry_manager.record_attempt();
let next_delay = retry_manager.next_delay();
let attempt = retry_manager.attempt_count();
log::warn!(
"Error connecting to peer {} (attempt {}): {}. Next retry in {:?}",
peer_name,
attempt,
e,
next_delay
);
}
}
} else {
// Still in backoff period, skip this attempt
let next_delay = retry_manager.next_delay();
log::debug!(
"Skipping connection to peer {} (backoff: {:?} remaining)",
peer_name,
next_delay
);
}
} else {
log::info!("No peer address available to connect");
}
}
}
}
Ok(())
}
async fn connect_to_peer(&self, peer: NodeState) -> Result<()> {
log::info!("Connecting to peer {} at {}", peer.name, peer.address);
let read_state = self.state.clone();
// TODO: Maybe we don't need to send the whole state.
let state_sync = StateSync {
nodes: read_state.read().values().cloned().collect(),
};
let peer_addr = peer.address.parse::<SocketAddr>()?;
let peer_name = peer.name.clone();
match try_ping(
&peer,
Some(gossip_message::Payload::Ping(Ping {
state_sync: Some(state_sync),
})),
)
.await
{
Ok(node_update) => {
log::info!("Received NodeUpdate from peer: {:?}", node_update);
// Update state for Alive or Leaving status
if node_update.status == NodeStatus::Alive as i32
|| node_update.status == NodeStatus::Leaving as i32
{
let mut s = read_state.write();
s.entry(node_update.name.clone())
.and_modify(|e| e.status = node_update.status)
.or_insert(NodeState {
name: node_update.name,
address: node_update.address,
status: node_update.status,
version: 1,
metadata: HashMap::new(),
});
}
}
Err(e) => {
log::info!("Failed to connect to peer: {}, now try ping-req", e);
let mut map = read_state.read().clone();
map.retain(|k, v| {
k.ne(&self.self_name)
&& k.ne(&peer_name)
&& v.status == NodeStatus::Alive as i32
&& v.status != NodeStatus::Leaving as i32
});
let random_nodes = get_random_values_refs(&map, 3);
let mut reachable = false;
for node in random_nodes {
log::info!(
"Trying to ping-req node {}, req target: {}",
node.address,
peer_addr
);
if try_ping(
node,
Some(gossip_message::Payload::PingReq(PingReq {
node: Some(peer.clone()),
})),
)
.await
.is_ok()
{
reachable = true;
break;
}
}
if !reachable {
let mut target = read_state.read().clone();
// Broadcast only the unreachable node's status is enough.
if let Some(mut unreachable_node) = target.remove(&peer_name) {
if unreachable_node.status == NodeStatus::Suspected as i32 {
unreachable_node.status = NodeStatus::Down as i32
} else {
unreachable_node.status = NodeStatus::Suspected as i32
}
unreachable_node.version += 1;
// Broadcast target nodes should include self.
let target_nodes: Vec<NodeState> = target
.values()
.filter(|v| {
v.name.ne(&peer_name)
&& v.status == NodeStatus::Alive as i32
&& v.status != NodeStatus::Leaving as i32
})
.cloned()
.collect();
log::info!(
"Broadcasting node status to {} alive nodes, new_state: {:?}",
target_nodes.len(),
unreachable_node
);
let (success_count, total_count) = broadcast_node_states(
vec![unreachable_node],
target_nodes,
None, // Use default timeout
)
.await;
log::info!(
"Broadcast node status: {}/{} successful",
success_count,
total_count
);
}
}
}
}
log::info!("Successfully connected to peer {}", peer_addr);
Ok(())
}
}
// TODO: Support weighted random selection. e.g. nodes in INIT state should be more likely to be selected.
fn get_random_values_refs<K, V>(map: &BTreeMap<K, V>, k: usize) -> Vec<&V> {
let values: Vec<&V> = map.values().collect();
if k >= values.len() {
let mut all_values = values;
all_values.shuffle(&mut rand::rng());
return all_values;
}
let mut rng = rand::rng();
values.choose_multiple(&mut rng, k).cloned().collect()
}

View File

@@ -1,962 +0,0 @@
//! CRDT (Conflict-free Replicated Data Types) wrapper for HA state synchronization
//!
//! This module provides CRDT data structures for eventual consistency:
//! - Map<SKey, LWWReg> for Last-Write-Wins Register maps
//! - PNCounter for rate-limit and load balance aggregates
use std::{
collections::BTreeMap,
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
use crdts::{CmRDT, CvRDT, PNCounter};
use num_bigint::BigInt;
use num_traits::ToPrimitive;
use parking_lot::RwLock;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
/// State key for CRDT maps
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct SKey(pub String);
impl SKey {
pub fn new(key: String) -> Self {
Self(key)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<String> for SKey {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for SKey {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
/// Last-Write-Wins Register wrapper
/// Simplified implementation using timestamp and version
#[derive(Debug, Clone, serde::Serialize)]
#[serde(bound(serialize = "T: Serialize"))]
#[derive(serde::Deserialize)]
#[serde(bound(deserialize = "T: DeserializeOwned"))]
pub struct LWWRegister<T: Clone + Serialize + DeserializeOwned> {
value: T,
timestamp: u64,
version: u64,
actor: String,
}
impl<T: Clone + Serialize + DeserializeOwned> LWWRegister<T> {
pub fn new(value: T, actor: String) -> Self {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos() as u64;
Self {
value,
timestamp,
version: 1,
actor,
}
}
pub fn read(&self) -> &T {
&self.value
}
pub fn write(&mut self, value: T, actor: String) {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos() as u64;
self.value = value;
self.timestamp = timestamp;
self.version += 1;
self.actor = actor;
}
pub fn merge(&mut self, other: &Self) {
// Last-Write-Wins: choose the one with higher timestamp, or higher version if equal
if other.timestamp > self.timestamp
|| (other.timestamp == self.timestamp && other.version > self.version)
{
self.value = other.value.clone();
self.timestamp = other.timestamp;
self.version = other.version;
self.actor = other.actor.clone();
}
}
}
/// CRDT Map wrapper using LWWRegister for values
/// Simplified implementation using BTreeMap with LWWRegister values
#[derive(Debug, Clone, serde::Serialize)]
#[serde(bound(serialize = "T: Serialize + DeserializeOwned"))]
#[derive(serde::Deserialize)]
#[serde(bound(deserialize = "T: DeserializeOwned"))]
pub struct CRDTMap<T: Clone + Serialize + DeserializeOwned> {
inner: BTreeMap<SKey, LWWRegister<T>>,
}
impl<T: Clone + Serialize + DeserializeOwned> Default for CRDTMap<T> {
fn default() -> Self {
Self {
inner: BTreeMap::new(),
}
}
}
impl<T: Clone + Serialize + DeserializeOwned> CRDTMap<T> {
pub fn new() -> Self {
Self::default()
}
pub fn get(&self, key: &SKey) -> Option<&T> {
self.inner.get(key).map(|reg| reg.read())
}
pub fn insert(&mut self, key: SKey, value: T, actor: String) {
// Check if key already exists to preserve version
if let Some(existing_reg) = self.inner.get_mut(&key) {
// Update existing register, which will increment version
existing_reg.write(value, actor);
} else {
// New entry, start with version 1
let reg = LWWRegister::new(value, actor);
self.inner.insert(key, reg);
}
}
/// Get the version and actor for a key
pub fn get_metadata(&self, key: &SKey) -> Option<(u64, String)> {
self.inner
.get(key)
.map(|reg| (reg.version, reg.actor.clone()))
}
pub fn remove(&mut self, key: &SKey) {
self.inner.remove(key);
}
pub fn contains_key(&self, key: &SKey) -> bool {
self.inner.contains_key(key)
}
pub fn iter(&self) -> impl Iterator<Item = (&SKey, &T)> {
self.inner.iter().map(|(k, v)| (k, v.read()))
}
pub fn keys(&self) -> impl Iterator<Item = &SKey> {
self.inner.keys()
}
pub fn values(&self) -> impl Iterator<Item = &T> {
self.inner.values().map(|v| v.read())
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
pub fn merge(&mut self, other: &Self) {
for (key, other_reg) in &other.inner {
match self.inner.get_mut(key) {
Some(self_reg) => {
self_reg.merge(other_reg);
}
None => {
self.inner.insert(key.clone(), other_reg.clone());
}
}
}
}
pub fn to_map(&self) -> BTreeMap<SKey, T> {
self.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
}
}
/// Positive-Negative Counter for rate-limit and load balance aggregates
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CRDTPNCounter {
inner: PNCounter<String>,
}
impl Default for CRDTPNCounter {
fn default() -> Self {
Self {
inner: PNCounter::new(),
}
}
}
impl CRDTPNCounter {
pub fn new() -> Self {
Self::default()
}
pub fn inc(&mut self, actor: String, delta: i64) {
// PNCounter API: inc(actor) and dec(actor) return operations that need to be applied
// In crdts 7.3, we need to call apply() to actually modify the counter
if delta > 0 {
for i in 0..delta as u64 {
// Use a unique actor for each increment to ensure they're all counted
let unique_actor = format!("{}:{}", actor, i);
let op = self.inner.inc(unique_actor);
self.inner.apply(op);
}
} else if delta < 0 {
for i in 0..(-delta) as u64 {
// Use a unique actor for each decrement
let unique_actor = format!("{}:{}", actor, i);
let op = self.inner.dec(unique_actor);
self.inner.apply(op);
}
}
}
pub fn value(&self) -> i64 {
// PNCounter read() returns BigInt in crdts 7.3
let val: BigInt = self.inner.read();
// Convert BigInt to i64, clamping to i64::MAX/i64::MIN if value is out of range
val.to_i64().unwrap_or_else(|| {
// If value is too large, clamp to i64::MAX
if val > BigInt::from(i64::MAX) {
i64::MAX
} else if val < BigInt::from(i64::MIN) {
i64::MIN
} else {
0
}
})
}
pub fn merge(&mut self, other: &Self) {
// Merge PNCounter using CvRDT trait
// CvRDT::merge takes &mut self and other by value, but we need to clone
let other_clone = other.inner.clone();
<PNCounter<String> as CvRDT>::merge(&mut self.inner, other_clone);
}
}
/// Thread-safe wrapper for CRDT Map
#[derive(Debug, Clone)]
pub struct SyncCRDTMap<T: Clone + Serialize + DeserializeOwned> {
inner: Arc<RwLock<CRDTMap<T>>>,
}
impl<T: Clone + Serialize + DeserializeOwned> Default for SyncCRDTMap<T> {
fn default() -> Self {
Self {
inner: Arc::new(RwLock::new(CRDTMap::new())),
}
}
}
impl<T: Clone + Serialize + DeserializeOwned> SyncCRDTMap<T> {
pub fn new() -> Self {
Self::default()
}
pub fn get(&self, key: &SKey) -> Option<T> {
self.inner.read().get(key).cloned()
}
pub fn insert(&self, key: SKey, value: T, actor: String) {
self.inner.write().insert(key, value, actor);
}
/// Get the version and actor for a key
pub fn get_metadata(&self, key: &SKey) -> Option<(u64, String)> {
self.inner.read().get_metadata(key)
}
pub fn remove(&self, key: &SKey) {
self.inner.write().remove(key);
}
pub fn contains_key(&self, key: &SKey) -> bool {
self.inner.read().contains_key(key)
}
pub fn merge(&self, other: &CRDTMap<T>) {
self.inner.write().merge(other);
}
pub fn snapshot(&self) -> CRDTMap<T> {
self.inner.read().clone()
}
pub fn len(&self) -> usize {
self.inner.read().len()
}
pub fn is_empty(&self) -> bool {
self.inner.read().is_empty()
}
}
/// Thread-safe wrapper for PNCounter
#[derive(Debug, Clone)]
pub struct SyncPNCounter {
inner: Arc<RwLock<CRDTPNCounter>>,
}
impl Default for SyncPNCounter {
fn default() -> Self {
Self {
inner: Arc::new(RwLock::new(CRDTPNCounter::new())),
}
}
}
impl SyncPNCounter {
pub fn new() -> Self {
Self::default()
}
pub fn inc(&self, actor: String, delta: i64) {
self.inner.write().inc(actor, delta);
}
pub fn value(&self) -> i64 {
self.inner.read().value()
}
pub fn merge(&self, other: &CRDTPNCounter) {
let mut inner = self.inner.write();
inner.merge(other);
}
pub fn snapshot(&self) -> CRDTPNCounter {
self.inner.read().clone()
}
}
#[cfg(test)]
mod tests {
use std::{thread, time::Duration};
use super::*;
#[test]
fn test_crdt_pncounter_inc_and_value() {
let mut counter = CRDTPNCounter::new();
assert_eq!(counter.value(), 0);
// Test direct PNCounter usage
use crdts::{CmRDT, PNCounter};
let mut pn = PNCounter::new();
let op = pn.inc("actor1".to_string());
pn.apply(op);
let pn_val: BigInt = pn.read();
println!("Direct PNCounter value after inc(1): {:?}", pn_val);
counter.inc("actor1".to_string(), 5);
let val = counter.value();
println!("Counter value after inc(5): {}", val);
println!("Counter inner read(): {:?}", counter.inner.read());
assert!(val > 0, "Counter should be incremented, got: {}", val);
counter.inc("actor2".to_string(), 3);
let val2 = counter.value();
println!("Counter value after inc(3): {}", val2);
assert!(val2 > val, "Counter should be incremented further");
}
// SKey tests
#[test]
fn test_skey_new() {
let key = SKey::new("test_key".to_string());
assert_eq!(key.as_str(), "test_key");
}
#[test]
fn test_skey_from_string() {
let key: SKey = "test_key".to_string().into();
assert_eq!(key.as_str(), "test_key");
}
#[test]
fn test_skey_from_str() {
let key: SKey = "test_key".into();
assert_eq!(key.as_str(), "test_key");
}
#[test]
fn test_skey_ordering() {
let key1 = SKey::new("a".to_string());
let key2 = SKey::new("b".to_string());
assert!(key1 < key2);
}
// LWWRegister tests with i32
#[test]
fn test_lww_register_new() {
let reg = LWWRegister::new(42, "actor1".to_string());
assert_eq!(*reg.read(), 42);
assert_eq!(reg.actor, "actor1");
assert_eq!(reg.version, 1);
}
#[test]
fn test_lww_register_write() {
let mut reg = LWWRegister::new(42, "actor1".to_string());
let old_version = reg.version;
reg.write(100, "actor2".to_string());
assert_eq!(*reg.read(), 100);
assert_eq!(reg.actor, "actor2");
assert_eq!(reg.version, old_version + 1);
}
#[test]
fn test_lww_register_merge_newer_wins() {
let mut reg1 = LWWRegister::new(42, "actor1".to_string());
thread::sleep(Duration::from_millis(1));
let reg2 = LWWRegister::new(100, "actor2".to_string());
reg1.merge(&reg2);
assert_eq!(*reg1.read(), 100);
assert_eq!(reg1.actor, "actor2");
}
// LWWRegister tests with String
#[test]
fn test_lww_register_create_and_read() {
let reg = LWWRegister::new("value1".to_string(), "actor1".to_string());
assert_eq!(reg.read(), "value1");
assert_eq!(reg.version, 1);
assert_eq!(reg.actor, "actor1");
}
#[test]
fn test_lww_register_version_increment() {
let mut reg = LWWRegister::new("value1".to_string(), "actor1".to_string());
let initial_version = reg.version;
reg.write("value2".to_string(), "actor2".to_string());
assert_eq!(reg.version, initial_version + 1);
assert_eq!(reg.read(), "value2");
assert_eq!(reg.actor, "actor2");
}
#[test]
fn test_lww_register_merge_timestamp_priority() {
let mut reg1 = LWWRegister::new("value1".to_string(), "actor1".to_string());
thread::sleep(Duration::from_millis(10)); // Ensure different timestamp
let reg2 = LWWRegister::new("value2".to_string(), "actor2".to_string());
// reg2 has newer timestamp, should win
reg1.merge(&reg2);
assert_eq!(reg1.read(), "value2");
assert_eq!(reg1.actor, "actor2");
}
#[test]
fn test_lww_register_merge_older_loses() {
let reg1 = LWWRegister::new(42, "actor1".to_string());
thread::sleep(Duration::from_millis(1));
let reg2 = LWWRegister::new(100, "actor2".to_string());
let mut reg2_clone = reg2.clone();
reg2_clone.merge(&reg1);
assert_eq!(*reg2_clone.read(), 100);
assert_eq!(reg2_clone.actor, "actor2");
}
#[test]
fn test_lww_register_merge_version_priority() {
let mut reg1 = LWWRegister::new("value1".to_string(), "actor1".to_string());
let mut reg2 = LWWRegister::new("value2".to_string(), "actor2".to_string());
// Set same timestamp but different versions
reg2.timestamp = reg1.timestamp;
reg2.version = reg1.version + 1;
reg1.merge(&reg2);
assert_eq!(reg1.read(), "value2");
assert_eq!(reg1.version, reg2.version);
}
#[test]
fn test_lww_register_concurrent_merge() {
let mut reg1 = LWWRegister::new("value1".to_string(), "actor1".to_string());
thread::sleep(Duration::from_millis(10));
let reg2 = LWWRegister::new("value2".to_string(), "actor2".to_string());
thread::sleep(Duration::from_millis(10));
let reg3 = LWWRegister::new("value3".to_string(), "actor3".to_string());
// Merge in different orders should give same result (latest wins)
reg1.merge(&reg2);
reg1.merge(&reg3);
assert_eq!(reg1.read(), "value3");
let mut reg4 = LWWRegister::new("value1".to_string(), "actor1".to_string());
thread::sleep(Duration::from_millis(10));
let reg5 = LWWRegister::new("value2".to_string(), "actor2".to_string());
thread::sleep(Duration::from_millis(10));
let reg6 = LWWRegister::new("value3".to_string(), "actor3".to_string());
reg4.merge(&reg6);
reg4.merge(&reg5);
// reg6 should win (latest timestamp)
assert_eq!(reg4.read(), "value3");
}
// CRDTMap tests with i32
#[test]
fn test_crdt_map_new() {
let map: CRDTMap<i32> = CRDTMap::new();
assert!(map.is_empty());
assert_eq!(map.len(), 0);
}
#[test]
fn test_crdt_map_insert_get() {
let mut map = CRDTMap::new();
let key = SKey::new("key1".to_string());
map.insert(key.clone(), 42, "actor1".to_string());
assert_eq!(map.get(&key), Some(&42));
assert_eq!(map.len(), 1);
assert!(!map.is_empty());
}
#[test]
fn test_crdt_map_remove() {
let mut map = CRDTMap::new();
let key = SKey::new("key1".to_string());
map.insert(key.clone(), 42, "actor1".to_string());
assert_eq!(map.len(), 1);
map.remove(&key);
assert_eq!(map.get(&key), None);
assert_eq!(map.len(), 0);
assert!(map.is_empty());
}
#[test]
fn test_crdt_map_contains_key() {
let mut map = CRDTMap::new();
let key = SKey::new("key1".to_string());
assert!(!map.contains_key(&key));
map.insert(key.clone(), 42, "actor1".to_string());
assert!(map.contains_key(&key));
}
#[test]
fn test_crdt_map_iter() {
let mut map = CRDTMap::new();
map.insert(SKey::new("key1".to_string()), 1, "actor1".to_string());
map.insert(SKey::new("key2".to_string()), 2, "actor1".to_string());
map.insert(SKey::new("key3".to_string()), 3, "actor1".to_string());
let mut values: Vec<i32> = map.values().cloned().collect();
values.sort();
assert_eq!(values, vec![1, 2, 3]);
}
// CRDTMap tests with String
#[test]
fn test_crdt_map_insert_get_remove_string() {
let mut map = CRDTMap::new();
let key = SKey::new("key1".to_string());
map.insert(key.clone(), "value1".to_string(), "actor1".to_string());
assert_eq!(map.get(&key), Some(&"value1".to_string()));
assert_eq!(map.len(), 1);
map.remove(&key);
assert_eq!(map.get(&key), None);
assert_eq!(map.len(), 0);
}
#[test]
fn test_crdt_map_version_management() {
let mut map = CRDTMap::new();
let key = SKey::new("key1".to_string());
map.insert(key.clone(), "value1".to_string(), "actor1".to_string());
let (version1, actor1) = map.get_metadata(&key).unwrap();
assert_eq!(version1, 1);
assert_eq!(actor1, "actor1");
map.insert(key.clone(), "value2".to_string(), "actor2".to_string());
let (version2, actor2) = map.get_metadata(&key).unwrap();
assert_eq!(version2, 2);
assert_eq!(actor2, "actor2");
}
#[test]
fn test_crdt_map_merge() {
let mut map1 = CRDTMap::new();
map1.insert(SKey::new("key1".to_string()), 1, "actor1".to_string());
map1.insert(SKey::new("key2".to_string()), 2, "actor1".to_string());
let mut map2 = CRDTMap::new();
map2.insert(SKey::new("key2".to_string()), 20, "actor2".to_string());
map2.insert(SKey::new("key3".to_string()), 3, "actor2".to_string());
// Wait a bit to ensure map2 has newer timestamps
thread::sleep(Duration::from_millis(1));
map1.merge(&map2);
assert_eq!(map1.get(&SKey::new("key1".to_string())), Some(&1));
assert_eq!(map1.get(&SKey::new("key2".to_string())), Some(&20)); // Newer value wins
assert_eq!(map1.get(&SKey::new("key3".to_string())), Some(&3));
assert_eq!(map1.len(), 3);
}
#[test]
fn test_crdt_map_merge_string() {
let mut map1 = CRDTMap::new();
let mut map2 = CRDTMap::new();
let key1 = SKey::new("key1".to_string());
let key2 = SKey::new("key2".to_string());
map1.insert(key1.clone(), "value1".to_string(), "actor1".to_string());
map2.insert(key2.clone(), "value2".to_string(), "actor2".to_string());
map1.merge(&map2);
assert_eq!(map1.get(&key1), Some(&"value1".to_string()));
assert_eq!(map1.get(&key2), Some(&"value2".to_string()));
assert_eq!(map1.len(), 2);
}
#[test]
fn test_crdt_map_merge_conflict_resolution() {
let mut map1 = CRDTMap::new();
let mut map2 = CRDTMap::new();
let key = SKey::new("key1".to_string());
map1.insert(key.clone(), "value1".to_string(), "actor1".to_string());
thread::sleep(Duration::from_millis(10));
map2.insert(key.clone(), "value2".to_string(), "actor2".to_string());
// map2 has newer timestamp, should win
map1.merge(&map2);
assert_eq!(map1.get(&key), Some(&"value2".to_string()));
}
#[test]
fn test_crdt_map_to_map() {
let mut map = CRDTMap::new();
map.insert(SKey::new("key1".to_string()), 1, "actor1".to_string());
map.insert(SKey::new("key2".to_string()), 2, "actor1".to_string());
let btree_map = map.to_map();
assert_eq!(btree_map.len(), 2);
assert_eq!(btree_map.get(&SKey::new("key1".to_string())), Some(&1));
assert_eq!(btree_map.get(&SKey::new("key2".to_string())), Some(&2));
}
// CRDTPNCounter tests
#[test]
fn test_pn_counter_new() {
let counter = CRDTPNCounter::new();
assert_eq!(counter.value(), 0);
}
#[test]
fn test_pn_counter_inc_positive() {
let mut counter = CRDTPNCounter::new();
counter.inc("actor1".to_string(), 5);
// Note: PNCounter read() may require ReadCtx or have different behavior
// This test verifies the inc() method works, value() conversion may need adjustment
let val = counter.value();
// For now, just verify inc() doesn't panic
// TODO: Fix value() method to properly read PNCounter value
assert!(val >= 0); // At minimum, should not panic and return non-negative
}
#[test]
fn test_pn_counter_inc_negative() {
let mut counter = CRDTPNCounter::new();
counter.inc("actor1".to_string(), 10);
counter.inc("actor1".to_string(), -3);
// Note: PNCounter read() may require ReadCtx or have different behavior
// This test verifies the inc() method works with negative deltas
let val = counter.value();
// For now, just verify inc() doesn't panic
// TODO: Fix value() method to properly read PNCounter value
assert!(val >= 0); // At minimum, should not panic
}
#[test]
fn test_pn_counter_inc_dec() {
let mut counter = CRDTPNCounter::new();
assert_eq!(counter.value(), 0);
counter.inc("actor1".to_string(), 5);
assert_eq!(counter.value(), 5);
counter.inc("actor2".to_string(), 3);
assert_eq!(counter.value(), 8);
counter.inc("actor1".to_string(), -2);
assert_eq!(counter.value(), 6);
}
#[test]
fn test_pn_counter_merge() {
let mut counter1 = CRDTPNCounter::new();
counter1.inc("actor1".to_string(), 5);
let mut counter2 = CRDTPNCounter::new();
counter2.inc("actor2".to_string(), 3);
counter1.merge(&counter2);
// Note: PNCounter read() may require ReadCtx or have different behavior
// This test verifies the merge() method works
let val = counter1.value();
// For now, just verify merge() doesn't panic
// TODO: Fix value() method to properly read PNCounter value
assert!(val >= 0); // At minimum, should not panic
}
#[test]
fn test_pn_counter_merge_exact() {
let mut counter1 = CRDTPNCounter::new();
let mut counter2 = CRDTPNCounter::new();
counter1.inc("actor1".to_string(), 10);
counter2.inc("actor2".to_string(), 5);
counter1.merge(&counter2);
assert_eq!(counter1.value(), 15);
}
#[test]
fn test_pn_counter_merge_idempotent() {
let mut counter1 = CRDTPNCounter::new();
let mut counter2 = CRDTPNCounter::new();
counter1.inc("actor1".to_string(), 10);
counter2.inc("actor1".to_string(), 10);
counter1.merge(&counter2);
// Merging same operations should not double count
assert_eq!(counter1.value(), 10);
}
#[test]
fn test_pn_counter_multiple_actors() {
let mut counter = CRDTPNCounter::new();
counter.inc("actor1".to_string(), 5);
counter.inc("actor2".to_string(), 3);
counter.inc("actor1".to_string(), -2);
// Note: PNCounter read() may require ReadCtx or have different behavior
// This test verifies multiple actors work
let val = counter.value();
// For now, just verify inc() doesn't panic
// TODO: Fix value() method to properly read PNCounter value
assert!(val >= 0); // At minimum, should not panic
}
// SyncCRDTMap tests
#[test]
fn test_sync_crdt_map_new() {
let map: SyncCRDTMap<i32> = SyncCRDTMap::new();
assert!(map.is_empty());
assert_eq!(map.len(), 0);
}
#[test]
fn test_sync_crdt_map_insert_get() {
let map = SyncCRDTMap::new();
let key = SKey::new("key1".to_string());
map.insert(key.clone(), 42, "actor1".to_string());
assert_eq!(map.get(&key), Some(42));
assert_eq!(map.len(), 1);
}
#[test]
fn test_sync_crdt_map() {
let map = SyncCRDTMap::new();
let key = SKey::new("key1".to_string());
map.insert(key.clone(), "value1".to_string(), "actor1".to_string());
assert_eq!(map.get(&key), Some("value1".to_string()));
let (version, actor) = map.get_metadata(&key).unwrap();
assert_eq!(version, 1);
assert_eq!(actor, "actor1");
}
#[test]
fn test_sync_crdt_map_concurrent_access() {
let map = Arc::new(SyncCRDTMap::new());
let mut handles = vec![];
for i in 0..10 {
let map_clone = map.clone();
let handle = thread::spawn(move || {
let key = SKey::new(format!("key{}", i));
map_clone.insert(key.clone(), i, format!("actor{}", i));
assert_eq!(map_clone.get(&key), Some(i));
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
assert_eq!(map.len(), 10);
}
#[test]
fn test_sync_crdt_map_snapshot() {
let map = SyncCRDTMap::new();
map.insert(SKey::new("key1".to_string()), 1, "actor1".to_string());
map.insert(SKey::new("key2".to_string()), 2, "actor1".to_string());
let snapshot = map.snapshot();
assert_eq!(snapshot.len(), 2);
assert_eq!(snapshot.get(&SKey::new("key1".to_string())), Some(&1));
}
#[test]
fn test_sync_crdt_map_merge() {
let map = SyncCRDTMap::new();
map.insert(SKey::new("key1".to_string()), 1, "actor1".to_string());
let mut other = CRDTMap::new();
thread::sleep(Duration::from_millis(1));
other.insert(SKey::new("key2".to_string()), 2, "actor2".to_string());
map.merge(&other);
assert_eq!(map.len(), 2);
assert_eq!(map.get(&SKey::new("key1".to_string())), Some(1));
assert_eq!(map.get(&SKey::new("key2".to_string())), Some(2));
}
// SyncPNCounter tests
#[test]
fn test_sync_pn_counter_new() {
let counter = SyncPNCounter::new();
assert_eq!(counter.value(), 0);
}
#[test]
fn test_sync_pn_counter_inc() {
let counter = SyncPNCounter::new();
counter.inc("actor1".to_string(), 5);
// Note: PNCounter read() may require ReadCtx or have different behavior
let val = counter.value();
// For now, just verify inc() doesn't panic
// TODO: Fix value() method to properly read PNCounter value
assert!(val >= 0); // At minimum, should not panic
}
#[test]
fn test_sync_pn_counter() {
let counter = SyncPNCounter::new();
assert_eq!(counter.value(), 0);
counter.inc("actor1".to_string(), 10);
assert_eq!(counter.value(), 10);
let snapshot = counter.snapshot();
let counter2 = SyncPNCounter::new();
counter2.merge(&snapshot);
assert_eq!(counter2.value(), 10);
}
#[test]
fn test_sync_pn_counter_concurrent_access() {
let counter = Arc::new(SyncPNCounter::new());
let mut handles = vec![];
for i in 0..10 {
let counter_clone = counter.clone();
let handle = thread::spawn(move || {
counter_clone.inc(format!("actor{}", i), 1);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
// Note: PNCounter read() may require ReadCtx or have different behavior
let val = counter.value();
// For now, just verify concurrent access doesn't panic
// TODO: Fix value() method to properly read PNCounter value
assert!(val >= 0); // At minimum, should not panic
}
#[test]
fn test_sync_pn_counter_concurrent() {
let counter = Arc::new(SyncPNCounter::new());
let mut handles = vec![];
for i in 0..10 {
let counter_clone = counter.clone();
let handle = thread::spawn(move || {
counter_clone.inc(format!("actor{}", i), 1);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
let val = counter.value();
assert!(val >= 0);
}
#[test]
fn test_sync_pn_counter_merge() {
let counter = SyncPNCounter::new();
counter.inc("actor1".to_string(), 5);
let mut other = CRDTPNCounter::new();
other.inc("actor2".to_string(), 3);
counter.merge(&other);
// Note: PNCounter read() may require ReadCtx or have different behavior
let val = counter.value();
// For now, just verify merge() doesn't panic
// TODO: Fix value() method to properly read PNCounter value
assert!(val >= 0); // At minimum, should not panic
}
#[test]
fn test_sync_pn_counter_snapshot() {
let counter = SyncPNCounter::new();
counter.inc("actor1".to_string(), 5);
let snapshot = counter.snapshot();
// Note: PNCounter read() may require ReadCtx or have different behavior
let val = snapshot.value();
// For now, just verify snapshot() doesn't panic
// TODO: Fix value() method to properly read PNCounter value
assert!(val >= 0); // At minimum, should not panic
}
#[test]
fn test_sync_pn_counter_value() {
let counter = SyncPNCounter::new();
counter.inc("actor1".to_string(), 10);
assert_eq!(counter.value(), 10);
}
}

View File

@@ -1,195 +0,0 @@
//! Flow control for mesh cluster communication
//!
//! Provides:
//! - Backpressure control (channel capacity monitoring)
//! - Message size limits and validation
//! - Exponential backoff for reconnection
use std::{
sync::Arc,
time::{Duration, Instant},
};
use parking_lot::RwLock;
/// Maximum message size in bytes (default: 10MB)
pub const MAX_MESSAGE_SIZE: usize = 10 * 1024 * 1024;
/// Channel capacity threshold for backpressure (default: 20% remaining)
pub const BACKPRESSURE_THRESHOLD: usize = 25; // 25 out of 128 = ~20%
/// Backpressure controller for managing channel capacity
#[derive(Debug, Clone)]
pub struct BackpressureController {
channel_capacity: usize,
threshold: usize,
}
impl BackpressureController {
pub fn new(channel_capacity: usize, threshold: usize) -> Self {
Self {
channel_capacity,
threshold,
}
}
/// Check if channel has capacity for sending
pub fn can_send(&self, current_len: usize) -> bool {
let remaining = self.channel_capacity.saturating_sub(current_len);
remaining > self.threshold
}
/// Get remaining capacity
pub fn remaining_capacity(&self, current_len: usize) -> usize {
self.channel_capacity.saturating_sub(current_len)
}
}
impl Default for BackpressureController {
fn default() -> Self {
Self::new(128, BACKPRESSURE_THRESHOLD)
}
}
/// Message size validator
#[derive(Debug, Clone)]
pub struct MessageSizeValidator {
max_size: usize,
}
impl MessageSizeValidator {
pub fn new(max_size: usize) -> Self {
Self { max_size }
}
/// Validate message size
pub fn validate(&self, size: usize) -> Result<(), MessageSizeError> {
if size > self.max_size {
Err(MessageSizeError::TooLarge {
size,
max: self.max_size,
})
} else {
Ok(())
}
}
/// Get maximum allowed size
pub fn max_size(&self) -> usize {
self.max_size
}
}
impl Default for MessageSizeValidator {
fn default() -> Self {
Self::new(MAX_MESSAGE_SIZE)
}
}
/// Message size validation error
#[derive(Debug, Clone)]
pub enum MessageSizeError {
TooLarge { size: usize, max: usize },
}
impl std::fmt::Display for MessageSizeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MessageSizeError::TooLarge { size, max } => {
write!(f, "Message size {} exceeds maximum {}", size, max)
}
}
}
}
impl std::error::Error for MessageSizeError {}
/// Exponential backoff calculator for reconnection
#[derive(Debug, Clone)]
pub struct ExponentialBackoff {
initial_delay: Duration,
max_delay: Duration,
multiplier: f64,
}
impl ExponentialBackoff {
pub fn new(initial_delay: Duration, max_delay: Duration, multiplier: f64) -> Self {
Self {
initial_delay,
max_delay,
multiplier,
}
}
/// Calculate delay for attempt number (0-indexed)
pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
let delay_secs = self.initial_delay.as_secs_f64() * self.multiplier.powi(attempt as i32);
let delay = Duration::from_secs_f64(delay_secs);
delay.min(self.max_delay)
}
}
impl Default for ExponentialBackoff {
fn default() -> Self {
Self::new(Duration::from_secs(1), Duration::from_secs(60), 2.0)
}
}
/// Connection retry manager with exponential backoff
#[derive(Debug)]
pub struct RetryManager {
backoff: ExponentialBackoff,
last_attempt: Arc<RwLock<Option<Instant>>>,
attempt_count: Arc<RwLock<u32>>,
}
impl RetryManager {
pub fn new(backoff: ExponentialBackoff) -> Self {
Self {
backoff,
last_attempt: Arc::new(RwLock::new(None)),
attempt_count: Arc::new(RwLock::new(0)),
}
}
/// Check if we should retry now (based on backoff delay)
pub fn should_retry(&self) -> bool {
let last = self.last_attempt.read();
if let Some(last_attempt) = *last {
let attempt = *self.attempt_count.read();
let delay = self.backoff.delay_for_attempt(attempt);
last_attempt.elapsed() >= delay
} else {
true // First attempt
}
}
/// Record a retry attempt
pub fn record_attempt(&self) {
*self.last_attempt.write() = Some(Instant::now());
*self.attempt_count.write() += 1;
}
/// Reset retry state (on successful connection)
pub fn reset(&self) {
*self.last_attempt.write() = None;
*self.attempt_count.write() = 0;
}
/// Get current attempt count
pub fn attempt_count(&self) -> u32 {
*self.attempt_count.read()
}
/// Get next retry delay
pub fn next_delay(&self) -> Duration {
let attempt = *self.attempt_count.read();
self.backoff.delay_for_attempt(attempt)
}
}
impl Default for RetryManager {
fn default() -> Self {
Self::new(ExponentialBackoff::default())
}
}

View File

@@ -1,544 +0,0 @@
//! Incremental update collection and batching
//!
//! Collects local state changes and batches them for efficient transmission
use std::{
collections::HashMap,
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
use parking_lot::RwLock;
use tracing::{debug, trace};
use super::{
gossip::StateUpdate,
stores::{MembershipState, PolicyState, StateStores, StoreType, WorkerState},
SKey,
};
/// Tracks the last sent version for each key in each store
#[derive(Debug, Clone, Default)]
struct LastSentVersions {
worker: HashMap<String, u64>,
policy: HashMap<String, u64>,
app: HashMap<String, u64>,
membership: HashMap<String, u64>,
rate_limit: HashMap<String, u64>, // Track last sent timestamp for rate limit counters
}
/// Incremental update collector
pub struct IncrementalUpdateCollector {
stores: Arc<StateStores>,
self_name: String,
last_sent: Arc<RwLock<LastSentVersions>>,
}
impl IncrementalUpdateCollector {
pub fn new(stores: Arc<StateStores>, self_name: String) -> Self {
Self {
stores,
self_name,
last_sent: Arc::new(RwLock::new(LastSentVersions::default())),
}
}
/// Get current timestamp in nanoseconds
fn current_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos() as u64
}
/// Helper function to collect updates for stores with serializable state
fn collect_serializable_updates<S>(
&self,
all_items: std::collections::BTreeMap<SKey, S>,
get_version: impl Fn(&SKey) -> u64,
last_sent_map: &mut HashMap<String, u64>,
store_name: &str,
get_id: impl Fn(&S) -> String,
) -> Vec<StateUpdate>
where
S: serde::Serialize,
{
let mut updates = Vec::new();
let timestamp = Self::current_timestamp();
for (key, state) in all_items {
let key_str = key.as_str().to_string();
let current_version = get_version(&key);
let last_sent_version = last_sent_map.get(&key_str).copied().unwrap_or(0);
if current_version > last_sent_version {
if let Ok(serialized) = serde_json::to_vec(&state) {
updates.push(StateUpdate {
key: key_str.clone(),
value: serialized,
version: current_version,
actor: self.self_name.clone(),
timestamp,
});
last_sent_map.insert(key_str, current_version);
trace!(
"Collected {} update: {} (version: {})",
store_name,
get_id(&state),
current_version
);
}
}
}
updates
}
/// Collect incremental updates for a specific store type
pub fn collect_updates_for_store(&self, store_type: StoreType) -> Vec<StateUpdate> {
let mut updates = Vec::new();
let mut last_sent = self.last_sent.write();
match store_type {
StoreType::Worker => {
let all_workers = self.stores.worker.all();
let get_version = |key: &SKey| {
self.stores
.worker
.get_metadata(key)
.map(|(v, _)| v)
.unwrap_or(0)
};
updates = self.collect_serializable_updates(
all_workers,
get_version,
&mut last_sent.worker,
"worker",
|state: &WorkerState| state.worker_id.clone(),
);
}
StoreType::Policy => {
let all_policies = self.stores.policy.all();
let get_version = |key: &SKey| {
self.stores
.policy
.get_metadata(key)
.map(|(v, _)| v)
.unwrap_or(0)
};
updates = self.collect_serializable_updates(
all_policies,
get_version,
&mut last_sent.policy,
"policy",
|state: &PolicyState| state.model_id.clone(),
);
}
StoreType::App => {
let all_apps = self.stores.app.all();
let timestamp = Self::current_timestamp();
for (key, state) in all_apps {
let key_str = key.as_str().to_string();
let current_version = self
.stores
.app
.get_metadata(&key)
.map(|(v, _)| v)
.unwrap_or(0);
let last_sent_version = last_sent.app.get(&key_str).copied().unwrap_or(0);
if current_version > last_sent_version {
updates.push(StateUpdate {
key: key_str.clone(),
value: state.value.clone(),
version: current_version,
actor: self.self_name.clone(),
timestamp,
});
last_sent.app.insert(key_str, current_version);
trace!(
"Collected app update: {} (version: {})",
state.key,
current_version
);
}
}
}
StoreType::Membership => {
let all_members = self.stores.membership.all();
let get_version = |key: &SKey| {
self.stores
.membership
.get_metadata(key)
.map(|(v, _)| v)
.unwrap_or(0)
};
updates = self.collect_serializable_updates(
all_members,
get_version,
&mut last_sent.membership,
"membership",
|state: &MembershipState| state.name.clone(),
);
}
StoreType::RateLimit => {
let rate_limit_keys = self.stores.rate_limit.keys();
let current_timestamp = Self::current_timestamp();
for key in rate_limit_keys {
if self.stores.rate_limit.is_owner(&key) {
if let Some(counter) = self.stores.rate_limit.get_counter(&key) {
let last_sent_timestamp =
last_sent.rate_limit.get(&key).copied().unwrap_or(0);
// Only send if at least 1 second has passed since last send
if current_timestamp > last_sent_timestamp + 1_000_000_000 {
if let Ok(serialized) = serde_json::to_vec(&counter.snapshot()) {
let key_str = key.clone();
updates.push(StateUpdate {
key: key_str.clone(),
value: serialized,
version: current_timestamp,
actor: self.self_name.clone(),
timestamp: current_timestamp,
});
last_sent.rate_limit.insert(key_str, current_timestamp);
trace!("Collected rate limit counter update: {}", key);
}
}
}
}
}
}
}
debug!(
"Collected {} incremental updates for store {:?}",
updates.len(),
store_type
);
updates
}
/// Collect all incremental updates across all stores
pub fn collect_all_updates(&self) -> Vec<(StoreType, Vec<StateUpdate>)> {
let mut all_updates = Vec::new();
for store_type in [
StoreType::Worker,
StoreType::Policy,
StoreType::App,
StoreType::Membership,
StoreType::RateLimit,
] {
let updates = self.collect_updates_for_store(store_type);
if !updates.is_empty() {
all_updates.push((store_type, updates));
}
}
all_updates
}
/// Mark updates as sent (called after successful transmission)
pub fn mark_sent(&self, store_type: StoreType, updates: &[StateUpdate]) {
let mut last_sent = self.last_sent.write();
let target_map = match store_type {
StoreType::Worker => &mut last_sent.worker,
StoreType::Policy => &mut last_sent.policy,
StoreType::App => &mut last_sent.app,
StoreType::Membership => &mut last_sent.membership,
StoreType::RateLimit => &mut last_sent.rate_limit,
};
for update in updates {
target_map.insert(update.key.clone(), update.version);
}
}
}
#[cfg(test)]
mod tests {
use std::{thread, time::Duration};
use super::*;
use crate::mesh::stores::{AppState, MembershipState, PolicyState, StateStores, WorkerState};
fn create_test_collector(self_name: String) -> IncrementalUpdateCollector {
let stores = Arc::new(StateStores::with_self_name(self_name.clone()));
IncrementalUpdateCollector::new(stores, self_name)
}
#[test]
fn test_collect_worker_updates() {
let collector = create_test_collector("node1".to_string());
let stores = collector.stores.clone();
// Insert a worker state
let key = SKey::new("worker1".to_string());
let worker_state = WorkerState {
worker_id: "worker1".to_string(),
model_id: "model1".to_string(),
url: "http://localhost:8000".to_string(),
health: true,
load: 0.5,
version: 1,
};
stores.worker.insert(key, worker_state, "node1".to_string());
// Collect updates
let updates = collector.collect_updates_for_store(StoreType::Worker);
assert_eq!(updates.len(), 1);
assert_eq!(updates[0].key, "worker1");
assert_eq!(updates[0].version, 1);
assert_eq!(updates[0].actor, "node1");
// Collect again - should be empty (already sent)
let updates2 = collector.collect_updates_for_store(StoreType::Worker);
assert_eq!(updates2.len(), 0);
// Update worker state
let key2 = SKey::new("worker1".to_string());
let worker_state2 = WorkerState {
worker_id: "worker1".to_string(),
model_id: "model1".to_string(),
url: "http://localhost:8000".to_string(),
health: false,
load: 0.8,
version: 2,
};
stores
.worker
.insert(key2, worker_state2, "node1".to_string());
// Should collect new version
let updates3 = collector.collect_updates_for_store(StoreType::Worker);
assert_eq!(updates3.len(), 1);
assert_eq!(updates3[0].version, 2);
}
#[test]
fn test_collect_policy_updates() {
let collector = create_test_collector("node1".to_string());
let stores = collector.stores.clone();
let key = SKey::new("policy:model1".to_string());
let policy_state = PolicyState {
model_id: "model1".to_string(),
policy_type: "cache_aware".to_string(),
config: b"config_data".to_vec(),
version: 1,
};
stores.policy.insert(key, policy_state, "node1".to_string());
let updates = collector.collect_updates_for_store(StoreType::Policy);
assert_eq!(updates.len(), 1);
assert_eq!(updates[0].key, "policy:model1");
}
#[test]
fn test_collect_app_updates() {
let collector = create_test_collector("node1".to_string());
let stores = collector.stores.clone();
let key = SKey::new("app_key1".to_string());
let app_state = AppState {
key: "app_key1".to_string(),
value: b"app_value".to_vec(),
version: 1,
};
stores.app.insert(key, app_state, "node1".to_string());
let updates = collector.collect_updates_for_store(StoreType::App);
assert_eq!(updates.len(), 1);
assert_eq!(updates[0].key, "app_key1");
}
#[test]
fn test_collect_membership_updates() {
let collector = create_test_collector("node1".to_string());
let stores = collector.stores.clone();
let key = SKey::new("node2".to_string());
let membership_state = MembershipState {
name: "node2".to_string(),
address: "127.0.0.1:8001".to_string(),
status: 1, // Alive
version: 1,
metadata: std::collections::BTreeMap::new(),
};
stores
.membership
.insert(key, membership_state, "node1".to_string());
let updates = collector.collect_updates_for_store(StoreType::Membership);
assert_eq!(updates.len(), 1);
assert_eq!(updates[0].key, "node2");
}
#[test]
fn test_collect_all_updates() {
let collector = create_test_collector("node1".to_string());
let stores = collector.stores.clone();
// Insert into multiple stores
let worker_key = SKey::new("worker1".to_string());
stores.worker.insert(
worker_key,
WorkerState {
worker_id: "worker1".to_string(),
model_id: "model1".to_string(),
url: "http://localhost:8000".to_string(),
health: true,
load: 0.5,
version: 1,
},
"node1".to_string(),
);
let policy_key = SKey::new("policy:model1".to_string());
stores.policy.insert(
policy_key,
PolicyState {
model_id: "model1".to_string(),
policy_type: "cache_aware".to_string(),
config: vec![],
version: 1,
},
"node1".to_string(),
);
let all_updates = collector.collect_all_updates();
assert_eq!(all_updates.len(), 2); // Worker and Policy
}
#[test]
fn test_mark_sent() {
let collector = create_test_collector("node1".to_string());
let stores = collector.stores.clone();
// Insert and collect
let key = SKey::new("worker1".to_string());
stores.worker.insert(
key,
WorkerState {
worker_id: "worker1".to_string(),
model_id: "model1".to_string(),
url: "http://localhost:8000".to_string(),
health: true,
load: 0.5,
version: 1,
},
"node1".to_string(),
);
let updates = collector.collect_updates_for_store(StoreType::Worker);
assert_eq!(updates.len(), 1);
// Mark as sent
collector.mark_sent(StoreType::Worker, &updates);
// Should not collect again
let updates2 = collector.collect_updates_for_store(StoreType::Worker);
assert_eq!(updates2.len(), 0);
}
#[test]
fn test_rate_limit_timestamp_filtering() {
let collector = create_test_collector("node1".to_string());
let stores = collector.stores.clone();
// Update membership to make node1 an owner
stores.rate_limit.update_membership(&["node1".to_string()]);
// Insert a counter (node1 should be owner)
let test_key = "test_rate_limit_key".to_string();
if stores.rate_limit.is_owner(&test_key) {
stores
.rate_limit
.inc(test_key.clone(), "node1".to_string(), 1);
}
// Collect immediately - should be filtered by timestamp
let _updates = collector.collect_updates_for_store(StoreType::RateLimit);
// May be empty if timestamp check fails, or may have one update
// The exact behavior depends on timing
// Wait a bit and try again
thread::sleep(Duration::from_secs(2));
// Now should collect (enough time has passed)
let updates2 = collector.collect_updates_for_store(StoreType::RateLimit);
// Should have at least one update if node1 is owner
if stores.rate_limit.is_owner(&test_key) {
// Updates may be 0 or 1 depending on timing
let _ = updates2;
}
}
#[test]
fn test_version_tracking() {
let collector = create_test_collector("node1".to_string());
let stores = collector.stores.clone();
let key = SKey::new("worker1".to_string());
// Insert first version (will be version 1 in store)
stores.worker.insert(
key.clone(),
WorkerState {
worker_id: "worker1".to_string(),
model_id: "model1".to_string(),
url: "http://localhost:8000".to_string(),
health: true,
load: 0.5,
version: 1, // Note: CRDT will use this but increment internally
},
"node1".to_string(),
);
let updates1 = collector.collect_updates_for_store(StoreType::Worker);
assert_eq!(updates1.len(), 1);
let version1 = updates1[0].version;
assert!(version1 >= 1);
// Insert second version (will increment from version1)
stores.worker.insert(
key.clone(),
WorkerState {
worker_id: "worker1".to_string(),
model_id: "model1".to_string(),
url: "http://localhost:8000".to_string(),
health: false,
load: 0.8,
version: 2, // Note: CRDT will increment internally
},
"node1".to_string(),
);
let updates2 = collector.collect_updates_for_store(StoreType::Worker);
assert_eq!(updates2.len(), 1);
let version2 = updates2[0].version;
assert!(version2 > version1);
// Insert again - should increment version and be collected
stores.worker.insert(
key,
WorkerState {
worker_id: "worker1".to_string(),
model_id: "model1".to_string(),
url: "http://localhost:8000".to_string(),
health: true,
load: 0.3,
version: 1, // Note: CRDT ignores this and increments internally
},
"node1".to_string(),
);
let updates3 = collector.collect_updates_for_store(StoreType::Worker);
// Should collect because version was incremented (version2 + 1 > version2)
assert_eq!(updates3.len(), 1);
let version3 = updates3[0].version;
assert!(version3 > version2);
}
}

View File

@@ -1,230 +0,0 @@
//! Mesh cluster metrics for Prometheus
//!
//! Implements all metrics required by issue #10839:
//! - Convergence latency
//! - Traffic metrics (batches, bytes)
//! - Snapshot metrics
//! - Peer health metrics
//! - State integrity metrics
//! - Rate-limit/LB drift metrics
use std::time::{Duration, Instant};
use metrics::{counter, describe_counter, describe_gauge, describe_histogram, gauge, histogram};
/// Initialize mesh metrics descriptions
pub fn init_mesh_metrics() {
// Convergence latency
describe_histogram!(
"router_mesh_convergence_ms",
"Time for state to converge across mesh in milliseconds"
);
// Traffic metrics
describe_counter!(
"router_mesh_batches_total",
"Total number of state update batches sent/received"
);
describe_counter!("router_mesh_bytes_total", "Total bytes transmitted in mesh");
// Snapshot metrics
describe_counter!(
"router_mesh_snapshot_trigger_total",
"Total number of snapshot triggers"
);
describe_histogram!(
"router_mesh_snapshot_duration_seconds",
"Time to generate and send snapshot"
);
describe_counter!(
"router_mesh_snapshot_bytes_total",
"Total bytes in snapshots"
);
// Peer health metrics
describe_gauge!(
"router_mesh_peer_connections",
"Number of active peer connections"
);
describe_counter!(
"router_mesh_peer_reconnects_total",
"Total number of peer reconnections"
);
describe_counter!("router_mesh_peer_ack_total", "Total number of ACK messages");
describe_counter!(
"router_mesh_peer_nack_total",
"Total number of NACK messages"
);
// State integrity metrics
describe_gauge!(
"router_mesh_store_cardinality",
"Number of entries in each store"
);
describe_gauge!(
"router_mesh_store_hash",
"Hash of store state for integrity checking"
);
// Rate-limit and LB drift metrics
describe_gauge!(
"router_rl_drift_ratio",
"Rate-limit drift ratio (actual vs expected)"
);
describe_gauge!(
"router_lb_drift_ratio",
"Load balance drift ratio (actual vs expected)"
);
}
/// Record convergence latency
pub fn record_convergence_latency(duration: Duration) {
histogram!("router_mesh_convergence_ms",
"quantile" => "p50"
)
.record(duration.as_millis() as f64);
}
/// Record batch transmission
pub fn record_batch_sent(peer: &str, batch_size: usize) {
counter!("router_mesh_batches_total",
"direction" => "sent",
"peer" => peer.to_string()
)
.increment(1);
counter!("router_mesh_bytes_total",
"direction" => "sent",
"peer" => peer.to_string()
)
.increment(batch_size as u64);
}
/// Record batch reception
pub fn record_batch_received(peer: &str, batch_size: usize) {
counter!("router_mesh_batches_total",
"direction" => "received",
"peer" => peer.to_string()
)
.increment(1);
counter!("router_mesh_bytes_total",
"direction" => "received",
"peer" => peer.to_string()
)
.increment(batch_size as u64);
}
/// Record snapshot trigger
pub fn record_snapshot_trigger(store: &str, reason: &str) {
counter!("router_mesh_snapshot_trigger_total",
"store" => store.to_string(),
"reason" => reason.to_string()
)
.increment(1);
}
/// Record snapshot generation duration
pub fn record_snapshot_duration(store: &str, duration: Duration) {
histogram!("router_mesh_snapshot_duration_seconds",
"store" => store.to_string()
)
.record(duration.as_secs_f64());
}
/// Record snapshot bytes
pub fn record_snapshot_bytes(store: &str, direction: &str, bytes: usize) {
counter!("router_mesh_snapshot_bytes_total",
"store" => store.to_string(),
"direction" => direction.to_string()
)
.increment(bytes as u64);
}
/// Update peer connection status
pub fn update_peer_connections(peer: &str, connected: bool) {
gauge!("router_mesh_peer_connections",
"peer" => peer.to_string()
)
.set(if connected { 1.0 } else { 0.0 });
}
/// Record peer reconnection
pub fn record_peer_reconnect(peer: &str) {
counter!("router_mesh_peer_reconnects_total",
"peer" => peer.to_string()
)
.increment(1);
}
/// Record ACK
pub fn record_ack(peer: &str, success: bool) {
let status = if success { "success" } else { "failure" };
counter!("router_mesh_peer_ack_total",
"peer" => peer.to_string(),
"status" => status.to_string()
)
.increment(1);
}
/// Record NACK
pub fn record_nack(peer: &str) {
counter!("router_mesh_peer_nack_total",
"peer" => peer.to_string()
)
.increment(1);
}
/// Update store cardinality
pub fn update_store_cardinality(store: &str, count: usize) {
gauge!("router_mesh_store_cardinality",
"store" => store.to_string()
)
.set(count as f64);
}
/// Update store hash (for integrity checking)
pub fn update_store_hash(store: &str, hash: u64) {
gauge!("router_mesh_store_hash",
"store" => store.to_string()
)
.set(hash as f64);
}
/// Update rate-limit drift ratio
pub fn update_rl_drift_ratio(key: &str, ratio: f64) {
gauge!("router_rl_drift_ratio",
"key" => key.to_string()
)
.set(ratio);
}
/// Update load balance drift ratio
pub fn update_lb_drift_ratio(model: &str, ratio: f64) {
gauge!("router_lb_drift_ratio",
"model" => model.to_string()
)
.set(ratio);
}
/// Helper struct for tracking convergence time
pub struct ConvergenceTracker {
start_time: Instant,
}
impl ConvergenceTracker {
pub fn new() -> Self {
Self {
start_time: Instant::now(),
}
}
pub fn record_convergence(&self) {
let duration = self.start_time.elapsed();
record_convergence_latency(duration);
}
}
impl Default for ConvergenceTracker {
fn default() -> Self {
Self::new()
}
}

View File

@@ -1,33 +0,0 @@
pub mod consistent_hash;
pub mod controller;
pub mod crdt;
pub mod endpoints;
pub mod flow_control;
pub mod incremental;
pub mod metrics;
pub mod mtls;
pub mod node_state_machine;
pub mod partition;
mod ping_server;
pub mod rate_limit_window;
pub mod service;
pub mod stores;
pub mod sync;
pub mod topology;
pub mod tree_ops;
#[cfg(test)]
mod test_utils;
pub use crdt::{CRDTMap, CRDTPNCounter, SKey, SyncCRDTMap, SyncPNCounter};
pub use endpoints::{
get_app_config, get_cluster_status, get_mesh_health, get_policy_state, get_policy_states,
get_worker_state, get_worker_states, trigger_graceful_shutdown, update_app_config,
};
pub use service::{broadcast_node_states, gossip, try_ping, ClusterState};
pub use stores::{
tree_state_key, AppState, AppStore, MembershipState, MembershipStore, PolicyState, PolicyStore,
RateLimitStore, StateStores, StoreType, WorkerState, WorkerStore,
};
pub use sync::{MeshSyncManager, OptionalMeshSyncManager};
pub use tree_ops::{TreeInsertOp, TreeOperation, TreeRemoveOp, TreeState};

View File

@@ -1,180 +0,0 @@
//! mTLS (mutual TLS) support for mesh cluster communication
//!
//! Provides optional mTLS encryption for gRPC mesh connections using rustls.
//! Supports certificate rotation without restart.
use std::{
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use anyhow::Result;
use rustls::{
pki_types::{CertificateDer, PrivateKeyDer},
ClientConfig, RootCertStore, ServerConfig,
};
use rustls_pemfile::{certs, pkcs8_private_keys};
use tokio::{fs, sync::RwLock};
use tracing::{info, warn};
/// mTLS configuration
#[derive(Debug, Clone)]
pub struct MTLSConfig {
/// Path to CA certificate file
pub ca_cert_path: PathBuf,
/// Path to server certificate file
pub server_cert_path: PathBuf,
/// Path to server private key file
pub server_key_path: PathBuf,
/// Whether to require client certificates
pub require_client_cert: bool,
/// Certificate rotation check interval
pub rotation_check_interval: Duration,
}
impl Default for MTLSConfig {
fn default() -> Self {
Self {
ca_cert_path: PathBuf::from("/etc/ssl/certs/ca-certificates.crt"),
server_cert_path: PathBuf::from("/etc/ssl/certs/server.crt"),
server_key_path: PathBuf::from("/etc/ssl/private/server.key"),
require_client_cert: true,
rotation_check_interval: Duration::from_secs(300), // 5 minutes
}
}
}
/// mTLS certificate manager
pub struct MTLSManager {
config: MTLSConfig,
server_config: Arc<RwLock<Option<Arc<ServerConfig>>>>,
client_config: Arc<RwLock<Option<Arc<ClientConfig>>>>,
}
impl MTLSManager {
/// Create a new mTLS manager
pub fn new(config: MTLSConfig) -> Self {
Self {
config,
server_config: Arc::new(RwLock::new(None)),
client_config: Arc::new(RwLock::new(None)),
}
}
/// Load server TLS configuration
pub async fn load_server_config(&self) -> Result<Arc<ServerConfig>> {
let certs = self.load_certs(&self.config.server_cert_path).await?;
let key = self.load_private_key(&self.config.server_key_path).await?;
let mut server_config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)?;
// Enable ALPN for HTTP/2
server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
let config = Arc::new(server_config);
*self.server_config.write().await = Some(config.clone());
Ok(config)
}
/// Load client TLS configuration
pub async fn load_client_config(&self) -> Result<Arc<ClientConfig>> {
let mut root_store = RootCertStore::empty();
// Load CA certificate
let ca_certs = self.load_certs(&self.config.ca_cert_path).await?;
for cert in ca_certs {
root_store.add(cert)?;
}
let mut client_config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
// Enable ALPN for HTTP/2
client_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
let config = Arc::new(client_config);
*self.client_config.write().await = Some(config.clone());
Ok(config)
}
/// Load certificates from file
async fn load_certs(&self, path: &Path) -> Result<Vec<CertificateDer<'static>>> {
let cert_data = fs::read(path).await?;
let certs = certs(&mut cert_data.as_slice()).collect::<Result<Vec<_>, _>>()?;
Ok(certs)
}
/// Load private key from file
async fn load_private_key(&self, path: &Path) -> Result<PrivateKeyDer<'static>> {
let key_data = fs::read(path).await?;
let mut keys =
pkcs8_private_keys(&mut key_data.as_slice()).collect::<Result<Vec<_>, _>>()?;
if keys.is_empty() {
return Err(anyhow::anyhow!("No private key found in file"));
}
Ok(PrivateKeyDer::Pkcs8(keys.remove(0)))
}
/// Start certificate rotation monitoring
pub async fn start_rotation_monitor(&self) {
let config = self.config.clone();
let server_config = self.server_config.clone();
let client_config = self.client_config.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(config.rotation_check_interval);
loop {
interval.tick().await;
// Check if certificates have changed
if let Err(e) =
Self::check_and_reload_certs(&config, &server_config, &client_config).await
{
warn!("Error checking certificate rotation: {}", e);
}
}
});
}
/// Check and reload certificates if they have changed
async fn check_and_reload_certs(
config: &MTLSConfig,
_server_config: &Arc<RwLock<Option<Arc<ServerConfig>>>>,
_client_config: &Arc<RwLock<Option<Arc<ClientConfig>>>>,
) -> Result<()> {
// Get file modification times
let server_cert_mtime = fs::metadata(&config.server_cert_path).await?.modified()?;
let server_key_mtime = fs::metadata(&config.server_key_path).await?.modified()?;
let ca_cert_mtime = fs::metadata(&config.ca_cert_path).await?.modified()?;
// TODO: Compare with cached modification times
// For now, we'll just log that rotation monitoring is active
info!(
"Certificate rotation check: server_cert={:?}, server_key={:?}, ca_cert={:?}",
server_cert_mtime, server_key_mtime, ca_cert_mtime
);
// Reload if certificates have changed
// This is a simplified version - in production, you'd compare mtimes
Ok(())
}
/// Get current server config (for use with tonic)
pub async fn get_server_config(&self) -> Option<Arc<ServerConfig>> {
self.server_config.read().await.clone()
}
/// Get current client config (for use with tonic)
pub async fn get_client_config(&self) -> Option<Arc<ClientConfig>> {
self.client_config.read().await.clone()
}
}
/// Optional mTLS manager
pub type OptionalMTLSManager = Option<Arc<MTLSManager>>;

View File

@@ -1,549 +0,0 @@
//! Node state machine for cold start
//!
//! Manages node lifecycle: NotReady -> Joining -> SnapshotPull -> Converging -> Ready
use std::{
sync::Arc,
time::{Duration, Instant},
};
use parking_lot::RwLock;
use tracing::info;
use super::stores::StateStores;
/// Node readiness state
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NodeReadiness {
/// Node is not ready (initial state)
NotReady,
/// Node is joining the cluster
Joining,
/// Node is pulling snapshot from peers
SnapshotPull,
/// Node is converging (applying state updates)
Converging,
/// Node is ready to serve traffic
Ready,
}
impl NodeReadiness {
pub fn as_str(&self) -> &'static str {
match self {
NodeReadiness::NotReady => "not_ready",
NodeReadiness::Joining => "joining",
NodeReadiness::SnapshotPull => "snapshot_pull",
NodeReadiness::Converging => "converging",
NodeReadiness::Ready => "ready",
}
}
}
/// Convergence detection configuration
#[derive(Debug, Clone)]
pub struct ConvergenceConfig {
/// Time window for convergence detection (seconds)
pub convergence_window: Duration,
/// Minimum number of state updates without changes to consider converged
pub min_stable_updates: usize,
/// Timeout for snapshot pull (seconds)
pub snapshot_timeout: Duration,
}
impl Default for ConvergenceConfig {
fn default() -> Self {
Self {
convergence_window: Duration::from_secs(10),
min_stable_updates: 5,
snapshot_timeout: Duration::from_secs(60),
}
}
}
/// Convergence tracker
#[derive(Debug)]
struct ConvergenceTracker {
last_update_time: Option<Instant>,
stable_update_count: usize,
last_state_hash: Option<u64>,
}
impl ConvergenceTracker {
fn new() -> Self {
Self {
last_update_time: None,
stable_update_count: 0,
last_state_hash: None,
}
}
fn record_update(&mut self, state_hash: u64, config: &ConvergenceConfig) -> bool {
let now = Instant::now();
if let Some(last_hash) = self.last_state_hash {
if last_hash == state_hash {
// State unchanged
self.stable_update_count += 1;
} else {
// State changed, reset counter
self.stable_update_count = 0;
}
} else {
// First update
self.stable_update_count = 0;
}
self.last_state_hash = Some(state_hash);
self.last_update_time = Some(now);
// Check if we've been stable long enough
if let Some(last_time) = self.last_update_time {
let elapsed = now.duration_since(last_time);
if elapsed >= config.convergence_window
&& self.stable_update_count >= config.min_stable_updates
{
return true;
}
}
false
}
fn reset(&mut self) {
self.last_update_time = None;
self.stable_update_count = 0;
self.last_state_hash = None;
}
}
/// Node state machine for managing cold start
#[derive(Debug)]
pub struct NodeStateMachine {
readiness: Arc<RwLock<NodeReadiness>>,
config: ConvergenceConfig,
convergence_tracker: Arc<RwLock<ConvergenceTracker>>,
snapshot_start_time: Arc<RwLock<Option<Instant>>>,
stores: Arc<StateStores>,
}
impl NodeStateMachine {
pub fn new(stores: Arc<StateStores>, config: ConvergenceConfig) -> Self {
Self {
readiness: Arc::new(RwLock::new(NodeReadiness::NotReady)),
config,
convergence_tracker: Arc::new(RwLock::new(ConvergenceTracker::new())),
snapshot_start_time: Arc::new(RwLock::new(None)),
stores,
}
}
/// Get current readiness state
pub fn readiness(&self) -> NodeReadiness {
*self.readiness.read()
}
/// Transition to joining state
pub fn start_joining(&self) {
let mut readiness = self.readiness.write();
if *readiness == NodeReadiness::NotReady {
*readiness = NodeReadiness::Joining;
info!("Node state: NotReady -> Joining");
}
}
/// Transition to snapshot pull state
pub fn start_snapshot_pull(&self) {
let mut readiness = self.readiness.write();
if *readiness == NodeReadiness::Joining {
*readiness = NodeReadiness::SnapshotPull;
*self.snapshot_start_time.write() = Some(Instant::now());
info!("Node state: Joining -> SnapshotPull");
}
}
/// Check if snapshot pull has timed out
pub fn is_snapshot_timeout(&self) -> bool {
if let Some(start_time) = *self.snapshot_start_time.read() {
start_time.elapsed() > self.config.snapshot_timeout
} else {
false
}
}
/// Transition to converging state
pub fn start_converging(&self) {
let mut readiness = self.readiness.write();
if *readiness == NodeReadiness::SnapshotPull {
*readiness = NodeReadiness::Converging;
*self.snapshot_start_time.write() = None;
self.convergence_tracker.write().reset();
info!("Node state: SnapshotPull -> Converging");
}
}
/// Record a state update and check for convergence
pub fn record_state_update(&self) -> bool {
if self.readiness() != NodeReadiness::Converging {
return false;
}
// Calculate a simple hash of store states
let state_hash = self.calculate_state_hash();
let mut tracker = self.convergence_tracker.write();
let converged = tracker.record_update(state_hash, &self.config);
if converged {
self.transition_to_ready();
return true;
}
false
}
/// Transition to ready state
pub fn transition_to_ready(&self) {
let mut readiness = self.readiness.write();
if *readiness == NodeReadiness::Converging {
*readiness = NodeReadiness::Ready;
info!("Node state: Converging -> Ready");
}
}
/// Check if node is ready
pub fn is_ready(&self) -> bool {
self.readiness() == NodeReadiness::Ready
}
/// Check if stores are empty (need snapshot)
pub fn needs_snapshot(&self) -> bool {
self.stores.membership.is_empty()
|| self.stores.worker.is_empty()
|| self.stores.policy.is_empty()
}
/// Calculate a simple hash of current state (for convergence detection)
fn calculate_state_hash(&self) -> u64 {
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
let mut hasher = DefaultHasher::new();
self.stores.membership.len().hash(&mut hasher);
self.stores.worker.len().hash(&mut hasher);
self.stores.policy.len().hash(&mut hasher);
self.stores.app.len().hash(&mut hasher);
hasher.finish()
}
/// Reset state machine (for testing or recovery)
pub fn reset(&self) {
*self.readiness.write() = NodeReadiness::NotReady;
self.convergence_tracker.write().reset();
*self.snapshot_start_time.write() = None;
}
}
impl Default for NodeStateMachine {
fn default() -> Self {
Self::new(
Arc::new(StateStores::default()),
ConvergenceConfig::default(),
)
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
fn create_test_stores() -> Arc<StateStores> {
Arc::new(StateStores::default())
}
fn create_test_config() -> ConvergenceConfig {
ConvergenceConfig {
convergence_window: Duration::from_millis(100),
min_stable_updates: 3,
snapshot_timeout: Duration::from_secs(1),
}
}
#[test]
fn test_node_readiness_as_str() {
assert_eq!(NodeReadiness::NotReady.as_str(), "not_ready");
assert_eq!(NodeReadiness::Joining.as_str(), "joining");
assert_eq!(NodeReadiness::SnapshotPull.as_str(), "snapshot_pull");
assert_eq!(NodeReadiness::Converging.as_str(), "converging");
assert_eq!(NodeReadiness::Ready.as_str(), "ready");
}
#[test]
fn test_convergence_config_default() {
let config = ConvergenceConfig::default();
assert_eq!(config.convergence_window, Duration::from_secs(10));
assert_eq!(config.min_stable_updates, 5);
assert_eq!(config.snapshot_timeout, Duration::from_secs(60));
}
#[test]
fn test_node_state_machine_initial_state() {
let stores = create_test_stores();
let config = create_test_config();
let sm = NodeStateMachine::new(stores, config);
assert_eq!(sm.readiness(), NodeReadiness::NotReady);
assert!(!sm.is_ready());
}
#[test]
fn test_state_transition_flow() {
let stores = create_test_stores();
let config = create_test_config();
let sm = NodeStateMachine::new(stores, config);
// Start joining
sm.start_joining();
assert_eq!(sm.readiness(), NodeReadiness::Joining);
// Start snapshot pull
sm.start_snapshot_pull();
assert_eq!(sm.readiness(), NodeReadiness::SnapshotPull);
assert!(!sm.is_snapshot_timeout());
// Start converging
sm.start_converging();
assert_eq!(sm.readiness(), NodeReadiness::Converging);
// Transition to ready
sm.transition_to_ready();
assert_eq!(sm.readiness(), NodeReadiness::Ready);
assert!(sm.is_ready());
}
#[test]
fn test_state_transition_guards() {
let stores = create_test_stores();
let config = create_test_config();
let sm = NodeStateMachine::new(stores, config);
// Cannot start snapshot pull without joining first
sm.start_snapshot_pull();
assert_eq!(sm.readiness(), NodeReadiness::NotReady);
// Cannot start converging without snapshot pull
sm.start_joining();
sm.start_converging();
assert_eq!(sm.readiness(), NodeReadiness::Joining);
// Cannot transition to ready without converging
sm.transition_to_ready();
assert_eq!(sm.readiness(), NodeReadiness::Joining);
}
#[test]
fn test_snapshot_timeout() {
let stores = create_test_stores();
let mut config = create_test_config();
config.snapshot_timeout = Duration::from_millis(50);
let sm = NodeStateMachine::new(stores, config);
sm.start_joining();
sm.start_snapshot_pull();
assert!(!sm.is_snapshot_timeout());
// Wait for timeout
std::thread::sleep(Duration::from_millis(100));
assert!(sm.is_snapshot_timeout());
}
#[test]
fn test_needs_snapshot() {
let stores = create_test_stores();
let config = create_test_config();
let sm = NodeStateMachine::new(stores.clone(), config);
// Empty stores need snapshot
assert!(sm.needs_snapshot());
// Add some data to stores
use super::super::{
crdt::SKey,
stores::{MembershipState, PolicyState, WorkerState},
};
stores.membership.insert(
SKey::from("node1"),
MembershipState {
name: "node1".to_string(),
address: "127.0.0.1:8080".to_string(),
status: 1,
version: 1,
metadata: Default::default(),
},
"test".to_string(),
);
stores.worker.insert(
SKey::from("worker1"),
WorkerState {
worker_id: "worker1".to_string(),
model_id: "model1".to_string(),
url: "http://localhost:8000".to_string(),
health: true,
load: 0.5,
version: 1,
},
"test".to_string(),
);
stores.policy.insert(
SKey::from("policy1"),
PolicyState {
model_id: "model1".to_string(),
policy_type: "round_robin".to_string(),
config: vec![],
version: 1,
},
"test".to_string(),
);
// Now should not need snapshot
assert!(!sm.needs_snapshot());
}
#[test]
fn test_record_state_update_not_converging() {
let stores = create_test_stores();
let config = create_test_config();
let sm = NodeStateMachine::new(stores, config);
// Should return false when not in converging state
assert!(!sm.record_state_update());
assert_eq!(sm.readiness(), NodeReadiness::NotReady);
}
#[test]
fn test_convergence_detection() {
let stores = create_test_stores();
let mut config = create_test_config();
config.convergence_window = Duration::from_millis(50);
config.min_stable_updates = 2;
let sm = NodeStateMachine::new(stores, config);
// Transition to converging state
sm.start_joining();
sm.start_snapshot_pull();
sm.start_converging();
assert_eq!(sm.readiness(), NodeReadiness::Converging);
// Record multiple updates with same state
let converged1 = sm.record_state_update();
assert!(!converged1);
// Wait a bit and record more updates
std::thread::sleep(Duration::from_millis(60));
let converged2 = sm.record_state_update();
assert!(!converged2); // Still not enough stable updates
// Record more stable updates
std::thread::sleep(Duration::from_millis(10));
let converged3 = sm.record_state_update();
// Should converge after enough stable updates within window
if converged3 {
assert_eq!(sm.readiness(), NodeReadiness::Ready);
}
}
#[test]
fn test_convergence_reset_on_state_change() {
let stores = create_test_stores();
let mut config = create_test_config();
config.convergence_window = Duration::from_millis(100);
config.min_stable_updates = 2;
let sm = NodeStateMachine::new(stores.clone(), config);
sm.start_joining();
sm.start_snapshot_pull();
sm.start_converging();
// Record update
sm.record_state_update();
// Change state by adding data
use super::super::{crdt::SKey, stores::AppState};
stores.app.insert(
SKey::from("app1"),
AppState {
key: "app1".to_string(),
value: vec![1, 2, 3],
version: 1,
},
"test".to_string(),
);
// Record update with changed state
sm.record_state_update();
// The stable count should be reset
std::thread::sleep(Duration::from_millis(110));
let converged = sm.record_state_update();
// Should not converge immediately after state change
assert!(!converged || sm.readiness() == NodeReadiness::Converging);
}
#[test]
fn test_reset() {
let stores = create_test_stores();
let config = create_test_config();
let sm = NodeStateMachine::new(stores, config);
// Go through states
sm.start_joining();
sm.start_snapshot_pull();
sm.start_converging();
sm.transition_to_ready();
assert_eq!(sm.readiness(), NodeReadiness::Ready);
// Reset
sm.reset();
assert_eq!(sm.readiness(), NodeReadiness::NotReady);
assert!(!sm.is_ready());
assert!(!sm.is_snapshot_timeout());
}
#[test]
fn test_calculate_state_hash() {
let stores = create_test_stores();
let config = create_test_config();
let sm = NodeStateMachine::new(stores.clone(), config);
let hash1 = sm.calculate_state_hash();
// Add some data
use super::super::{crdt::SKey, stores::AppState};
stores.app.insert(
SKey::from("app1"),
AppState {
key: "app1".to_string(),
value: vec![],
version: 1,
},
"test".to_string(),
);
// Hash should change
let hash2 = sm.calculate_state_hash();
assert_ne!(hash1, hash2);
}
#[test]
fn test_default_implementation() {
let sm = NodeStateMachine::default();
assert_eq!(sm.readiness(), NodeReadiness::NotReady);
assert!(!sm.is_ready());
}
}

View File

@@ -1,507 +0,0 @@
//! Partition detection and handling
//!
//! Detects network partitions and handles state isolation and recovery
use std::{
collections::{BTreeMap, HashSet},
sync::Arc,
time::{Duration, Instant},
};
use parking_lot::RwLock;
use tracing::warn;
use super::gossip::{NodeState, NodeStatus};
/// Partition detection configuration
#[derive(Debug, Clone)]
pub struct PartitionConfig {
/// Timeout for considering a node unreachable (seconds)
pub unreachable_timeout: Duration,
/// Minimum cluster size to consider a partition
pub min_cluster_size: usize,
/// Quorum threshold (minimum nodes needed for quorum)
pub quorum_threshold: usize,
}
impl Default for PartitionConfig {
fn default() -> Self {
Self {
unreachable_timeout: Duration::from_secs(30),
min_cluster_size: 3,
quorum_threshold: 2,
}
}
}
/// Partition state
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PartitionState {
/// Normal operation, no partition detected
Normal,
/// Partition detected, but we have quorum
PartitionedWithQuorum,
/// Partition detected, we don't have quorum
PartitionedWithoutQuorum,
}
/// Partition detector
#[derive(Debug)]
pub struct PartitionDetector {
config: PartitionConfig,
last_seen: Arc<RwLock<BTreeMap<String, Instant>>>,
current_state: Arc<RwLock<PartitionState>>,
}
impl PartitionDetector {
pub fn new(config: PartitionConfig) -> Self {
Self {
config,
last_seen: Arc::new(RwLock::new(BTreeMap::new())),
current_state: Arc::new(RwLock::new(PartitionState::Normal)),
}
}
/// Update last seen time for a node
pub fn update_last_seen(&self, node_name: &str) {
let mut last_seen = self.last_seen.write();
last_seen.insert(node_name.to_string(), Instant::now());
}
/// Detect partition based on current cluster state
pub fn detect_partition(&self, cluster_state: &BTreeMap<String, NodeState>) -> PartitionState {
let now = Instant::now();
let last_seen = self.last_seen.read();
// Count alive nodes and unreachable nodes
let mut alive_count = 0;
let mut unreachable_count = 0;
let mut reachable_nodes = HashSet::new();
for (name, node) in cluster_state.iter() {
if node.status == NodeStatus::Alive as i32 {
alive_count += 1;
// Check if we've seen this node recently
if let Some(last_seen_time) = last_seen.get(name) {
if now.duration_since(*last_seen_time) < self.config.unreachable_timeout {
reachable_nodes.insert(name.clone());
} else {
unreachable_count += 1;
warn!(
"Node {} unreachable for {:?}",
name,
now.duration_since(*last_seen_time)
);
}
} else {
// New node, consider it reachable for now
reachable_nodes.insert(name.clone());
}
}
}
let reachable_count = reachable_nodes.len();
// Determine partition state
let state = if unreachable_count == 0 {
PartitionState::Normal
} else if reachable_count >= self.config.quorum_threshold {
PartitionState::PartitionedWithQuorum
} else {
PartitionState::PartitionedWithoutQuorum
};
// Update current state
*self.current_state.write() = state.clone();
if state != PartitionState::Normal {
warn!(
"Partition detected: state={:?}, reachable={}, unreachable={}, total_alive={}",
state, reachable_count, unreachable_count, alive_count
);
}
state
}
/// Get current partition state
pub fn current_state(&self) -> PartitionState {
self.current_state.read().clone()
}
/// Check if we have quorum
pub fn has_quorum(&self, reachable_count: usize) -> bool {
reachable_count >= self.config.quorum_threshold
}
/// Get unreachable nodes
pub fn get_unreachable_nodes(
&self,
cluster_state: &BTreeMap<String, NodeState>,
) -> Vec<String> {
let now = Instant::now();
let last_seen = self.last_seen.read();
let mut unreachable = Vec::new();
for (name, node) in cluster_state.iter() {
if node.status == NodeStatus::Alive as i32 {
if let Some(last_seen_time) = last_seen.get(name) {
if now.duration_since(*last_seen_time) >= self.config.unreachable_timeout {
unreachable.push(name.clone());
}
}
}
}
unreachable
}
/// Check if we should continue serving (have quorum)
pub fn should_serve(&self) -> bool {
let state = self.current_state.read();
matches!(
*state,
PartitionState::Normal | PartitionState::PartitionedWithQuorum
)
}
}
impl Default for PartitionDetector {
fn default() -> Self {
Self::new(PartitionConfig::default())
}
}
#[cfg(test)]
mod tests {
use std::{collections::BTreeMap, time::Duration};
use super::*;
// Import NodeState and NodeStatus from gossip module
use crate::mesh::service::gossip::{NodeState, NodeStatus};
fn create_test_config() -> PartitionConfig {
PartitionConfig {
unreachable_timeout: Duration::from_millis(100),
min_cluster_size: 3,
quorum_threshold: 2,
}
}
fn create_node_state(name: &str, address: &str, status: NodeStatus) -> NodeState {
NodeState {
name: name.to_string(),
address: address.to_string(),
status: status as i32,
version: 1,
metadata: std::collections::HashMap::new(),
}
}
#[test]
fn test_partition_config_default() {
let config = PartitionConfig::default();
assert_eq!(config.unreachable_timeout, Duration::from_secs(30));
assert_eq!(config.min_cluster_size, 3);
assert_eq!(config.quorum_threshold, 2);
}
#[test]
fn test_partition_detector_initial_state() {
let config = create_test_config();
let detector = PartitionDetector::new(config);
assert_eq!(detector.current_state(), PartitionState::Normal);
assert!(detector.should_serve());
}
#[test]
fn test_update_last_seen() {
let config = create_test_config();
let detector = PartitionDetector::new(config);
detector.update_last_seen("node1");
detector.update_last_seen("node2");
// Verify nodes are tracked
let cluster_state = BTreeMap::new();
let state = detector.detect_partition(&cluster_state);
assert_eq!(state, PartitionState::Normal);
}
#[test]
fn test_detect_partition_normal() {
let config = create_test_config();
let detector = PartitionDetector::new(config);
let mut cluster_state = BTreeMap::new();
cluster_state.insert(
"node1".to_string(),
create_node_state("node1", "127.0.0.1:8080", NodeStatus::Alive),
);
cluster_state.insert(
"node2".to_string(),
create_node_state("node2", "127.0.0.1:8081", NodeStatus::Alive),
);
cluster_state.insert(
"node3".to_string(),
create_node_state("node3", "127.0.0.1:8082", NodeStatus::Alive),
);
// Update last seen for all nodes
detector.update_last_seen("node1");
detector.update_last_seen("node2");
detector.update_last_seen("node3");
let state = detector.detect_partition(&cluster_state);
assert_eq!(state, PartitionState::Normal);
assert!(detector.should_serve());
}
#[test]
fn test_detect_partition_with_quorum() {
let config = create_test_config();
let detector = PartitionDetector::new(config);
let mut cluster_state = BTreeMap::new();
cluster_state.insert(
"node1".to_string(),
create_node_state("node1", "127.0.0.1:8080", NodeStatus::Alive),
);
cluster_state.insert(
"node2".to_string(),
create_node_state("node2", "127.0.0.1:8081", NodeStatus::Alive),
);
cluster_state.insert(
"node3".to_string(),
create_node_state("node3", "127.0.0.1:8082", NodeStatus::Alive),
);
// Update last seen for node1 and node2 (quorum)
detector.update_last_seen("node1");
detector.update_last_seen("node2");
// Don't update node3, but wait for it to be considered unreachable
// Since node3 is new, it's initially considered reachable
// We need to update it first, then wait for timeout
detector.update_last_seen("node3");
std::thread::sleep(Duration::from_millis(150));
// Update node1 and node2 again to keep them reachable
detector.update_last_seen("node1");
detector.update_last_seen("node2");
let state = detector.detect_partition(&cluster_state);
// node1 and node2 are still reachable (quorum of 2), node3 is unreachable
assert_eq!(state, PartitionState::PartitionedWithQuorum);
assert!(detector.should_serve());
}
#[test]
fn test_detect_partition_without_quorum() {
let mut config = create_test_config();
config.quorum_threshold = 2;
let detector = PartitionDetector::new(config);
let mut cluster_state = BTreeMap::new();
cluster_state.insert(
"node1".to_string(),
create_node_state("node1", "127.0.0.1:8080", NodeStatus::Alive),
);
cluster_state.insert(
"node2".to_string(),
create_node_state("node2", "127.0.0.1:8081", NodeStatus::Alive),
);
cluster_state.insert(
"node3".to_string(),
create_node_state("node3", "127.0.0.1:8082", NodeStatus::Alive),
);
// Update last seen for all nodes first
detector.update_last_seen("node1");
detector.update_last_seen("node2");
detector.update_last_seen("node3");
// Wait for node2 and node3 to become unreachable
std::thread::sleep(Duration::from_millis(150));
// Only update node1 again to keep it reachable
detector.update_last_seen("node1");
let state = detector.detect_partition(&cluster_state);
// Only node1 is reachable (below quorum of 2)
assert_eq!(state, PartitionState::PartitionedWithoutQuorum);
assert!(!detector.should_serve());
}
#[test]
fn test_has_quorum() {
let config = create_test_config();
let detector = PartitionDetector::new(config);
assert!(detector.has_quorum(2));
assert!(detector.has_quorum(3));
assert!(!detector.has_quorum(1));
assert!(!detector.has_quorum(0));
}
#[test]
fn test_get_unreachable_nodes() {
let config = create_test_config();
let detector = PartitionDetector::new(config);
let mut cluster_state = BTreeMap::new();
cluster_state.insert(
"node1".to_string(),
create_node_state("node1", "127.0.0.1:8080", NodeStatus::Alive),
);
cluster_state.insert(
"node2".to_string(),
create_node_state("node2", "127.0.0.1:8081", NodeStatus::Alive),
);
cluster_state.insert(
"node3".to_string(),
create_node_state("node3", "127.0.0.1:8082", NodeStatus::Alive),
);
// Update last seen for all nodes
detector.update_last_seen("node1");
detector.update_last_seen("node2");
detector.update_last_seen("node3");
// Initially no unreachable nodes
let unreachable = detector.get_unreachable_nodes(&cluster_state);
assert!(unreachable.is_empty());
// Wait for timeout
std::thread::sleep(Duration::from_millis(150));
// All nodes should be unreachable now
let unreachable = detector.get_unreachable_nodes(&cluster_state);
assert_eq!(unreachable.len(), 3);
assert!(unreachable.contains(&"node1".to_string()));
assert!(unreachable.contains(&"node2".to_string()));
assert!(unreachable.contains(&"node3".to_string()));
}
#[test]
fn test_get_unreachable_nodes_with_recent_updates() {
let config = create_test_config();
let detector = PartitionDetector::new(config);
let mut cluster_state = BTreeMap::new();
cluster_state.insert(
"node1".to_string(),
create_node_state("node1", "127.0.0.1:8080", NodeStatus::Alive),
);
cluster_state.insert(
"node2".to_string(),
create_node_state("node2", "127.0.0.1:8081", NodeStatus::Alive),
);
// Update node1 first (old)
detector.update_last_seen("node1");
std::thread::sleep(Duration::from_millis(50));
// Update node2 later (more recent)
detector.update_last_seen("node2");
// Wait for node1 to timeout but node2 should still be reachable
std::thread::sleep(Duration::from_millis(60));
let unreachable = detector.get_unreachable_nodes(&cluster_state);
// node1 should be unreachable (updated 110ms ago), node2 should still be reachable (updated 60ms ago)
assert!(unreachable.contains(&"node1".to_string()));
assert!(!unreachable.contains(&"node2".to_string()));
}
#[test]
fn test_detect_partition_ignores_non_alive_nodes() {
let config = create_test_config();
let detector = PartitionDetector::new(config);
let mut cluster_state = BTreeMap::new();
cluster_state.insert(
"node1".to_string(),
create_node_state("node1", "127.0.0.1:8080", NodeStatus::Alive),
);
cluster_state.insert(
"node2".to_string(),
create_node_state("node2", "127.0.0.1:8081", NodeStatus::Down),
);
cluster_state.insert(
"node3".to_string(),
create_node_state("node3", "127.0.0.1:8082", NodeStatus::Suspected),
);
detector.update_last_seen("node1");
let state = detector.detect_partition(&cluster_state);
// Only node1 is alive and reachable
// Since node2 and node3 are not alive, they don't count as unreachable
// If all alive nodes are reachable (unreachable_count == 0), state is Normal
assert_eq!(state, PartitionState::Normal);
}
#[test]
fn test_new_node_considered_reachable() {
let config = create_test_config();
let detector = PartitionDetector::new(config);
let mut cluster_state = BTreeMap::new();
cluster_state.insert(
"node1".to_string(),
create_node_state("node1", "127.0.0.1:8080", NodeStatus::Alive),
);
cluster_state.insert(
"new_node".to_string(),
create_node_state("new_node", "127.0.0.1:8083", NodeStatus::Alive),
);
// Don't update last_seen for new_node, it should be considered reachable
detector.update_last_seen("node1");
let state = detector.detect_partition(&cluster_state);
// Both nodes should be considered reachable (node1 explicitly, new_node by default)
assert_eq!(state, PartitionState::Normal);
}
#[test]
fn test_should_serve() {
let config = create_test_config();
let detector = PartitionDetector::new(config);
// Normal state should serve
*detector.current_state.write() = PartitionState::Normal;
assert!(detector.should_serve());
// Partitioned with quorum should serve
*detector.current_state.write() = PartitionState::PartitionedWithQuorum;
assert!(detector.should_serve());
// Partitioned without quorum should not serve
*detector.current_state.write() = PartitionState::PartitionedWithoutQuorum;
assert!(!detector.should_serve());
}
#[test]
fn test_default_implementation() {
let detector = PartitionDetector::default();
assert_eq!(detector.current_state(), PartitionState::Normal);
assert!(detector.should_serve());
}
#[test]
fn test_partition_state_equality() {
assert_eq!(PartitionState::Normal, PartitionState::Normal);
assert_ne!(
PartitionState::Normal,
PartitionState::PartitionedWithQuorum
);
assert_ne!(
PartitionState::PartitionedWithQuorum,
PartitionState::PartitionedWithoutQuorum
);
}
}

View File

@@ -1,964 +0,0 @@
use std::{
net::SocketAddr,
pin::Pin,
sync::Arc,
time::{Duration, Instant},
};
use anyhow::Result;
use futures::Stream;
use tokio_stream::StreamExt;
use tonic::{transport::Server, Response, Status};
use tracing as log;
use tracing::instrument;
use super::{
crdt::SKey,
flow_control::MessageSizeValidator,
gossip::{
self,
gossip_server::{Gossip, GossipServer},
GossipMessage, IncrementalUpdate, NodeState, NodeStatus, NodeUpdate, PingReq,
SnapshotChunk, SnapshotRequest, StateUpdate, StreamAck, StreamMessage, StreamMessageType,
},
incremental::IncrementalUpdateCollector,
metrics::{
record_ack, record_batch_sent, record_nack, record_peer_reconnect, record_snapshot_bytes,
record_snapshot_duration, record_snapshot_trigger, update_peer_connections,
ConvergenceTracker,
},
node_state_machine::NodeStateMachine,
partition::PartitionDetector,
stores::{StateStores, StoreType as LocalStoreType},
sync::MeshSyncManager,
try_ping, ClusterState,
};
#[derive(Debug)]
pub struct GossipService {
state: ClusterState,
self_addr: SocketAddr,
self_name: String,
stores: Option<Arc<StateStores>>, // Optional state stores for CRDT-based sync
sync_manager: Option<Arc<MeshSyncManager>>, // Optional sync manager for applying remote updates
state_machine: Option<Arc<NodeStateMachine>>,
partition_detector: Option<Arc<PartitionDetector>>,
}
impl GossipService {
/// Create snapshot chunks for a store
async fn create_snapshot_chunks(
&self,
store_type: LocalStoreType,
chunk_size: usize,
) -> Vec<SnapshotChunk> {
let stores = match self.stores.as_ref() {
Some(s) => s,
None => {
log::warn!("State stores not available for snapshot generation");
return vec![];
}
};
let proto_store_type = match store_type {
LocalStoreType::Membership => gossip::StoreType::Membership as i32,
LocalStoreType::App => gossip::StoreType::App as i32,
LocalStoreType::Worker => gossip::StoreType::Worker as i32,
LocalStoreType::Policy => gossip::StoreType::Policy as i32,
LocalStoreType::RateLimit => gossip::StoreType::RateLimit as i32,
};
// Get all entries from the store
let entries: Vec<(SKey, Vec<u8>)> = match store_type {
LocalStoreType::Membership => stores
.membership
.all()
.into_iter()
.map(|(k, v)| {
let serialized = serde_json::to_vec(&v).unwrap_or_else(|e| {
log::error!("Failed to serialize membership state: {}", e);
vec![]
});
(k, serialized)
})
.collect(),
LocalStoreType::App => stores
.app
.all()
.into_iter()
.map(|(k, v)| {
let serialized = serde_json::to_vec(&v).unwrap_or_else(|e| {
log::error!("Failed to serialize app state: {}", e);
vec![]
});
(k, serialized)
})
.collect(),
LocalStoreType::Worker => stores
.worker
.all()
.into_iter()
.map(|(k, v)| {
let serialized = serde_json::to_vec(&v).unwrap_or_else(|e| {
log::error!("Failed to serialize worker state: {}", e);
vec![]
});
(k, serialized)
})
.collect(),
LocalStoreType::Policy => stores
.policy
.all()
.into_iter()
.map(|(k, v)| {
let serialized = serde_json::to_vec(&v).unwrap_or_else(|e| {
log::error!("Failed to serialize policy state: {}", e);
vec![]
});
(k, serialized)
})
.collect(),
LocalStoreType::RateLimit => {
// For rate limit, serialize all counters from owners
stores
.rate_limit
.keys()
.into_iter()
.filter_map(|key| {
if stores.rate_limit.is_owner(&key) {
stores.rate_limit.get_counter(&key).map(|counter| {
let serialized = serde_json::to_vec(&counter.snapshot())
.unwrap_or_else(|e| {
log::error!(
"Failed to serialize rate limit counter: {}",
e
);
vec![]
});
(SKey::new(key.clone()), serialized)
})
} else {
None
}
})
.collect()
}
};
if entries.is_empty() {
return vec![];
}
// Split entries into chunks
let mut chunks = Vec::new();
let total_chunks = entries.len().div_ceil(chunk_size);
for (chunk_idx, chunk_entries) in entries.chunks(chunk_size).enumerate() {
let state_updates: Vec<StateUpdate> = chunk_entries
.iter()
.map(|(key, value)| {
// Get actual version from CRDT metadata
let version = match store_type {
LocalStoreType::Membership => stores
.membership
.get_metadata(key)
.map(|(v, _)| v)
.unwrap_or(1),
LocalStoreType::App => {
stores.app.get_metadata(key).map(|(v, _)| v).unwrap_or(1)
}
LocalStoreType::Worker => {
stores.worker.get_metadata(key).map(|(v, _)| v).unwrap_or(1)
}
LocalStoreType::Policy => {
stores.policy.get_metadata(key).map(|(v, _)| v).unwrap_or(1)
}
LocalStoreType::RateLimit => {
// For rate limit, use timestamp as version
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos() as u64
}
};
StateUpdate {
key: key.as_str().to_string(),
value: value.clone(),
version,
actor: self.self_name.clone(),
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos() as u64,
}
})
.collect();
// Calculate checksum for integrity verification
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
for update in &state_updates {
update.key.hash(&mut hasher);
update.value.hash(&mut hasher);
}
let checksum = hasher.finish().to_le_bytes().to_vec();
chunks.push(SnapshotChunk {
store: proto_store_type,
chunk_index: chunk_idx as u64,
total_chunks: total_chunks as u64,
entries: state_updates,
checksum,
});
}
log::info!(
"Generated {} snapshot chunks for store {:?}",
chunks.len(),
store_type
);
chunks
}
}
impl GossipService {
pub fn new(state: ClusterState, self_addr: SocketAddr, self_name: &str) -> Self {
Self {
state,
self_addr,
self_name: self_name.to_string(),
stores: None,
sync_manager: None,
state_machine: None,
partition_detector: None,
}
}
pub fn with_stores(mut self, stores: Arc<StateStores>) -> Self {
self.stores = Some(stores.clone());
// Create state machine if stores are provided
if self.state_machine.is_none() {
use super::node_state_machine::ConvergenceConfig;
self.state_machine = Some(Arc::new(NodeStateMachine::new(
stores,
ConvergenceConfig::default(),
)));
}
self
}
pub fn with_sync_manager(mut self, sync_manager: Arc<MeshSyncManager>) -> Self {
self.sync_manager = Some(sync_manager);
self
}
pub fn with_partition_detector(mut self, partition_detector: Arc<PartitionDetector>) -> Self {
self.partition_detector = Some(partition_detector);
self
}
pub async fn serve_ping_with_shutdown<F: std::future::Future<Output = ()>>(
self,
signal: F,
) -> Result<()> {
let listen_addr = self.self_addr;
let service = GossipServer::new(self);
Server::builder()
.add_service(service)
.serve_with_shutdown(listen_addr, signal)
.await?;
Ok(())
}
async fn merge_state(&self, incoming_nodes: Vec<NodeState>) -> bool {
let mut state = self.state.write();
let mut updated = false;
for node in incoming_nodes {
state
.entry(node.name.clone())
.and_modify(|entry| {
if node.version > entry.version {
*entry = node.clone();
updated = true;
}
})
.or_insert_with(|| {
updated = true;
node
});
}
if updated {
log::info!("Cluster state updated. Current nodes: {}", state.len());
}
updated
}
}
#[tonic::async_trait]
impl Gossip for GossipService {
type SyncStreamStream =
Pin<Box<dyn Stream<Item = Result<StreamMessage, Status>> + Send + 'static>>;
#[instrument(fields(name = %self.self_name), skip(self, request))]
async fn ping_server(
&self,
request: tonic::Request<GossipMessage>,
) -> std::result::Result<Response<NodeUpdate>, Status> {
let message = request.into_inner();
match message.payload {
Some(gossip::gossip_message::Payload::Ping(ping)) => {
log::info!("Received {:?}", ping);
if let Some(stat_sync) = ping.state_sync {
log::info!("Merging state from Ping: {} nodes", stat_sync.nodes.len());
self.merge_state(stat_sync.nodes).await;
}
// Return current status of self node (could be Alive or Leaving)
let current_status = {
let state = self.state.read();
state
.get(&self.self_name)
.map(|n| n.status)
.unwrap_or(NodeStatus::Alive as i32)
};
Ok(Response::new(NodeUpdate {
name: self.self_name.clone(),
address: self.self_addr.to_string(),
status: current_status,
}))
}
Some(gossip::gossip_message::Payload::PingReq(PingReq { node: Some(node) })) => {
log::info!("PingReq to node {} addr:{}", node.name, node.address);
let res = try_ping(&node, None).await?;
Ok(Response::new(res))
}
_ => Err(Status::invalid_argument("Invalid message payload")),
}
}
#[instrument(fields(name = %self.self_name), skip(self, request))]
async fn sync_stream(
&self,
request: tonic::Request<tonic::Streaming<StreamMessage>>,
) -> Result<Response<Self::SyncStreamStream>, Status> {
let mut incoming = request.into_inner();
let self_name = self.self_name.clone();
let state = self.state.clone();
let stores = self.stores.clone();
let sync_manager = self.sync_manager.clone();
// Create output stream with flow control
const CHANNEL_CAPACITY: usize = 128;
let (tx, rx) =
tokio::sync::mpsc::channel::<Result<StreamMessage, Status>>(CHANNEL_CAPACITY);
let size_validator = MessageSizeValidator::default();
// Create incremental update collector if stores are available
let collector = stores.as_ref().map(|stores| {
Arc::new(IncrementalUpdateCollector::new(
stores.clone(),
self_name.clone(),
))
});
// Spawn task to periodically send incremental updates
if let Some(collector) = collector {
let tx_incremental = tx.clone();
let self_name_incremental = self_name.clone();
let size_validator_clone = size_validator.clone();
tokio::spawn(async move {
// Use 1 second interval for rate limit counter sync (faster than other stores)
let mut interval = tokio::time::interval(Duration::from_secs(1)); // Send every 1 second
let mut sequence_counter: u64 = 0;
loop {
interval.tick().await;
// Collect all incremental updates
let all_updates = collector.collect_all_updates();
if !all_updates.is_empty() {
for (store_type, updates) in all_updates {
let proto_store_type = match store_type {
LocalStoreType::Membership => gossip::StoreType::Membership as i32,
LocalStoreType::App => gossip::StoreType::App as i32,
LocalStoreType::Worker => gossip::StoreType::Worker as i32,
LocalStoreType::Policy => gossip::StoreType::Policy as i32,
LocalStoreType::RateLimit => gossip::StoreType::RateLimit as i32,
};
sequence_counter += 1;
let batch_size: usize = updates.iter().map(|u| u.value.len()).sum();
// Validate message size
if let Err(e) = size_validator_clone.validate(batch_size) {
log::warn!(
"Incremental update too large, skipping: {} (max: {} bytes)",
e,
size_validator_clone.max_size()
);
continue;
}
let incremental_update = StreamMessage {
message_type: StreamMessageType::IncrementalUpdate as i32,
payload: Some(gossip::stream_message::Payload::Incremental(
IncrementalUpdate {
store: proto_store_type,
updates: updates.clone(),
version: 0, // Version is tracked per key in StateUpdate
},
)),
sequence: sequence_counter,
peer_id: self_name_incremental.clone(),
};
// Check backpressure using try_send (mpsc::Sender doesn't have len())
match tx_incremental.try_send(Ok(incremental_update)) {
Ok(_) => {
// Successfully queued
// Record metrics
record_batch_sent(&self_name_incremental, batch_size);
// Mark as sent after successful transmission
collector.mark_sent(store_type, &updates);
}
Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
log::debug!(
"Backpressure: channel full, skipping send (will retry next interval)"
);
// Don't mark as sent, will retry next interval
continue;
}
Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
log::warn!(
"Channel closed, stopping incremental update sender"
);
break;
}
}
log::debug!(
"Sent incremental update: store={:?}, {} updates",
store_type,
updates.len()
);
}
}
}
});
}
// Spawn task to handle incoming messages
let mut sequence: u64 = 0;
let _convergence_tracker = ConvergenceTracker::new();
// Track snapshot reception state: (store_type, total_chunks) -> received_chunks
use std::collections::HashMap;
let mut snapshot_state: HashMap<(LocalStoreType, u64), Vec<SnapshotChunk>> = HashMap::new();
tokio::spawn(async move {
let mut peer_id = String::new();
update_peer_connections(&peer_id, true);
// Check if we need to request snapshots on connection
// This happens when:
// 1. We're a new node joining (stores are empty or very small)
// 2. We detect a version gap
if let Some(ref stores) = stores {
for store_type in [
LocalStoreType::Membership,
LocalStoreType::App,
LocalStoreType::Worker,
LocalStoreType::Policy,
LocalStoreType::RateLimit,
] {
let store_len = match store_type {
LocalStoreType::Membership => stores.membership.len(),
LocalStoreType::App => stores.app.len(),
LocalStoreType::Worker => stores.worker.len(),
LocalStoreType::Policy => stores.policy.len(),
LocalStoreType::RateLimit => stores.rate_limit.keys().len(),
};
// If store is empty or very small, request snapshot
if store_len == 0 {
log::info!(
"Store {:?} is empty, requesting snapshot from {}",
store_type,
peer_id
);
let proto_store_type = match store_type {
LocalStoreType::Membership => gossip::StoreType::Membership as i32,
LocalStoreType::App => gossip::StoreType::App as i32,
LocalStoreType::Worker => gossip::StoreType::Worker as i32,
LocalStoreType::Policy => gossip::StoreType::Policy as i32,
LocalStoreType::RateLimit => gossip::StoreType::RateLimit as i32,
};
let snapshot_request = StreamMessage {
message_type: StreamMessageType::SnapshotRequest as i32,
payload: Some(gossip::stream_message::Payload::SnapshotRequest(
SnapshotRequest {
store: proto_store_type,
from_version: 0, // Request from beginning
},
)),
sequence: 0,
peer_id: self_name.clone(),
};
if tx.send(Ok(snapshot_request)).await.is_err() {
log::warn!("Failed to send snapshot request");
}
}
}
}
while let Some(msg_result) = incoming.next().await {
match msg_result {
Ok(msg) => {
sequence += 1;
peer_id = msg.peer_id.clone();
match msg.message_type() {
StreamMessageType::IncrementalUpdate => {
if let Some(gossip::stream_message::Payload::Incremental(update)) =
&msg.payload
{
// Validate message size
let msg_size: usize =
update.updates.iter().map(|u| u.value.len()).sum();
if let Err(e) = size_validator.validate(msg_size) {
log::warn!(
"Received oversized incremental update from {}: {} (max: {} bytes), rejecting",
peer_id, e, size_validator.max_size()
);
let nack = StreamMessage {
message_type: StreamMessageType::Nack as i32,
payload: Some(gossip::stream_message::Payload::Ack(
StreamAck {
sequence: msg.sequence,
success: false,
error_message: format!(
"Message too large: {}",
e
),
},
)),
sequence,
peer_id: self_name.clone(),
};
if tx.send(Ok(nack)).await.is_err() {
break;
}
record_nack(&peer_id);
continue;
}
let store_type = LocalStoreType::from_proto(update.store);
log::info!("Received incremental update from {}: store={:?}, {} updates",
peer_id, store_type, update.updates.len());
// Apply incremental updates to state stores
// This will be handled by the sync manager if available
// For now, we acknowledge and the sync manager will handle it
if let Some(ref sync_manager) = sync_manager {
for state_update in &update.updates {
match store_type {
LocalStoreType::Worker => {
// Deserialize and apply worker state
if let Ok(worker_state) = serde_json::from_slice::<
super::stores::WorkerState,
>(
&state_update.value
) {
// Extract actor from StateUpdate
let actor =
Some(state_update.actor.clone());
sync_manager.apply_remote_worker_state(
worker_state,
actor,
);
}
}
LocalStoreType::Policy => {
// Deserialize and apply policy state
if let Ok(policy_state) = serde_json::from_slice::<
super::stores::PolicyState,
>(
&state_update.value
) {
// Extract actor from StateUpdate
let actor =
Some(state_update.actor.clone());
// Check if this is a tree state update
if policy_state.policy_type == "tree_state"
{
// Deserialize tree state
if let Ok(tree_state) =
serde_json::from_slice::<
super::tree_ops::TreeState,
>(
&policy_state.config
)
{
sync_manager
.apply_remote_tree_operation(
policy_state
.model_id
.clone(),
tree_state,
actor,
);
}
} else {
// Regular policy state update
sync_manager.apply_remote_policy_state(
policy_state,
actor,
);
}
}
}
LocalStoreType::RateLimit => {
// Deserialize and apply rate limit counter
if let Ok(counter) = serde_json::from_slice::<
super::crdt::CRDTPNCounter,
>(
&state_update.value
) {
// Convert CRDTPNCounter to SyncPNCounter for merging
let sync_counter =
super::crdt::SyncPNCounter::new();
sync_counter.merge(&counter);
sync_manager
.apply_remote_rate_limit_counter(
state_update.key.clone(),
&sync_counter,
);
}
}
_ => {
// Other store types handled elsewhere
}
}
}
}
let ack = StreamMessage {
message_type: StreamMessageType::Ack as i32,
payload: Some(gossip::stream_message::Payload::Ack(
StreamAck {
sequence: msg.sequence,
success: true,
error_message: String::new(),
},
)),
sequence,
peer_id: self_name.clone(),
};
if tx.send(Ok(ack)).await.is_err() {
break;
}
}
}
StreamMessageType::SnapshotRequest => {
if let Some(gossip::stream_message::Payload::SnapshotRequest(req)) =
&msg.payload
{
let store_type = LocalStoreType::from_proto(req.store);
let store_name = store_type.as_str();
log::info!("Received snapshot request from {}: store={:?}, from_version={}",
peer_id, store_type, req.from_version);
record_snapshot_trigger(store_name, "request");
let snapshot_start = Instant::now();
// Generate and send snapshot chunks
let service = GossipService {
state: state.clone(),
self_addr: SocketAddr::from(([0, 0, 0, 0], 0)), // Not used in snapshot generation
self_name: self_name.clone(),
stores: stores.clone(),
sync_manager: sync_manager.clone(),
state_machine: None,
partition_detector: None,
};
let chunks =
service.create_snapshot_chunks(store_type, 100).await; // chunk_size = 100 entries
let total_chunks = chunks.len() as u64;
let mut total_bytes = 0;
for (idx, chunk) in chunks.into_iter().enumerate() {
let chunk_bytes = chunk
.entries
.iter()
.map(|e| e.value.len())
.sum::<usize>();
total_bytes += chunk_bytes;
let mut chunk_msg = StreamMessage {
message_type: StreamMessageType::SnapshotChunk as i32,
payload: Some(
gossip::stream_message::Payload::SnapshotChunk(
chunk,
),
),
sequence: sequence + idx as u64 + 1,
peer_id: self_name.clone(),
};
// Update chunk metadata
if let Some(
gossip::stream_message::Payload::SnapshotChunk(
ref mut c,
),
) = chunk_msg.payload
{
c.chunk_index = idx as u64;
c.total_chunks = total_chunks;
}
// Check backpressure using try_send
match tx.try_send(Ok(chunk_msg)) {
Ok(_) => {
// Successfully queued
}
Err(tokio::sync::mpsc::error::TrySendError::Full(
msg,
)) => {
log::debug!(
"Backpressure: channel full, waiting for drain"
);
// Wait a bit for channel to drain, then use blocking send
tokio::time::sleep(Duration::from_millis(100))
.await;
if tx.send(msg).await.is_err() {
log::warn!("Backpressure: channel closed, stopping snapshot");
break;
}
}
Err(
tokio::sync::mpsc::error::TrySendError::Closed(_),
) => {
log::warn!("Channel closed, stopping snapshot");
break;
}
}
}
record_snapshot_duration(store_name, snapshot_start.elapsed());
record_snapshot_bytes(store_name, "sent", total_bytes);
// Send snapshot complete message
let complete = StreamMessage {
message_type: StreamMessageType::SnapshotComplete as i32,
payload: None,
sequence: sequence + total_chunks + 1,
peer_id: self_name.clone(),
};
if tx.send(Ok(complete)).await.is_err() {
break;
}
// Send ACK
let ack = StreamMessage {
message_type: StreamMessageType::Ack as i32,
payload: Some(gossip::stream_message::Payload::Ack(
StreamAck {
sequence: msg.sequence,
success: true,
error_message: String::new(),
},
)),
sequence,
peer_id: self_name.clone(),
};
record_ack(&peer_id, true);
if tx.send(Ok(ack)).await.is_err() {
break;
}
}
}
StreamMessageType::SnapshotChunk => {
if let Some(gossip::stream_message::Payload::SnapshotChunk(chunk)) =
&msg.payload
{
let store_type = LocalStoreType::from_proto(chunk.store);
let store_name = store_type.as_str();
log::info!(
"Received snapshot chunk from {}: store={:?}, chunk={}/{}",
peer_id,
store_type,
chunk.chunk_index,
chunk.total_chunks
);
// Record metrics
let chunk_bytes: usize =
chunk.entries.iter().map(|e| e.value.len()).sum();
record_snapshot_bytes(store_name, "received", chunk_bytes);
// Store chunk for later application
let chunk_key = (store_type, chunk.total_chunks);
snapshot_state
.entry(chunk_key)
.or_default()
.push(chunk.clone());
// Check if we've received all chunks
if let Some(received_chunks) = snapshot_state.get(&chunk_key) {
if received_chunks.len() as u64 == chunk.total_chunks {
// All chunks received, apply snapshot
log::info!("All {} chunks received for store {:?}, applying snapshot",
chunk.total_chunks, store_type);
if let Some(ref stores) = stores {
// Sort chunks by index
let mut sorted_chunks = received_chunks.clone();
sorted_chunks.sort_by_key(|c| c.chunk_index);
// Apply all entries from chunks
for chunk in &sorted_chunks {
for entry in &chunk.entries {
let key = SKey::new(entry.key.clone());
match store_type {
LocalStoreType::Membership => {
if let Ok(membership_state) = serde_json::from_slice::<super::stores::MembershipState>(&entry.value) {
stores.membership.insert(key, membership_state, entry.actor.clone());
}
}
LocalStoreType::App => {
if let Ok(app_state) = serde_json::from_slice::<super::stores::AppState>(&entry.value) {
stores.app.insert(key, app_state, entry.actor.clone());
}
}
LocalStoreType::Worker => {
if let Ok(worker_state) = serde_json::from_slice::<super::stores::WorkerState>(&entry.value) {
stores.worker.insert(key, worker_state.clone(), entry.actor.clone());
// Also update sync manager if available
if let Some(ref sync_manager) = sync_manager {
sync_manager.apply_remote_worker_state(worker_state, Some(entry.actor.clone()));
}
}
}
LocalStoreType::Policy => {
if let Ok(policy_state) = serde_json::from_slice::<super::stores::PolicyState>(&entry.value) {
stores.policy.insert(key, policy_state.clone(), entry.actor.clone());
// Also update sync manager if available
if let Some(ref sync_manager) = sync_manager {
// Check if this is a tree state update
if policy_state.policy_type == "tree_state" {
// Deserialize tree state
if let Ok(tree_state) = serde_json::from_slice::<
super::tree_ops::TreeState,
>(
&policy_state.config
) {
sync_manager.apply_remote_tree_operation(
policy_state.model_id.clone(),
tree_state,
Some(entry.actor.clone()),
);
}
} else {
sync_manager.apply_remote_policy_state(policy_state, Some(entry.actor.clone()));
}
}
}
}
LocalStoreType::RateLimit => {
// For rate limit counters, deserialize and merge
if let Ok(counter) = serde_json::from_slice::<super::crdt::CRDTPNCounter>(&entry.value) {
if let Some(ref sync_manager) = sync_manager {
let sync_counter = super::crdt::SyncPNCounter::new();
sync_counter.merge(&counter);
sync_manager.apply_remote_rate_limit_counter(entry.key.clone(), &sync_counter);
}
}
}
}
}
}
// Clear snapshot state
snapshot_state.remove(&chunk_key);
log::info!(
"Snapshot applied successfully for store {:?}",
store_type
);
}
}
}
let ack = StreamMessage {
message_type: StreamMessageType::Ack as i32,
payload: Some(gossip::stream_message::Payload::Ack(
StreamAck {
sequence: msg.sequence,
success: true,
error_message: String::new(),
},
)),
sequence,
peer_id: self_name.clone(),
};
record_ack(&peer_id, true);
if tx.send(Ok(ack)).await.is_err() {
break;
}
}
}
StreamMessageType::Ack => {
log::debug!(
"Received ACK from {}: sequence={}",
peer_id,
msg.sequence
);
if let Some(gossip::stream_message::Payload::Ack(ack)) =
&msg.payload
{
record_ack(&peer_id, ack.success);
}
}
StreamMessageType::Heartbeat => {
// Send heartbeat back
let heartbeat = StreamMessage {
message_type: StreamMessageType::Heartbeat as i32,
payload: None,
sequence,
peer_id: self_name.clone(),
};
if tx.send(Ok(heartbeat)).await.is_err() {
break;
}
}
_ => {
log::warn!(
"Unknown message type from {}: {:?}",
peer_id,
msg.message_type
);
}
}
}
Err(e) => {
log::error!("Error receiving stream message: {}", e);
record_nack(&peer_id);
update_peer_connections(&peer_id, false);
record_peer_reconnect(&peer_id);
break;
}
}
}
log::info!("Stream from {} closed", peer_id);
update_peer_connections(&peer_id, false);
});
// Convert receiver to stream
let output_stream = tokio_stream::wrappers::ReceiverStream::new(rx);
Ok(Response::new(
Box::pin(output_stream) as Self::SyncStreamStream
))
}
}

View File

@@ -1,122 +0,0 @@
syntax = "proto3";
package sglang.mesh.gossip;
service Gossip {
rpc PingServer(GossipMessage) returns (NodeUpdate);
// Bidirectional streaming for state synchronization
rpc SyncStream(stream StreamMessage) returns (stream StreamMessage);
}
enum NodeStatus {
INIT = 0;
ALIVE = 1;
SUSPECTED = 2;
DOWN = 3;
LEAVING = 4;
}
message NodeState {
string name = 1;
string address = 2;
NodeStatus status = 3;
uint64 version = 4;
map<string, bytes> metadata = 5;
}
message StateSync {
repeated NodeState nodes = 1;
}
message Ping {
StateSync state_sync = 1;
}
message PingReq {
NodeState node = 1;
}
message Ack {
uint64 timestamp = 1;
}
message NodeUpdate {
string name = 1;
string address = 2;
NodeStatus status = 3;
}
message GossipMessage {
oneof payload {
Ping ping = 1;
PingReq ping_req = 2;
}
}
// Stream message types for bidirectional streaming
enum StreamMessageType {
INCREMENTAL_UPDATE = 0;
SNAPSHOT_REQUEST = 1;
SNAPSHOT_CHUNK = 2;
SNAPSHOT_COMPLETE = 3;
ACK = 4;
NACK = 5;
HEARTBEAT = 6;
}
message StreamMessage {
StreamMessageType message_type = 1;
oneof payload {
IncrementalUpdate incremental = 2;
SnapshotRequest snapshot_request = 3;
SnapshotChunk snapshot_chunk = 4;
StreamAck ack = 5;
}
uint64 sequence = 6; // Sequence number for ordering
string peer_id = 7; // Sender peer ID
}
// Incremental state update (steady-state)
message IncrementalUpdate {
StoreType store = 1;
repeated StateUpdate updates = 2;
uint64 version = 3;
}
message StateUpdate {
string key = 1;
bytes value = 2; // Serialized state value
uint64 version = 3;
string actor = 4;
uint64 timestamp = 5;
}
// Snapshot request (on gap or new join)
message SnapshotRequest {
StoreType store = 1;
uint64 from_version = 2; // Request snapshot from this version
}
// Snapshot chunk (per-store chunking)
message SnapshotChunk {
StoreType store = 1;
uint64 chunk_index = 2;
uint64 total_chunks = 3;
repeated StateUpdate entries = 4;
bytes checksum = 5; // For integrity verification
}
message StreamAck {
uint64 sequence = 1;
bool success = 2;
string error_message = 3;
}
enum StoreType {
MEMBERSHIP = 0;
APP = 1;
WORKER = 2;
POLICY = 3;
RATE_LIMIT = 4;
}

View File

@@ -1,257 +0,0 @@
//! Rate limit time window management
//!
//! Manages time windows for global rate limiting with periodic counter resets
use std::{sync::Arc, time::Duration};
use tokio::time::interval;
use tracing::{debug, info};
use super::sync::MeshSyncManager;
/// Rate limit window manager
/// Handles periodic reset of rate limit counters for time window management
pub struct RateLimitWindow {
sync_manager: Arc<MeshSyncManager>,
window_seconds: u64,
}
impl RateLimitWindow {
pub fn new(sync_manager: Arc<MeshSyncManager>, window_seconds: u64) -> Self {
Self {
sync_manager,
window_seconds,
}
}
/// Start the window reset task
/// This task periodically resets the global rate limit counter
pub async fn start_reset_task(self) {
let mut interval_timer = interval(Duration::from_secs(self.window_seconds));
info!(
"Starting rate limit window reset task with {}s interval",
self.window_seconds
);
loop {
interval_timer.tick().await;
debug!("Resetting global rate limit counter");
self.sync_manager.reset_global_rate_limit_counter();
}
}
}
#[cfg(test)]
mod tests {
use std::{sync::Arc, time::Duration};
use tokio::time::sleep;
use super::*;
use crate::mesh::stores::{
RateLimitConfig, StateStores, GLOBAL_RATE_LIMIT_COUNTER_KEY, GLOBAL_RATE_LIMIT_KEY,
};
#[test]
fn test_rate_limit_window_new() {
let stores = Arc::new(StateStores::with_self_name("node1".to_string()));
let sync_manager = Arc::new(MeshSyncManager::new(stores, "node1".to_string()));
let window = RateLimitWindow::new(sync_manager, 60);
// Should create without panicking
assert_eq!(window.window_seconds, 60);
}
#[test]
fn test_rate_limit_window_different_intervals() {
let stores = Arc::new(StateStores::with_self_name("node1".to_string()));
let sync_manager = Arc::new(MeshSyncManager::new(stores, "node1".to_string()));
let window1 = RateLimitWindow::new(sync_manager.clone(), 30);
assert_eq!(window1.window_seconds, 30);
let window2 = RateLimitWindow::new(sync_manager, 120);
assert_eq!(window2.window_seconds, 120);
}
#[tokio::test]
async fn test_rate_limit_window_reset_task_interval() {
let stores = Arc::new(StateStores::with_self_name("node1".to_string()));
let sync_manager = Arc::new(MeshSyncManager::new(stores, "node1".to_string()));
// Set a very short window for testing (1 second)
let window = RateLimitWindow::new(sync_manager, 1);
// Spawn the reset task
let task_handle = tokio::spawn(async move {
window.start_reset_task().await;
});
// Wait a bit to allow the task to run
sleep(Duration::from_millis(1500)).await;
// Cancel the task
task_handle.abort();
// The task should have started (we can't easily verify it ran without
// more complex mocking, but we can verify it doesn't panic)
// In a real scenario, you'd use a mock to track reset calls
}
#[tokio::test]
async fn test_rate_limit_window_reset_task() {
let stores = Arc::new(StateStores::with_self_name("node1".to_string()));
let sync_manager = Arc::new(MeshSyncManager::new(stores.clone(), "node1".to_string()));
// Setup membership
stores.rate_limit.update_membership(&["node1".to_string()]);
// Setup config
let key = crate::mesh::crdt::SKey::new(GLOBAL_RATE_LIMIT_KEY.to_string());
let config = RateLimitConfig {
limit_per_second: 100,
};
let serialized = serde_json::to_vec(&config).unwrap();
stores.app.insert(
key,
crate::mesh::stores::AppState {
key: GLOBAL_RATE_LIMIT_KEY.to_string(),
value: serialized,
version: 1,
},
"node1".to_string(),
);
// Increment counter
if stores.rate_limit.is_owner(GLOBAL_RATE_LIMIT_COUNTER_KEY) {
sync_manager.sync_rate_limit_inc(GLOBAL_RATE_LIMIT_COUNTER_KEY.to_string(), 10);
let value_before = sync_manager.get_rate_limit_value(GLOBAL_RATE_LIMIT_COUNTER_KEY);
assert!(value_before.is_some() && value_before.unwrap() > 0);
// Create window manager with short interval for testing
let window = RateLimitWindow::new(sync_manager.clone(), 1); // 1 second
// Start reset task in background
let reset_handle = tokio::spawn(async move {
window.start_reset_task().await;
});
// Wait a bit for reset to happen
sleep(Duration::from_millis(1500)).await;
// Check that counter was reset (or at least decremented)
let _value_after = sync_manager.get_rate_limit_value(GLOBAL_RATE_LIMIT_COUNTER_KEY);
// Counter should be reset or significantly reduced
// Note: The exact value depends on timing, but it should be less than initial
// Cancel the task
reset_handle.abort();
}
}
#[tokio::test]
async fn test_rate_limit_window_reset_with_counter() {
use crate::mesh::{crdt::SKey, stores::MembershipState};
// Use with_self_name to ensure RateLimitStore uses the same self_name
let stores = Arc::new(StateStores::with_self_name("test_node".to_string()));
let sync_manager = Arc::new(MeshSyncManager::new(
stores.clone(),
"test_node".to_string(),
));
// First, add this node to membership so it can be an owner
let membership_key = SKey::new("test_node".to_string());
let membership_state = MembershipState {
name: "test_node".to_string(),
address: "127.0.0.1:8080".to_string(),
status: 1, // NodeStatus::Alive
version: 1,
metadata: Default::default(),
};
stores
.membership
.insert(membership_key, membership_state, "test_node".to_string());
// Update rate limit membership so this node becomes an owner
sync_manager.update_rate_limit_membership();
// Check if node is owner before incrementing
let key = GLOBAL_RATE_LIMIT_COUNTER_KEY.to_string();
let is_owner = stores.rate_limit.is_owner(&key);
assert!(is_owner, "Node should be owner of the rate limit key");
// Set up a rate limit counter via sync_manager
// This should increment the counter if the node is an owner
sync_manager.sync_rate_limit_inc(key.clone(), 10);
// Verify counter exists (was created)
// Note: The actual value might be 0 due to PNCounter implementation details,
// but the counter should exist after inc is called
let counter_opt = stores.rate_limit.get_counter(&key);
assert!(counter_opt.is_some(), "Counter should exist after inc call");
// Verify counter was created after inc call
// Note: The actual value depends on PNCounter implementation,
// but the counter should exist after inc is called
// Reset the counter
sync_manager.reset_global_rate_limit_counter();
// Verify reset was called (counter should still exist)
// The reset implementation decrements by current count,
// so the value should be 0 or negative after reset
let reset_value = stores.rate_limit.value(&key).unwrap_or(0);
// After reset, value should be <= 0 (since we decrement by current count)
assert!(
reset_value <= 0,
"Counter should be reset to 0 or less, got: {}",
reset_value
);
}
#[test]
fn test_rate_limit_window_zero_seconds() {
let stores = Arc::new(StateStores::with_self_name("node1".to_string()));
let sync_manager = Arc::new(MeshSyncManager::new(stores, "node1".to_string()));
// Should handle zero seconds (though not recommended in practice)
let window = RateLimitWindow::new(sync_manager, 0);
assert_eq!(window.window_seconds, 0);
}
#[test]
fn test_rate_limit_window_large_interval() {
let stores = Arc::new(StateStores::with_self_name("node1".to_string()));
let sync_manager = Arc::new(MeshSyncManager::new(stores, "node1".to_string()));
// Test with a large interval
let window = RateLimitWindow::new(sync_manager, 86400); // 24 hours
assert_eq!(window.window_seconds, 86400);
}
#[tokio::test]
async fn test_reset_global_rate_limit_counter_logic() {
let stores = Arc::new(StateStores::with_self_name("node1".to_string()));
let sync_manager = Arc::new(MeshSyncManager::new(stores.clone(), "node1".to_string()));
// Setup membership
stores.rate_limit.update_membership(&["node1".to_string()]);
if stores.rate_limit.is_owner(GLOBAL_RATE_LIMIT_COUNTER_KEY) {
// Increment counter
sync_manager.sync_rate_limit_inc(GLOBAL_RATE_LIMIT_COUNTER_KEY.to_string(), 20);
let value_before = sync_manager.get_rate_limit_value(GLOBAL_RATE_LIMIT_COUNTER_KEY);
assert!(value_before.is_some() && value_before.unwrap() > 0);
// Reset
sync_manager.reset_global_rate_limit_counter();
// Check that counter was reset
let value_after = sync_manager.get_rate_limit_value(GLOBAL_RATE_LIMIT_COUNTER_KEY);
// Should be 0 or negative after reset
assert!(value_after.is_none() || value_after.unwrap() <= 0);
}
}
}

View File

@@ -1,600 +0,0 @@
use std::{
collections::{BTreeMap, HashMap},
net::SocketAddr,
str::FromStr,
sync::Arc,
time::Duration,
};
use anyhow::Result;
use parking_lot::RwLock;
use tonic::Request;
use tracing as log;
pub mod gossip {
#![allow(unused_qualifications)]
tonic::include_proto!("sglang.mesh.gossip");
}
use gossip::{
gossip_client, gossip_message, GossipMessage, NodeState, NodeStatus, NodeUpdate, Ping,
StateSync,
};
use crate::mesh::{
controller::MeshController,
node_state_machine::{ConvergenceConfig, NodeStateMachine},
partition::PartitionDetector,
ping_server::GossipService,
};
pub type ClusterState = Arc<RwLock<BTreeMap<String, NodeState>>>;
pub struct MeshServerConfig {
pub self_name: String,
pub self_addr: SocketAddr,
pub init_peer: Option<SocketAddr>,
}
/// MeshServerHandler
/// It is the handler for the mesh server, which is responsible for the node management.
/// Includes some basic node management logic, like shutdown,
/// node discovery(TODO), node status update(TODO), etc.
pub struct MeshServerHandler {
pub state: ClusterState,
pub self_name: String,
_self_addr: SocketAddr,
signal_tx: tokio::sync::watch::Sender<()>,
partition_detector: Option<Arc<PartitionDetector>>,
state_machine: Option<Arc<NodeStateMachine>>,
}
impl MeshServerHandler {
pub fn new(
state: ClusterState,
self_name: &str,
self_addr: SocketAddr,
signal_tx: tokio::sync::watch::Sender<()>,
) -> Self {
Self {
state,
self_name: self_name.to_string(),
_self_addr: self_addr,
signal_tx,
partition_detector: None,
state_machine: None,
}
}
/// Create with partition detector and state machine
pub fn with_partition_and_state_machine(
state: ClusterState,
self_name: &str,
self_addr: SocketAddr,
signal_tx: tokio::sync::watch::Sender<()>,
stores: Option<Arc<super::stores::StateStores>>,
) -> Self {
let partition_detector = Some(Arc::new(PartitionDetector::default()));
let state_machine =
stores.map(|s| Arc::new(NodeStateMachine::new(s, ConvergenceConfig::default())));
Self {
state,
self_name: self_name.to_string(),
_self_addr: self_addr,
signal_tx,
partition_detector,
state_machine,
}
}
/// Get partition detector
pub fn partition_detector(&self) -> Option<&Arc<PartitionDetector>> {
self.partition_detector.as_ref()
}
/// Get state machine
pub fn state_machine(&self) -> Option<&Arc<NodeStateMachine>> {
self.state_machine.as_ref()
}
/// Check if node is ready
pub fn is_ready(&self) -> bool {
self.state_machine
.as_ref()
.map(|sm| sm.is_ready())
.unwrap_or(true) // If no state machine, consider ready
}
/// Check if we should serve (have quorum)
pub fn should_serve(&self) -> bool {
self.partition_detector
.as_ref()
.map(|pd| pd.should_serve())
.unwrap_or(true) // If no partition detector, consider should serve
}
/// Shutdown immediately without graceful shutdown
pub fn shutdown(&self) {
self.signal_tx.send(()).ok();
}
/// Graceful shutdown: broadcast LEAVING status to all alive nodes,
/// wait for propagation, then shutdown
pub async fn graceful_shutdown(&self) -> Result<()> {
log::info!("Starting graceful shutdown for node {}", self.self_name);
let (leaving_node, alive_nodes) = {
let state = self.state.read();
let mut self_node = if let Some(self_node) = state.get(&self.self_name) {
self_node.clone()
} else {
self.signal_tx.send(()).ok();
return Ok(());
};
if self_node.status != NodeStatus::Leaving as i32 {
self_node.status = NodeStatus::Leaving as i32;
self_node.version += 1;
let alive_nodes = state
.values()
.filter(|node| {
node.status == NodeStatus::Alive as i32 // include self
})
.cloned()
.collect::<Vec<NodeState>>();
(self_node.clone(), alive_nodes)
} else {
self.signal_tx.send(()).ok();
return Ok(());
}
};
log::info!(
"Broadcasting LEAVING status to {} alive nodes",
alive_nodes.len()
);
// Broadcast LEAVING status to all alive nodes
let (success_count, total_count) = broadcast_node_states(
vec![leaving_node],
alive_nodes,
Some(Duration::from_secs(3)),
)
.await;
log::info!(
"Broadcast LEAVING status: {}/{} successful",
success_count,
total_count
);
// Wait a bit more for state propagation
let propagation_delay = Duration::from_secs(1);
log::info!(
"Waiting {} seconds for LEAVING status propagation",
propagation_delay.as_secs()
);
tokio::time::sleep(propagation_delay).await;
log::info!("Sending shutdown signal");
self.signal_tx.send(()).ok();
Ok(())
}
pub fn write_data(&self, key: String, value: Vec<u8>) {
let mut state = self.state.write();
state.entry(self.self_name.clone()).and_modify(|e| {
e.version += 1;
e.metadata.insert(key, value);
});
}
pub fn read_data(&self, key: String) -> Option<Vec<u8>> {
let state = self.state.read();
state
.get(&self.self_name)
.and_then(|e| e.metadata.get(&key).cloned())
}
}
pub struct MeshServerBuilder {
state: ClusterState,
self_name: String,
self_addr: SocketAddr,
init_peer: Option<SocketAddr>,
}
impl MeshServerBuilder {
pub fn new(self_name: String, self_addr: SocketAddr, init_peer: Option<SocketAddr>) -> Self {
let state = Arc::new(RwLock::new(BTreeMap::from([(
self_name.clone(),
NodeState {
name: self_name.clone(),
address: self_addr.to_string(),
status: NodeStatus::Alive as i32,
version: 1,
metadata: HashMap::new(),
},
)])));
Self {
state,
self_name,
self_addr,
init_peer,
}
}
pub fn build(&self) -> (MeshServer, MeshServerHandler) {
self.build_with_stores(None)
}
pub fn build_with_stores(
&self,
stores: Option<Arc<super::stores::StateStores>>,
) -> (MeshServer, MeshServerHandler) {
let (signal_tx, signal_rx) = tokio::sync::watch::channel(());
(
MeshServer::new(
self.state.clone(),
&self.self_name,
self.self_addr,
self.init_peer,
signal_rx,
),
MeshServerHandler::with_partition_and_state_machine(
self.state.clone(),
&self.self_name,
self.self_addr,
signal_tx,
stores,
),
)
}
}
pub struct MeshServer {
state: ClusterState,
self_name: String,
self_addr: SocketAddr,
init_peer: Option<SocketAddr>,
signal_rx: tokio::sync::watch::Receiver<()>,
}
impl MeshServer {
pub fn new(
state: ClusterState,
self_name: &str,
self_addr: SocketAddr,
init_peer: Option<SocketAddr>,
signal_rx: tokio::sync::watch::Receiver<()>,
) -> Self {
MeshServer {
state,
self_name: self_name.to_string(),
self_addr,
init_peer,
signal_rx,
}
}
pub fn build_ping_server(&self) -> GossipService {
GossipService::new(self.state.clone(), self.self_addr, &self.self_name)
}
pub fn build_controller(&self) -> MeshController {
MeshController::new(
self.state.clone(),
self.self_addr,
&self.self_name,
self.init_peer,
)
}
pub async fn start_serve(self) -> Result<()> {
log::info!("Mesh server listening on {}", self.self_addr);
let self_name = self.self_name.clone();
let self_address = self.self_addr;
let service = self.build_ping_server();
let controller = self.build_controller();
let mut service_shutdown = self.signal_rx.clone();
let listener = tokio::spawn(service.serve_ping_with_shutdown(async move {
_ = service_shutdown.changed().await;
}));
tokio::time::sleep(Duration::from_secs(1)).await;
let app_handle = tokio::spawn(controller.event_loop(self.signal_rx.clone()));
tokio::select! {
res = listener => res??,
res = app_handle => res??,
}
log::info!(
"Mesh server {} at {} is shutting down",
self_name,
self_address
);
Ok(())
}
pub async fn start_serve_with_stores(
self,
stores: Option<Arc<super::stores::StateStores>>,
sync_manager: Option<Arc<super::sync::MeshSyncManager>>,
partition_detector: Option<Arc<PartitionDetector>>,
) -> Result<()> {
log::info!("Mesh server listening on {}", self.self_addr);
let self_name = self.self_name.clone();
let self_address = self.self_addr;
let mut service = self.build_ping_server();
if let Some(stores) = stores {
service = service.with_stores(stores);
}
if let Some(sync_manager) = sync_manager {
service = service.with_sync_manager(sync_manager);
}
if let Some(partition_detector) = partition_detector {
service = service.with_partition_detector(partition_detector);
}
let controller = self.build_controller();
let mut service_shutdown = self.signal_rx.clone();
let listener = tokio::spawn(service.serve_ping_with_shutdown(async move {
_ = service_shutdown.changed().await;
}));
tokio::time::sleep(Duration::from_secs(1)).await;
let app_handle = tokio::spawn(controller.event_loop(self.signal_rx.clone()));
tokio::select! {
res = listener => res??,
res = app_handle => res??,
}
log::info!(
"Mesh server {} at {} is shutting down",
self_name,
self_address
);
Ok(())
}
}
/// Broadcast node state updates to target nodes
/// Returns (success_count, total_count)
pub async fn broadcast_node_states(
nodes_to_broadcast: Vec<NodeState>,
target_nodes: Vec<NodeState>,
timeout: Option<Duration>,
) -> (usize, usize) {
if nodes_to_broadcast.is_empty() || target_nodes.is_empty() {
log::debug!(
"Nothing to broadcast: nodes_to_broadcast={}, target_nodes={}",
nodes_to_broadcast.len(),
target_nodes.len()
);
return (0, target_nodes.len());
}
let mut broadcast_tasks = Vec::new();
for target_node in &target_nodes {
let target_node_clone = target_node.clone();
let nodes_for_task = nodes_to_broadcast.clone();
let task = tokio::spawn(async move {
let state_sync = StateSync {
nodes: nodes_for_task,
};
let ping_payload = gossip_message::Payload::Ping(Ping {
state_sync: Some(state_sync),
});
match try_ping(&target_node_clone, Some(ping_payload)).await {
Ok(_) => {
log::debug!("Successfully broadcasted to {}", target_node_clone.name);
Ok(())
}
Err(e) => {
log::warn!("Failed to broadcast to {}: {}", target_node_clone.name, e);
Err(e)
}
}
});
broadcast_tasks.push(task);
}
let timeout_duration = timeout.unwrap_or(Duration::from_secs(3));
let broadcast_result = tokio::time::timeout(timeout_duration, async {
futures::future::join_all(broadcast_tasks).await
})
.await;
match broadcast_result {
Ok(results) => {
let success_count = results.iter().filter(|r| r.is_ok()).count();
let total_count = target_nodes.len();
log::info!(
"Broadcast completed: {}/{} successful",
success_count,
total_count
);
(success_count, total_count)
}
Err(_) => {
log::warn!(
"Broadcast timeout after {} seconds",
timeout_duration.as_secs()
);
(0, target_nodes.len())
}
}
}
pub async fn try_ping(
peer_node: &NodeState,
payload: Option<gossip_message::Payload>,
) -> Result<NodeUpdate, tonic::Status> {
let peer_name = peer_node.name.clone();
let peer_addr = SocketAddr::from_str(&peer_node.address).map_err(|e| {
tonic::Status::invalid_argument(format!(
"Invalid address for node {}: {}, {}",
peer_name, peer_node.address, e
))
})?;
let mut client = gossip_client::GossipClient::connect(format!("http://{}", peer_addr))
.await
.map_err(|e| {
log::warn!(
"Failed to connect to peer {} {}: {}.",
peer_name,
peer_addr,
e
);
tonic::Status::unavailable("Failed to connect to peer")
})?;
let ping_message = GossipMessage { payload };
let response = client.ping_server(Request::new(ping_message)).await?;
Ok(response.into_inner())
}
#[macro_export]
macro_rules! mesh_run {
($addr:expr, $init_peer:expr) => {{
mesh_run!($addr.to_string(), $addr, $init_peer)
}};
($name:expr, $addr:expr, $init_peer:expr) => {{
tracing::info!("Starting mesh server : {}", $addr);
let (server, handler) =
$crate::mesh::service::MeshServerBuilder::new($name.to_string(), $addr, $init_peer)
.build();
tokio::spawn(async move {
if let Err(e) = server.start_serve().await {
tracing::error!("Mesh server failed: {}", e);
}
});
handler
}};
}
#[cfg(test)]
mod tests {
use std::sync::Once;
use tokio::net::TcpListener;
use tracing as log;
use tracing_subscriber::{
filter::LevelFilter, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter,
};
use super::*;
static INIT: Once = Once::new();
fn init() {
INIT.call_once(|| {
let _ = tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.with(
EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.from_env_lossy(),
)
.try_init();
});
}
async fn find_free_port() -> (TcpListener, u16) {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
log::info!("Found free port: {}", port);
(listener, port)
}
async fn get_node() -> SocketAddr {
let (_listener, port) = find_free_port().await;
format!("127.0.0.1:{}", port).parse().unwrap()
}
fn print_state(handler: &MeshServerHandler) -> String {
let state = handler.state.read();
let mut res = vec![];
for (k, v) in state.iter() {
res.push(format!(
"{}: {:?} - {:?}",
k,
NodeStatus::try_from(v.status).unwrap(),
v.metadata
));
}
res.join(", ")
}
#[tokio::test]
async fn test_state_synchronization() {
init();
log::info!("Starting test_state_synchronization");
// 1. setup node A and B for initial cluster
let addr_a = get_node().await;
let handler_a = mesh_run!("A", addr_a, None);
let addr_b = get_node().await;
let handler_b = mesh_run!("B", addr_b, Some(addr_a));
// 2. wait for node A and B to sync and write some data
tokio::time::sleep(Duration::from_secs(2)).await;
handler_a.write_data("hello".into(), "world".into());
log::info!("================================================");
// 3. add node C and D and wait for them to sync
let addr_c = get_node().await;
let handler_c = mesh_run!("C", addr_c, Some(addr_a));
let addr_d = get_node().await;
let handler_d = mesh_run!("D", addr_d, Some(addr_c));
tokio::time::sleep(Duration::from_secs(2)).await;
log::info!("================================================");
// 4. add node E and wait for it to sync and kill it
{
let addr_e = get_node().await;
let handler_e = mesh_run!("E", addr_e, Some(addr_d));
tokio::time::sleep(Duration::from_secs(3)).await;
log::info!("State E: {:?}", print_state(&handler_e));
// killing_button.send(()).unwrap();
handler_e.shutdown();
}
handler_d.graceful_shutdown().await.unwrap();
tokio::time::sleep(Duration::from_secs(2)).await;
log::info!("================================================");
// 5. wait for node status to sync
tokio::time::sleep(Duration::from_secs(8)).await;
log::info!("================================================");
// 6. verify node status, status of all nodes should be same, and node E should be down
let final_state = String::from("A: Alive - {\"hello\": [119, 111, 114, 108, 100]}, B: Alive - {}, C: Alive - {}, D: Leaving - {}, E: Down - {}");
assert_eq!(
print_state(&handler_a),
final_state,
"State A: {:?}",
print_state(&handler_a)
);
assert_eq!(
print_state(&handler_b),
final_state,
"State B: {:?}",
print_state(&handler_b)
);
assert_eq!(
print_state(&handler_c),
final_state,
"State C: {:?}",
print_state(&handler_c)
);
}
}

View File

@@ -1,742 +0,0 @@
//! State stores for mesh cluster synchronization
//!
//! Four types of state stores:
//! - MembershipStore: Router node membership
//! - AppStore: Application configuration, rate-limiting rules, LB algorithms
//! - WorkerStore: Worker status, load, health
//! - PolicyStore: Routing policy internal state
use std::{collections::BTreeMap, sync::Arc};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use tracing::debug;
use super::{
consistent_hash::ConsistentHashRing,
crdt::{SKey, SyncCRDTMap, SyncPNCounter},
};
/// Store type identifier
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum StoreType {
Membership,
App,
Worker,
Policy,
RateLimit,
}
impl StoreType {
pub fn as_str(&self) -> &'static str {
match self {
StoreType::Membership => "membership",
StoreType::App => "app",
StoreType::Worker => "worker",
StoreType::Policy => "policy",
StoreType::RateLimit => "rate_limit",
}
}
/// Convert from proto StoreType (i32) to local StoreType
pub fn from_proto(proto_value: i32) -> Self {
match proto_value {
0 => StoreType::Membership,
1 => StoreType::App,
2 => StoreType::Worker,
3 => StoreType::Policy,
4 => StoreType::RateLimit,
_ => StoreType::Membership, // Default fallback
}
}
}
/// Membership state entry
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
pub struct MembershipState {
pub name: String,
pub address: String,
pub status: i32, // NodeStatus enum value
pub version: u64,
pub metadata: BTreeMap<String, Vec<u8>>,
}
/// App state entry (application configuration)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
pub struct AppState {
pub key: String,
pub value: Vec<u8>, // Serialized config
pub version: u64,
}
/// Global rate limit configuration
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct RateLimitConfig {
pub limit_per_second: u64,
}
/// Key for global rate limit configuration in AppStore
pub const GLOBAL_RATE_LIMIT_KEY: &str = "global_rate_limit";
/// Key for global rate limit counter in RateLimitStore
pub const GLOBAL_RATE_LIMIT_COUNTER_KEY: &str = "global";
/// Worker state entry
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct WorkerState {
pub worker_id: String,
pub model_id: String,
pub url: String,
pub health: bool,
pub load: f64,
pub version: u64,
}
// Implement Hash manually for WorkerState (excluding f64)
impl std::hash::Hash for WorkerState {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.worker_id.hash(state);
self.model_id.hash(state);
self.url.hash(state);
self.health.hash(state);
// f64 cannot be hashed directly, use a workaround
(self.load as i64).hash(state);
self.version.hash(state);
}
}
// Implement Eq manually (f64 comparison with epsilon)
impl Eq for WorkerState {}
/// Policy state entry
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
pub struct PolicyState {
pub model_id: String,
pub policy_type: String,
pub config: Vec<u8>, // Serialized policy config
pub version: u64,
}
/// Helper function to get tree state key for a model
pub fn tree_state_key(model_id: &str) -> String {
format!("tree:{}", model_id)
}
/// Membership store
#[derive(Debug, Clone)]
pub struct MembershipStore {
inner: SyncCRDTMap<MembershipState>,
}
impl MembershipStore {
pub fn new() -> Self {
Self {
inner: SyncCRDTMap::new(),
}
}
pub fn get(&self, key: &SKey) -> Option<MembershipState> {
self.inner.get(key)
}
pub fn insert(&self, key: SKey, value: MembershipState, actor: String) {
self.inner.insert(key, value, actor);
}
pub fn remove(&self, key: &SKey) {
self.inner.remove(key);
}
pub fn merge(&self, other: &crate::mesh::crdt::CRDTMap<MembershipState>) {
self.inner.merge(other);
}
pub fn snapshot(&self) -> crate::mesh::crdt::CRDTMap<MembershipState> {
self.inner.snapshot()
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.len() == 0
}
pub fn all(&self) -> BTreeMap<SKey, MembershipState> {
self.inner.snapshot().to_map()
}
pub fn get_metadata(&self, key: &SKey) -> Option<(u64, String)> {
self.inner.get_metadata(key)
}
}
impl Default for MembershipStore {
fn default() -> Self {
Self::new()
}
}
/// App store
#[derive(Debug, Clone)]
pub struct AppStore {
inner: SyncCRDTMap<AppState>,
}
impl AppStore {
pub fn new() -> Self {
Self {
inner: SyncCRDTMap::new(),
}
}
pub fn get(&self, key: &SKey) -> Option<AppState> {
self.inner.get(key)
}
pub fn insert(&self, key: SKey, value: AppState, actor: String) {
self.inner.insert(key, value, actor);
}
pub fn remove(&self, key: &SKey) {
self.inner.remove(key);
}
pub fn merge(&self, other: &crate::mesh::crdt::CRDTMap<AppState>) {
self.inner.merge(other);
}
pub fn snapshot(&self) -> crate::mesh::crdt::CRDTMap<AppState> {
self.inner.snapshot()
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.len() == 0
}
pub fn all(&self) -> BTreeMap<SKey, AppState> {
self.inner.snapshot().to_map()
}
pub fn get_metadata(&self, key: &SKey) -> Option<(u64, String)> {
self.inner.get_metadata(key)
}
}
impl Default for AppStore {
fn default() -> Self {
Self::new()
}
}
/// Worker store
#[derive(Debug, Clone)]
pub struct WorkerStore {
inner: SyncCRDTMap<WorkerState>,
}
impl WorkerStore {
pub fn new() -> Self {
Self {
inner: SyncCRDTMap::new(),
}
}
pub fn get(&self, key: &SKey) -> Option<WorkerState> {
self.inner.get(key)
}
pub fn insert(&self, key: SKey, value: WorkerState, actor: String) {
self.inner.insert(key, value, actor);
}
pub fn remove(&self, key: &SKey) {
self.inner.remove(key);
}
pub fn merge(&self, other: &crate::mesh::crdt::CRDTMap<WorkerState>) {
self.inner.merge(other);
}
pub fn snapshot(&self) -> crate::mesh::crdt::CRDTMap<WorkerState> {
self.inner.snapshot()
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.len() == 0
}
pub fn all(&self) -> BTreeMap<SKey, WorkerState> {
self.inner.snapshot().to_map()
}
pub fn get_metadata(&self, key: &SKey) -> Option<(u64, String)> {
self.inner.get_metadata(key)
}
}
impl Default for WorkerStore {
fn default() -> Self {
Self::new()
}
}
/// Policy store
#[derive(Debug, Clone)]
pub struct PolicyStore {
inner: SyncCRDTMap<PolicyState>,
}
impl PolicyStore {
pub fn new() -> Self {
Self {
inner: SyncCRDTMap::new(),
}
}
pub fn get(&self, key: &SKey) -> Option<PolicyState> {
self.inner.get(key)
}
pub fn insert(&self, key: SKey, value: PolicyState, actor: String) {
self.inner.insert(key, value, actor);
}
pub fn remove(&self, key: &SKey) {
self.inner.remove(key);
}
pub fn merge(&self, other: &crate::mesh::crdt::CRDTMap<PolicyState>) {
self.inner.merge(other);
}
pub fn snapshot(&self) -> crate::mesh::crdt::CRDTMap<PolicyState> {
self.inner.snapshot()
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.len() == 0
}
pub fn all(&self) -> BTreeMap<SKey, PolicyState> {
self.inner.snapshot().to_map()
}
pub fn get_metadata(&self, key: &SKey) -> Option<(u64, String)> {
self.inner.get_metadata(key)
}
}
impl Default for PolicyStore {
fn default() -> Self {
Self::new()
}
}
/// Rate-limit counter store (using PNCounter with consistent hashing)
#[derive(Debug, Clone)]
pub struct RateLimitStore {
counters: Arc<RwLock<BTreeMap<String, SyncPNCounter>>>, // key -> counter
hash_ring: Arc<RwLock<ConsistentHashRing>>,
self_name: String,
}
impl RateLimitStore {
pub fn new(self_name: String) -> Self {
Self {
counters: Arc::new(RwLock::new(BTreeMap::new())),
hash_ring: Arc::new(RwLock::new(ConsistentHashRing::new())),
self_name,
}
}
/// Update the hash ring with current membership
pub fn update_membership(&self, nodes: &[String]) {
let mut ring = self.hash_ring.write();
ring.update_membership(nodes);
debug!("Updated rate-limit hash ring with {} nodes", nodes.len());
}
/// Check if this node is an owner of a key
pub fn is_owner(&self, key: &str) -> bool {
let ring = self.hash_ring.read();
ring.is_owner(key, &self.self_name)
}
/// Get owners for a key
pub fn get_owners(&self, key: &str) -> Vec<String> {
let ring = self.hash_ring.read();
ring.get_owners(key)
.into_iter()
.map(|s| s.to_string())
.collect()
}
/// Get or create counter (only if this node is an owner)
#[allow(dead_code)]
fn get_or_create_counter_internal(&self, key: String) -> Option<SyncPNCounter> {
if !self.is_owner(&key) {
return None;
}
let mut counters = self.counters.write();
Some(counters.entry(key.clone()).or_default().clone())
}
pub fn get_counter(&self, key: &str) -> Option<SyncPNCounter> {
if !self.is_owner(key) {
return None;
}
let counters = self.counters.read();
counters.get(key).cloned()
}
/// Increment counter (only if this node is an owner)
pub fn inc(&self, key: String, actor: String, delta: i64) {
if !self.is_owner(&key) {
// Not an owner, skip
return;
}
let mut counters = self.counters.write();
let counter = counters.entry(key.clone()).or_default();
counter.inc(actor, delta);
}
/// Get counter value (aggregate from all owners via CRDT merge)
pub fn value(&self, key: &str) -> Option<i64> {
let counters = self.counters.read();
counters.get(key).map(|c| c.value())
}
/// Merge counter from another node (for CRDT synchronization)
pub fn merge_counter(&self, key: String, other: &SyncPNCounter) {
let mut counters = self.counters.write();
let counter = counters.entry(key).or_default();
// Get the inner CRDTPNCounter from other SyncPNCounter
let other_inner = other.snapshot();
counter.merge(&other_inner);
}
/// Get all counter keys
pub fn keys(&self) -> Vec<String> {
let counters = self.counters.read();
counters.keys().cloned().collect()
}
/// Check if we need to transfer ownership due to node failure
pub fn check_ownership_transfer(&self, failed_nodes: &[String]) -> Vec<String> {
let mut affected_keys = Vec::new();
let ring = self.hash_ring.read();
let counters = self.counters.read();
for key in counters.keys() {
let owners = ring.get_owners(key);
if owners
.iter()
.any(|&owner| failed_nodes.iter().any(|f| f == owner))
{
// Check if we are now an owner
if ring.is_owner(key, &self.self_name) {
affected_keys.push(key.clone());
}
}
}
affected_keys
}
}
impl Default for RateLimitStore {
fn default() -> Self {
Self::new("default".to_string())
}
}
/// All state stores container
#[derive(Debug, Clone)]
pub struct StateStores {
pub membership: MembershipStore,
pub app: AppStore,
pub worker: WorkerStore,
pub policy: PolicyStore,
pub rate_limit: RateLimitStore,
}
impl StateStores {
pub fn new() -> Self {
Self {
membership: MembershipStore::new(),
app: AppStore::new(),
worker: WorkerStore::new(),
policy: PolicyStore::new(),
rate_limit: RateLimitStore::new("default".to_string()),
}
}
pub fn with_self_name(self_name: String) -> Self {
Self {
membership: MembershipStore::new(),
app: AppStore::new(),
worker: WorkerStore::new(),
policy: PolicyStore::new(),
rate_limit: RateLimitStore::new(self_name),
}
}
}
impl Default for StateStores {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::*;
use crate::mesh::service::gossip::NodeStatus;
#[test]
fn test_membership_store() {
let store = MembershipStore::new();
let key = SKey::new("node1".to_string());
let state = MembershipState {
name: "node1".to_string(),
address: "127.0.0.1:8000".to_string(),
status: NodeStatus::Alive as i32,
version: 1,
metadata: BTreeMap::new(),
};
store.insert(key.clone(), state.clone(), "node1".to_string());
assert_eq!(store.get(&key).unwrap().name, "node1");
assert_eq!(store.len(), 1);
store.remove(&key);
assert!(store.get(&key).is_none());
}
#[test]
fn test_app_store() {
let store = AppStore::new();
let key = SKey::new("app_key1".to_string());
let state = AppState {
key: "app_key1".to_string(),
value: b"app_value".to_vec(),
version: 1,
};
store.insert(key.clone(), state.clone(), "node1".to_string());
assert_eq!(store.get(&key).unwrap().key, "app_key1");
assert_eq!(store.len(), 1);
}
#[test]
fn test_worker_store() {
let store = WorkerStore::new();
let key = SKey::new("worker1".to_string());
let state = WorkerState {
worker_id: "worker1".to_string(),
model_id: "model1".to_string(),
url: "http://localhost:8000".to_string(),
health: true,
load: 0.5,
version: 1,
};
store.insert(key.clone(), state.clone(), "node1".to_string());
assert_eq!(store.get(&key).unwrap().worker_id, "worker1");
assert_eq!(store.len(), 1);
}
#[test]
fn test_policy_store() {
let store = PolicyStore::new();
let key = SKey::new("policy:model1".to_string());
let state = PolicyState {
model_id: "model1".to_string(),
policy_type: "cache_aware".to_string(),
config: b"config_data".to_vec(),
version: 1,
};
store.insert(key.clone(), state.clone(), "node1".to_string());
assert_eq!(store.get(&key).unwrap().model_id, "model1");
assert_eq!(store.len(), 1);
}
#[test]
fn test_rate_limit_store_update_membership() {
let store = RateLimitStore::new("node1".to_string());
store.update_membership(&[
"node1".to_string(),
"node2".to_string(),
"node3".to_string(),
]);
let owners = store.get_owners("test_key");
assert_eq!(owners.len(), 3);
assert!(
owners.contains(&"node1".to_string())
|| owners.contains(&"node2".to_string())
|| owners.contains(&"node3".to_string())
);
}
#[test]
fn test_rate_limit_store_is_owner() {
let store = RateLimitStore::new("node1".to_string());
store.update_membership(&["node1".to_string()]);
let test_key = "test_key".to_string();
let is_owner = store.is_owner(&test_key);
// node1 should be owner since it's the only node
assert!(is_owner);
}
#[test]
fn test_rate_limit_store_inc_only_owner() {
let store = RateLimitStore::new("node1".to_string());
store.update_membership(&["node1".to_string()]);
let test_key = "test_key".to_string();
if store.is_owner(&test_key) {
store.inc(test_key.clone(), "node1".to_string(), 5);
let value = store.value(&test_key);
assert_eq!(value, Some(5));
}
}
#[test]
fn test_rate_limit_store_inc_non_owner() {
let store = RateLimitStore::new("node1".to_string());
// Setup membership without node1 as owner
store.update_membership(&["node2".to_string(), "node3".to_string()]);
let test_key = "test_key".to_string();
if !store.is_owner(&test_key) {
store.inc(test_key.clone(), "node1".to_string(), 5);
// Should not increment if not owner
let value = store.value(&test_key);
assert_eq!(value, None);
}
}
#[test]
fn test_rate_limit_store_merge_counter() {
let store1 = RateLimitStore::new("node1".to_string());
let store2 = RateLimitStore::new("node2".to_string());
store1.update_membership(&["node1".to_string()]);
store2.update_membership(&["node2".to_string()]);
let test_key = "test_key".to_string();
// Both nodes increment their counters
if store1.is_owner(&test_key) {
store1.inc(test_key.clone(), "node1".to_string(), 10);
}
if store2.is_owner(&test_key) {
store2.inc(test_key.clone(), "node2".to_string(), 5);
}
// Merge counter from store2 into store1
if let Some(counter2) = store2.get_counter(&test_key) {
store1.merge_counter(test_key.clone(), &counter2);
}
// Get aggregated value (if node1 is owner)
if store1.is_owner(&test_key) {
let value = store1.value(&test_key);
// Should include merged value
assert!(value.is_some());
}
}
#[test]
fn test_rate_limit_store_check_ownership_transfer() {
let store = RateLimitStore::new("node1".to_string());
store.update_membership(&[
"node1".to_string(),
"node2".to_string(),
"node3".to_string(),
]);
let test_key = "test_key".to_string();
// Setup a counter (if node1 is owner)
if store.is_owner(&test_key) {
store.inc(test_key.clone(), "node1".to_string(), 10);
}
// Check ownership transfer when node2 fails
let affected = store.check_ownership_transfer(&["node2".to_string()]);
// Should detect if node2 was an owner
let _ = affected;
}
#[test]
fn test_rate_limit_store_keys() {
let store = RateLimitStore::new("node1".to_string());
store.update_membership(&["node1".to_string()]);
let key1 = "key1".to_string();
let key2 = "key2".to_string();
if store.is_owner(&key1) {
store.inc(key1.clone(), "node1".to_string(), 1);
}
if store.is_owner(&key2) {
store.inc(key2.clone(), "node1".to_string(), 1);
}
let keys = store.keys();
// Should contain keys where node1 is owner
let _ = keys;
}
#[test]
fn test_state_stores_new() {
let stores = StateStores::new();
assert_eq!(stores.membership.len(), 0);
assert_eq!(stores.app.len(), 0);
assert_eq!(stores.worker.len(), 0);
assert_eq!(stores.policy.len(), 0);
}
#[test]
fn test_state_stores_with_self_name() {
let stores = StateStores::with_self_name("test_node".to_string());
// Rate limit store should have the self_name
let test_key = "test_key".to_string();
stores
.rate_limit
.update_membership(&["test_node".to_string()]);
assert!(stores.rate_limit.is_owner(&test_key));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,86 +0,0 @@
//! Test utilities for mesh module
use std::{
collections::{BTreeMap, HashMap},
sync::Arc,
};
use parking_lot::RwLock;
use super::{
service::{gossip::NodeState, ClusterState},
stores::{MembershipState, StateStores},
sync::MeshSyncManager,
};
/// Create test StateStores with a given node name
pub fn create_test_stores(self_name: String) -> Arc<StateStores> {
Arc::new(StateStores::with_self_name(self_name))
}
/// Create test MeshSyncManager
pub fn create_test_sync_manager(self_name: String) -> Arc<MeshSyncManager> {
let stores = create_test_stores(self_name.clone());
Arc::new(MeshSyncManager::new(stores, self_name))
}
/// Create test cluster state with given nodes
pub fn create_test_cluster_state(
nodes: Vec<(String, String, i32)>, // (name, address, status)
) -> ClusterState {
let mut state = BTreeMap::new();
for (name, address, status) in nodes {
state.insert(
name.clone(),
NodeState {
name: name.clone(),
address,
status,
version: 1,
metadata: HashMap::new(),
},
);
}
Arc::new(RwLock::new(state))
}
/// Create test membership state
#[allow(dead_code)]
pub fn create_test_membership_state(name: String, address: String, status: i32) -> MembershipState {
MembershipState {
name,
address,
status,
version: 1,
metadata: BTreeMap::new(),
}
}
#[cfg(test)]
mod test_utils_tests {
use super::*;
#[test]
fn test_create_test_stores() {
let stores = create_test_stores("test_node".to_string());
assert!(!stores.rate_limit.is_owner("key1"));
}
#[test]
fn test_create_test_sync_manager() {
let manager = create_test_sync_manager("test_node".to_string());
assert_eq!(manager.self_name(), "test_node");
}
#[test]
fn test_create_test_cluster_state() {
let state = create_test_cluster_state(vec![
("node1".to_string(), "127.0.0.1:8000".to_string(), 1),
("node2".to_string(), "127.0.0.1:8001".to_string(), 1),
]);
let read_state = state.read();
assert_eq!(read_state.len(), 2);
assert!(read_state.contains_key("node1"));
assert!(read_state.contains_key("node2"));
}
}

View File

@@ -1,629 +0,0 @@
//! Topology management for mesh cluster
//!
//! Supports:
//! - Full mesh for small/medium clusters
//! - Sparse mesh for large clusters (by region/AZ)
use std::{
collections::{BTreeMap, HashSet},
sync::Arc,
};
use parking_lot::RwLock;
use tracing::debug;
use super::{service::ClusterState, stores::MembershipState};
/// Topology configuration
#[derive(Debug, Clone)]
pub struct TopologyConfig {
/// Maximum nodes for full mesh (beyond this, use sparse)
pub full_mesh_threshold: usize,
/// Region identifier (for sparse mesh)
pub region: Option<String>,
/// Availability zone identifier (for sparse mesh)
pub availability_zone: Option<String>,
}
impl Default for TopologyConfig {
fn default() -> Self {
Self {
full_mesh_threshold: 10,
region: None,
availability_zone: None,
}
}
}
/// Topology manager
pub struct TopologyManager {
config: TopologyConfig,
state: ClusterState,
self_name: String,
/// Active peer connections (for sparse mesh)
active_peers: Arc<RwLock<HashSet<String>>>,
}
impl TopologyManager {
pub fn new(config: TopologyConfig, state: ClusterState, self_name: String) -> Self {
Self {
config,
state,
self_name,
active_peers: Arc::new(RwLock::new(HashSet::new())),
}
}
/// Get peers to connect to based on topology
pub fn get_peers(&self, count: usize) -> Vec<MembershipState> {
let state = self.state.read();
let total_nodes = state.len();
if total_nodes <= self.config.full_mesh_threshold {
// Full mesh: connect to all nodes
self.get_full_mesh_peers(&state, count)
} else {
// Sparse mesh: connect based on region/AZ
self.get_sparse_mesh_peers(&state, count)
}
}
/// Get peers for full mesh topology
fn get_full_mesh_peers(
&self,
state: &BTreeMap<String, super::gossip::NodeState>,
count: usize,
) -> Vec<MembershipState> {
let mut peers = Vec::new();
let active = self.active_peers.read();
for (name, node) in state.iter() {
if name != &self.self_name
&& node.status == super::gossip::NodeStatus::Alive as i32
&& !active.contains(name)
{
let metadata: BTreeMap<String, Vec<u8>> = node
.metadata
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect::<BTreeMap<_, _>>();
peers.push(MembershipState {
name: node.name.clone(),
address: node.address.clone(),
status: node.status,
version: node.version,
metadata,
});
if peers.len() >= count {
break;
}
}
}
peers
}
/// Get peers for sparse mesh topology (by region/AZ)
fn get_sparse_mesh_peers(
&self,
state: &BTreeMap<String, super::gossip::NodeState>,
count: usize,
) -> Vec<MembershipState> {
let mut peers = Vec::new();
let active = self.active_peers.read();
// First, try to connect to nodes in same region/AZ
if let (Some(ref region), Some(ref az)) =
(&self.config.region, &self.config.availability_zone)
{
for (name, node) in state.iter() {
if name != &self.self_name
&& node.status == super::gossip::NodeStatus::Alive as i32
&& !active.contains(name)
{
// Check if node is in same region/AZ (from metadata)
let node_region = node
.metadata
.get("region")
.and_then(|v| String::from_utf8(v.clone()).ok());
let node_az = node
.metadata
.get("availability_zone")
.and_then(|v| String::from_utf8(v.clone()).ok());
if node_region.as_ref() == Some(region) && node_az.as_ref() == Some(az) {
let metadata: BTreeMap<String, Vec<u8>> = node
.metadata
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
peers.push(MembershipState {
name: node.name.clone(),
address: node.address.clone(),
status: node.status,
version: node.version,
metadata,
});
if peers.len() >= count {
break;
}
}
}
}
}
// If not enough peers, add from other regions
if peers.len() < count {
for (name, node) in state.iter() {
if name != &self.self_name
&& node.status == super::gossip::NodeStatus::Alive as i32
&& !active.contains(name)
&& !peers.iter().any(|p| p.name == node.name)
{
let metadata: BTreeMap<String, Vec<u8>> = node
.metadata
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
peers.push(MembershipState {
name: node.name.clone(),
address: node.address.clone(),
status: node.status,
version: node.version,
metadata,
});
if peers.len() >= count {
break;
}
}
}
}
peers
}
/// Mark peer as active
pub fn mark_peer_active(&self, peer_name: &str) {
self.active_peers.write().insert(peer_name.to_string());
debug!("Marked peer {} as active", peer_name);
}
/// Mark peer as inactive
pub fn mark_peer_inactive(&self, peer_name: &str) {
self.active_peers.write().remove(peer_name);
debug!("Marked peer {} as inactive", peer_name);
}
/// Get number of active peers
pub fn active_peer_count(&self) -> usize {
self.active_peers.read().len()
}
/// Check if we should use full mesh
pub fn is_full_mesh(&self) -> bool {
let state = self.state.read();
state.len() <= self.config.full_mesh_threshold
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::*;
use crate::mesh::service::gossip::{NodeState, NodeStatus};
fn create_test_cluster_state(nodes: Vec<(String, String, i32)>) -> ClusterState {
let mut state = BTreeMap::new();
for (name, address, status) in nodes {
state.insert(
name.clone(),
NodeState {
name: name.clone(),
address,
status,
version: 1,
metadata: std::collections::HashMap::new(),
},
);
}
Arc::new(RwLock::new(state))
}
#[test]
fn test_full_mesh_topology() {
let state = create_test_cluster_state(vec![
(
"node1".to_string(),
"127.0.0.1:8000".to_string(),
NodeStatus::Alive as i32,
),
(
"node2".to_string(),
"127.0.0.1:8001".to_string(),
NodeStatus::Alive as i32,
),
(
"node3".to_string(),
"127.0.0.1:8002".to_string(),
NodeStatus::Alive as i32,
),
]);
let config = TopologyConfig {
full_mesh_threshold: 10,
region: None,
availability_zone: None,
};
let manager = TopologyManager::new(config, state, "node1".to_string());
let peers = manager.get_peers(5);
// Should return all available peers (node2 and node3)
assert_eq!(peers.len(), 2);
assert!(peers.iter().any(|p| p.name == "node2"));
assert!(peers.iter().any(|p| p.name == "node3"));
}
#[test]
fn test_full_mesh_topology_excludes_self() {
let state = create_test_cluster_state(vec![
(
"node1".to_string(),
"127.0.0.1:8000".to_string(),
NodeStatus::Alive as i32,
),
(
"node2".to_string(),
"127.0.0.1:8001".to_string(),
NodeStatus::Alive as i32,
),
]);
let config = TopologyConfig {
full_mesh_threshold: 10,
region: None,
availability_zone: None,
};
let manager = TopologyManager::new(config, state, "node1".to_string());
let peers = manager.get_peers(5);
// Should not include self (node1)
assert_eq!(peers.len(), 1);
assert_eq!(peers[0].name, "node2");
}
#[test]
fn test_full_mesh_topology_filters_down_nodes() {
let state = create_test_cluster_state(vec![
(
"node1".to_string(),
"127.0.0.1:8000".to_string(),
NodeStatus::Alive as i32,
),
(
"node2".to_string(),
"127.0.0.1:8001".to_string(),
NodeStatus::Down as i32,
),
(
"node3".to_string(),
"127.0.0.1:8002".to_string(),
NodeStatus::Alive as i32,
),
]);
let config = TopologyConfig {
full_mesh_threshold: 10,
region: None,
availability_zone: None,
};
let manager = TopologyManager::new(config, state, "node1".to_string());
let peers = manager.get_peers(5);
// Should only return alive nodes (node3)
assert_eq!(peers.len(), 1);
assert_eq!(peers[0].name, "node3");
}
#[test]
fn test_sparse_mesh_topology() {
let state = create_test_cluster_state(vec![
(
"node1".to_string(),
"127.0.0.1:8000".to_string(),
NodeStatus::Alive as i32,
),
(
"node2".to_string(),
"127.0.0.1:8001".to_string(),
NodeStatus::Alive as i32,
),
(
"node3".to_string(),
"127.0.0.1:8002".to_string(),
NodeStatus::Alive as i32,
),
(
"node4".to_string(),
"127.0.0.1:8003".to_string(),
NodeStatus::Alive as i32,
),
(
"node5".to_string(),
"127.0.0.1:8004".to_string(),
NodeStatus::Alive as i32,
),
(
"node6".to_string(),
"127.0.0.1:8005".to_string(),
NodeStatus::Alive as i32,
),
(
"node7".to_string(),
"127.0.0.1:8006".to_string(),
NodeStatus::Alive as i32,
),
(
"node8".to_string(),
"127.0.0.1:8007".to_string(),
NodeStatus::Alive as i32,
),
(
"node9".to_string(),
"127.0.0.1:8008".to_string(),
NodeStatus::Alive as i32,
),
(
"node10".to_string(),
"127.0.0.1:8009".to_string(),
NodeStatus::Alive as i32,
),
(
"node11".to_string(),
"127.0.0.1:8010".to_string(),
NodeStatus::Alive as i32,
),
]);
let config = TopologyConfig {
full_mesh_threshold: 10, // 11 nodes > 10, should use sparse
region: None,
availability_zone: None,
};
let manager = TopologyManager::new(config, state, "node1".to_string());
let peers = manager.get_peers(5);
// Should return peers (sparse mesh mode)
assert!(!peers.is_empty());
assert!(peers.len() <= 5);
}
#[test]
fn test_sparse_mesh_with_region_az() {
let mut state_map = BTreeMap::new();
// Create nodes with region/AZ metadata
let mut node1_metadata = std::collections::HashMap::new();
node1_metadata.insert("region".to_string(), b"us-west".to_vec());
node1_metadata.insert("availability_zone".to_string(), b"us-west-1a".to_vec());
state_map.insert(
"node1".to_string(),
NodeState {
name: "node1".to_string(),
address: "127.0.0.1:8000".to_string(),
status: NodeStatus::Alive as i32,
version: 1,
metadata: node1_metadata.clone(),
},
);
let mut node2_metadata = std::collections::HashMap::new();
node2_metadata.insert("region".to_string(), b"us-west".to_vec());
node2_metadata.insert("availability_zone".to_string(), b"us-west-1a".to_vec());
state_map.insert(
"node2".to_string(),
NodeState {
name: "node2".to_string(),
address: "127.0.0.1:8001".to_string(),
status: NodeStatus::Alive as i32,
version: 1,
metadata: node2_metadata,
},
);
let mut node3_metadata = std::collections::HashMap::new();
node3_metadata.insert("region".to_string(), b"us-east".to_vec());
node3_metadata.insert("availability_zone".to_string(), b"us-east-1a".to_vec());
state_map.insert(
"node3".to_string(),
NodeState {
name: "node3".to_string(),
address: "127.0.0.1:8002".to_string(),
status: NodeStatus::Alive as i32,
version: 1,
metadata: node3_metadata,
},
);
let state = Arc::new(RwLock::new(state_map));
let config = TopologyConfig {
full_mesh_threshold: 2,
region: Some("us-west".to_string()),
availability_zone: Some("us-west-1a".to_string()),
};
let manager = TopologyManager::new(config, state, "node1".to_string());
let peers = manager.get_peers(5);
// Should prefer nodes in same region/AZ (node2)
assert!(!peers.is_empty());
// node2 should be in the list (same region/AZ)
assert!(peers.iter().any(|p| p.name == "node2"));
}
#[test]
fn test_mark_peer_active_inactive() {
let state = create_test_cluster_state(vec![
(
"node1".to_string(),
"127.0.0.1:8000".to_string(),
NodeStatus::Alive as i32,
),
(
"node2".to_string(),
"127.0.0.1:8001".to_string(),
NodeStatus::Alive as i32,
),
]);
let config = TopologyConfig {
full_mesh_threshold: 10,
region: None,
availability_zone: None,
};
let manager = TopologyManager::new(config, state, "node1".to_string());
assert_eq!(manager.active_peer_count(), 0);
manager.mark_peer_active("node2");
assert_eq!(manager.active_peer_count(), 1);
manager.mark_peer_inactive("node2");
assert_eq!(manager.active_peer_count(), 0);
}
#[test]
fn test_get_peers_excludes_active_peers() {
let state = create_test_cluster_state(vec![
(
"node1".to_string(),
"127.0.0.1:8000".to_string(),
NodeStatus::Alive as i32,
),
(
"node2".to_string(),
"127.0.0.1:8001".to_string(),
NodeStatus::Alive as i32,
),
(
"node3".to_string(),
"127.0.0.1:8002".to_string(),
NodeStatus::Alive as i32,
),
]);
let config = TopologyConfig {
full_mesh_threshold: 10,
region: None,
availability_zone: None,
};
let manager = TopologyManager::new(config, state, "node1".to_string());
manager.mark_peer_active("node2");
let peers = manager.get_peers(5);
// Should exclude node2 (already active)
assert!(!peers.iter().any(|p| p.name == "node2"));
// Should include node3
assert!(peers.iter().any(|p| p.name == "node3"));
}
#[test]
fn test_is_full_mesh() {
let state = create_test_cluster_state(vec![
(
"node1".to_string(),
"127.0.0.1:8000".to_string(),
NodeStatus::Alive as i32,
),
(
"node2".to_string(),
"127.0.0.1:8001".to_string(),
NodeStatus::Alive as i32,
),
]);
let config = TopologyConfig {
full_mesh_threshold: 10,
region: None,
availability_zone: None,
};
let manager = TopologyManager::new(config, state, "node1".to_string());
assert!(manager.is_full_mesh());
let state2 = create_test_cluster_state(vec![
(
"node1".to_string(),
"127.0.0.1:8000".to_string(),
NodeStatus::Alive as i32,
),
(
"node2".to_string(),
"127.0.0.1:8001".to_string(),
NodeStatus::Alive as i32,
),
(
"node3".to_string(),
"127.0.0.1:8002".to_string(),
NodeStatus::Alive as i32,
),
(
"node4".to_string(),
"127.0.0.1:8003".to_string(),
NodeStatus::Alive as i32,
),
(
"node5".to_string(),
"127.0.0.1:8004".to_string(),
NodeStatus::Alive as i32,
),
(
"node6".to_string(),
"127.0.0.1:8005".to_string(),
NodeStatus::Alive as i32,
),
(
"node7".to_string(),
"127.0.0.1:8006".to_string(),
NodeStatus::Alive as i32,
),
(
"node8".to_string(),
"127.0.0.1:8007".to_string(),
NodeStatus::Alive as i32,
),
(
"node9".to_string(),
"127.0.0.1:8008".to_string(),
NodeStatus::Alive as i32,
),
(
"node10".to_string(),
"127.0.0.1:8009".to_string(),
NodeStatus::Alive as i32,
),
(
"node11".to_string(),
"127.0.0.1:8010".to_string(),
NodeStatus::Alive as i32,
),
]);
let config2 = TopologyConfig {
full_mesh_threshold: 10,
region: None,
availability_zone: None,
};
let manager2 = TopologyManager::new(config2, state2, "node1".to_string());
assert!(!manager2.is_full_mesh());
}
}

View File

@@ -1,274 +0,0 @@
//! Tree operation definitions for mesh synchronization
//!
//! Defines serializable tree operations that can be synchronized across mesh cluster nodes
use serde::{Deserialize, Serialize};
/// Tree insert operation
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct TreeInsertOp {
pub text: String,
pub tenant: String, // worker URL
}
/// Tree remove operation
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct TreeRemoveOp {
pub tenant: String, // worker URL
}
/// Tree operation type
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum TreeOperation {
Insert(TreeInsertOp),
Remove(TreeRemoveOp),
}
/// Tree state for a specific model
/// Contains a sequence of operations that can be applied to reconstruct the tree
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
pub struct TreeState {
pub model_id: String,
pub operations: Vec<TreeOperation>,
pub version: u64,
}
impl TreeState {
pub fn new(model_id: String) -> Self {
Self {
model_id,
operations: Vec::new(),
version: 0,
}
}
pub fn add_operation(&mut self, operation: TreeOperation) {
self.operations.push(operation);
self.version += 1;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tree_insert_op_creation() {
let op = TreeInsertOp {
text: "test_text".to_string(),
tenant: "http://worker1:8000".to_string(),
};
assert_eq!(op.text, "test_text");
assert_eq!(op.tenant, "http://worker1:8000");
}
#[test]
fn test_tree_remove_op_creation() {
let op = TreeRemoveOp {
tenant: "http://worker1:8000".to_string(),
};
assert_eq!(op.tenant, "http://worker1:8000");
}
#[test]
fn test_tree_operation_insert() {
let insert_op = TreeInsertOp {
text: "test_text".to_string(),
tenant: "http://worker1:8000".to_string(),
};
let operation = TreeOperation::Insert(insert_op.clone());
match &operation {
TreeOperation::Insert(op) => {
assert_eq!(op.text, "test_text");
assert_eq!(op.tenant, "http://worker1:8000");
}
TreeOperation::Remove(_) => panic!("Expected Insert operation"),
}
}
#[test]
fn test_tree_operation_remove() {
let remove_op = TreeRemoveOp {
tenant: "http://worker1:8000".to_string(),
};
let operation = TreeOperation::Remove(remove_op.clone());
match &operation {
TreeOperation::Insert(_) => panic!("Expected Remove operation"),
TreeOperation::Remove(op) => {
assert_eq!(op.tenant, "http://worker1:8000");
}
}
}
#[test]
fn test_tree_operation_serialization() {
let insert_op = TreeInsertOp {
text: "test_text".to_string(),
tenant: "http://worker1:8000".to_string(),
};
let operation = TreeOperation::Insert(insert_op);
let serialized = serde_json::to_string(&operation).unwrap();
let deserialized: TreeOperation = serde_json::from_str(&serialized).unwrap();
match (&operation, &deserialized) {
(TreeOperation::Insert(a), TreeOperation::Insert(b)) => {
assert_eq!(a.text, b.text);
assert_eq!(a.tenant, b.tenant);
}
_ => panic!("Operations should match"),
}
}
#[test]
fn test_tree_operation_remove_serialization() {
let remove_op = TreeRemoveOp {
tenant: "http://worker1:8000".to_string(),
};
let operation = TreeOperation::Remove(remove_op);
let serialized = serde_json::to_string(&operation).unwrap();
let deserialized: TreeOperation = serde_json::from_str(&serialized).unwrap();
match (&operation, &deserialized) {
(TreeOperation::Remove(a), TreeOperation::Remove(b)) => {
assert_eq!(a.tenant, b.tenant);
}
_ => panic!("Operations should match"),
}
}
#[test]
fn test_tree_state_new() {
let state = TreeState::new("model1".to_string());
assert_eq!(state.model_id, "model1");
assert_eq!(state.operations.len(), 0);
assert_eq!(state.version, 0);
}
#[test]
fn test_tree_state_default() {
let state = TreeState::default();
assert_eq!(state.model_id, "");
assert_eq!(state.operations.len(), 0);
assert_eq!(state.version, 0);
}
#[test]
fn test_tree_state_add_operation() {
let mut state = TreeState::new("model1".to_string());
let insert_op = TreeInsertOp {
text: "text1".to_string(),
tenant: "http://worker1:8000".to_string(),
};
state.add_operation(TreeOperation::Insert(insert_op));
assert_eq!(state.operations.len(), 1);
assert_eq!(state.version, 1);
let remove_op = TreeRemoveOp {
tenant: "http://worker1:8000".to_string(),
};
state.add_operation(TreeOperation::Remove(remove_op));
assert_eq!(state.operations.len(), 2);
assert_eq!(state.version, 2);
}
#[test]
fn test_tree_state_add_multiple_operations() {
let mut state = TreeState::new("model1".to_string());
for i in 0..5 {
let insert_op = TreeInsertOp {
text: format!("text_{}", i),
tenant: format!("http://worker{}:8000", i),
};
state.add_operation(TreeOperation::Insert(insert_op));
}
assert_eq!(state.operations.len(), 5);
assert_eq!(state.version, 5);
}
#[test]
fn test_tree_state_serialization() {
let mut state = TreeState::new("model1".to_string());
let insert_op = TreeInsertOp {
text: "test_text".to_string(),
tenant: "http://worker1:8000".to_string(),
};
state.add_operation(TreeOperation::Insert(insert_op));
let remove_op = TreeRemoveOp {
tenant: "http://worker1:8000".to_string(),
};
state.add_operation(TreeOperation::Remove(remove_op));
let serialized = serde_json::to_string(&state).unwrap();
let deserialized: TreeState = serde_json::from_str(&serialized).unwrap();
assert_eq!(state.model_id, deserialized.model_id);
assert_eq!(state.operations.len(), deserialized.operations.len());
assert_eq!(state.version, deserialized.version);
}
#[test]
fn test_tree_state_clone() {
let mut state = TreeState::new("model1".to_string());
let insert_op = TreeInsertOp {
text: "test_text".to_string(),
tenant: "http://worker1:8000".to_string(),
};
state.add_operation(TreeOperation::Insert(insert_op));
let cloned = state.clone();
assert_eq!(state.model_id, cloned.model_id);
assert_eq!(state.operations.len(), cloned.operations.len());
assert_eq!(state.version, cloned.version);
}
#[test]
fn test_tree_state_equality() {
let mut state1 = TreeState::new("model1".to_string());
let mut state2 = TreeState::new("model1".to_string());
let insert_op = TreeInsertOp {
text: "test_text".to_string(),
tenant: "http://worker1:8000".to_string(),
};
state1.add_operation(TreeOperation::Insert(insert_op.clone()));
state2.add_operation(TreeOperation::Insert(insert_op));
assert_eq!(state1, state2);
}
#[test]
fn test_tree_operation_hash() {
use std::collections::HashSet;
let insert_op1 = TreeInsertOp {
text: "text1".to_string(),
tenant: "http://worker1:8000".to_string(),
};
let insert_op2 = TreeInsertOp {
text: "text1".to_string(),
tenant: "http://worker1:8000".to_string(),
};
let op1 = TreeOperation::Insert(insert_op1);
let op2 = TreeOperation::Insert(insert_op2);
let mut set = HashSet::new();
set.insert(op1.clone());
set.insert(op2.clone());
// Same operations should be considered equal
assert_eq!(set.len(), 1);
}
}

View File

@@ -1,10 +1,12 @@
//! Mesh management endpoints
//! Mesh management HTTP handlers
//!
//! Provides REST API for mesh cluster management:
//! - Configuration CRUD operations
//! - Health checks
//! - Cluster status
use std::sync::Arc;
use axum::{
extract::{Path, State},
http::StatusCode,
@@ -13,8 +15,11 @@ use axum::{
};
use serde::{Deserialize, Serialize};
use serde_json::json;
use smg_mesh::{RateLimitConfig, GLOBAL_RATE_LIMIT_COUNTER_KEY, GLOBAL_RATE_LIMIT_KEY};
use tracing::{info, warn};
use crate::server::AppState;
/// Mesh cluster status response
#[derive(Debug, Serialize, Deserialize)]
pub struct ClusterStatusResponse {
@@ -392,7 +397,7 @@ pub async fn get_global_rate_limit_stats(State(app_state): State<Arc<AppState>>)
// Get current counter value
let current_count = sync_manager
.get_rate_limit_value(crate::mesh::stores::GLOBAL_RATE_LIMIT_COUNTER_KEY)
.get_rate_limit_value(GLOBAL_RATE_LIMIT_COUNTER_KEY)
.unwrap_or(0);
(
@@ -434,10 +439,3 @@ pub async fn trigger_graceful_shutdown(State(app_state): State<Arc<AppState>>) -
)
.into_response()
}
use std::sync::Arc;
use crate::{
mesh::stores::{RateLimitConfig, GLOBAL_RATE_LIMIT_KEY},
server::AppState,
};

View File

@@ -0,0 +1,7 @@
//! Mesh cluster management HTTP handlers
//!
//! This module provides HTTP API endpoints for mesh cluster management.
mod handlers;
pub use handlers::*;

View File

@@ -27,6 +27,7 @@ pub mod grpc;
pub mod header_utils;
pub mod http;
pub mod mcp_utils;
pub mod mesh;
pub mod openai;
pub mod parse;
pub mod persistence_utils;

View File

@@ -16,6 +16,9 @@ use axum::{
use rustls::crypto::ring;
use serde::Deserialize;
use serde_json::{json, Value};
use smg_mesh::{
rate_limit_window::RateLimitWindow, MeshServerConfig, MeshServerHandler, MeshSyncManager,
};
use tokio::{signal, spawn};
use tracing::{debug, error, info, warn, Level};
@@ -29,16 +32,6 @@ use crate::{
worker_manager::WorkerManager,
Job,
},
mesh::{
endpoints::{
get_app_config, get_cluster_status, get_global_rate_limit, get_global_rate_limit_stats,
get_mesh_health, get_policy_state, get_policy_states, get_worker_state,
get_worker_states, set_global_rate_limit, trigger_graceful_shutdown, update_app_config,
},
rate_limit_window::RateLimitWindow,
service::{MeshServerConfig, MeshServerHandler},
sync::MeshSyncManager,
},
middleware::{self, AuthConfig, QueuedRequest},
observability::{
logging::{self, LoggingConfig},
@@ -58,7 +51,17 @@ use crate::{
validated::ValidatedJson,
worker_spec::{WorkerConfigRequest, WorkerUpdateRequest},
},
routers::{conversations, parse, router_manager::RouterManager, tokenize, RouterTrait},
routers::{
conversations,
mesh::{
get_app_config, get_cluster_status, get_global_rate_limit, get_global_rate_limit_stats,
get_mesh_health, get_policy_state, get_policy_states, get_worker_state,
get_worker_states, set_global_rate_limit, trigger_graceful_shutdown, update_app_config,
},
parse,
router_manager::RouterManager,
tokenize, RouterTrait,
},
service_discovery::{start_service_discovery, ServiceDiscoveryConfig},
tokenizer::TokenizerRegistry,
wasm::route::{add_wasm_module, list_wasm_modules, remove_wasm_module},

View File

@@ -1,384 +0,0 @@
//! Integration tests for mesh functionality
//!
//! Tests multi-node scenarios including state synchronization,
//! rate limiting, and cache-aware routing across cluster nodes.
use std::sync::Arc;
use smg::mesh::{
crdt::SKey,
gossip::NodeStatus,
stores::{
AppState, MembershipState, RateLimitConfig, StateStores, WorkerState,
GLOBAL_RATE_LIMIT_COUNTER_KEY, GLOBAL_RATE_LIMIT_KEY,
},
sync::MeshSyncManager,
tree_ops::{TreeInsertOp, TreeOperation},
};
/// Create test stores for a node
fn create_test_stores(node_name: String) -> Arc<StateStores> {
Arc::new(StateStores::with_self_name(node_name))
}
/// Create test sync manager for a node
fn create_test_sync_manager(node_name: String) -> Arc<MeshSyncManager> {
let stores = create_test_stores(node_name.clone());
Arc::new(MeshSyncManager::new(stores, node_name))
}
#[tokio::test]
async fn test_multi_node_state_synchronization() {
// Create three nodes
let manager1 = create_test_sync_manager("node1".to_string());
let manager2 = create_test_sync_manager("node2".to_string());
let manager3 = create_test_sync_manager("node3".to_string());
// Node1 syncs a worker state
manager1.sync_worker_state(
"worker1".to_string(),
"model1".to_string(),
"http://localhost:8000".to_string(),
true,
0.5,
);
// Simulate synchronization: Node2 and Node3 receive the update
let worker_state = manager1.get_worker_state("worker1").unwrap();
manager2.apply_remote_worker_state(worker_state.clone(), Some("node1".to_string()));
manager3.apply_remote_worker_state(worker_state, Some("node1".to_string()));
// Verify all nodes have the same state
let state1 = manager1.get_worker_state("worker1").unwrap();
let state2 = manager2.get_worker_state("worker1").unwrap();
let state3 = manager3.get_worker_state("worker1").unwrap();
assert_eq!(state1.worker_id, state2.worker_id);
assert_eq!(state2.worker_id, state3.worker_id);
assert_eq!(state1.version, state2.version);
assert_eq!(state2.version, state3.version);
}
#[tokio::test]
async fn test_node_join_and_leave() {
let manager1 = create_test_sync_manager("node1".to_string());
let manager2 = create_test_sync_manager("node2".to_string());
// Node1 has some state
manager1.sync_worker_state(
"worker1".to_string(),
"model1".to_string(),
"http://localhost:8000".to_string(),
true,
0.5,
);
manager1.sync_policy_state(
"model1".to_string(),
"cache_aware".to_string(),
b"config".to_vec(),
);
// Node2 joins and receives state
let worker_state = manager1.get_worker_state("worker1").unwrap();
manager2.apply_remote_worker_state(worker_state, Some("node1".to_string()));
let policy_state = manager1.get_policy_state("model1").unwrap();
manager2.apply_remote_policy_state(policy_state, Some("node1".to_string()));
// Verify Node2 has the state
assert!(manager2.get_worker_state("worker1").is_some());
assert!(manager2.get_policy_state("model1").is_some());
// Node1 removes worker
manager1.remove_worker_state("worker1");
// In a real scenario, this would be propagated via gossip
// For test, we verify the removal happened
assert!(manager1.get_worker_state("worker1").is_none());
}
#[tokio::test]
async fn test_rate_limit_cluster_consistency() {
// Create stores and managers
let stores1 = create_test_stores("node1".to_string());
let stores2 = create_test_stores("node2".to_string());
let stores3 = create_test_stores("node3".to_string());
// Add all nodes to membership store (required for rate limit hash ring)
let node_names = ["node1", "node2", "node3"];
let node_addresses = ["127.0.0.1:8001", "127.0.0.1:8002", "127.0.0.1:8003"];
for stores in [&stores1, &stores2, &stores3] {
for (i, &name) in node_names.iter().enumerate() {
let key = SKey::new(name.to_string());
stores.membership.insert(
key,
MembershipState {
name: name.to_string(),
address: node_addresses[i].to_string(),
status: NodeStatus::Alive as i32,
version: 1,
metadata: std::collections::BTreeMap::new(),
},
name.to_string(),
);
}
}
// Setup global rate limit config
let config = RateLimitConfig {
limit_per_second: 100,
};
let serialized = serde_json::to_vec(&config).unwrap();
let key = SKey::new(GLOBAL_RATE_LIMIT_KEY.to_string());
for stores in [&stores1, &stores2, &stores3] {
stores.app.insert(
key.clone(),
AppState {
key: GLOBAL_RATE_LIMIT_KEY.to_string(),
value: serialized.clone(),
version: 1,
},
"node1".to_string(),
);
}
// Create managers with updated stores
let manager1 = Arc::new(MeshSyncManager::new(stores1.clone(), "node1".to_string()));
let manager2 = Arc::new(MeshSyncManager::new(stores2.clone(), "node2".to_string()));
let manager3 = Arc::new(MeshSyncManager::new(stores3.clone(), "node3".to_string()));
// Update rate limit membership (reads from membership store)
manager1.update_rate_limit_membership();
manager2.update_rate_limit_membership();
manager3.update_rate_limit_membership();
// Each node increments the counter (if it's an owner)
let test_key = GLOBAL_RATE_LIMIT_COUNTER_KEY.to_string();
manager1.sync_rate_limit_inc(test_key.clone(), 10);
manager2.sync_rate_limit_inc(test_key.clone(), 5);
manager3.sync_rate_limit_inc(test_key.clone(), 3);
// Simulate counter merging (in real scenario, this happens via gossip)
// Get counters from each node and merge them into all nodes
if let Some(counter2) = stores2.rate_limit.get_counter(&test_key) {
manager1.apply_remote_rate_limit_counter(test_key.clone(), &counter2);
manager3.apply_remote_rate_limit_counter(test_key.clone(), &counter2);
}
if let Some(counter3) = stores3.rate_limit.get_counter(&test_key) {
manager1.apply_remote_rate_limit_counter(test_key.clone(), &counter3);
manager2.apply_remote_rate_limit_counter(test_key.clone(), &counter3);
}
if let Some(counter1) = stores1.rate_limit.get_counter(&test_key) {
manager2.apply_remote_rate_limit_counter(test_key.clone(), &counter1);
manager3.apply_remote_rate_limit_counter(test_key.clone(), &counter1);
}
// Check aggregated value
let value = manager1.get_rate_limit_value(&test_key);
// Should have aggregated value from all owners
assert!(value.is_some());
// The value should be the sum of all increments (10 + 5 + 3 = 18)
// But note: only owners actually increment, so the sum depends on ownership
let value = value.unwrap();
assert!(value > 0, "Counter value should be greater than 0");
}
#[tokio::test]
async fn test_rate_limit_node_failure() {
let manager1 = create_test_sync_manager("node1".to_string());
let _manager2 = create_test_sync_manager("node2".to_string());
let _manager3 = create_test_sync_manager("node3".to_string());
// Setup membership through sync manager
// In a real scenario, membership would be updated through gossip protocol
manager1.update_rate_limit_membership();
// Simulate node2 failure
manager1.handle_node_failure(&["node2".to_string()]);
// Update membership to reflect failure
manager1.update_rate_limit_membership();
// Verify system continues to work
let test_key = "test_key".to_string();
manager1.sync_rate_limit_inc(test_key.clone(), 1);
let _value = manager1.get_rate_limit_value(&test_key);
// Value may be None if not owner, which is acceptable
// In a real scenario, ownership would be redistributed after node failure
}
#[tokio::test]
async fn test_cache_aware_tree_synchronization() {
let manager1 = create_test_sync_manager("node1".to_string());
let manager2 = create_test_sync_manager("node2".to_string());
// Node1 syncs tree operations
let op1 = TreeOperation::Insert(TreeInsertOp {
text: "request1".to_string(),
tenant: "http://worker1:8000".to_string(),
});
manager1
.sync_tree_operation("model1".to_string(), op1)
.unwrap();
let op2 = TreeOperation::Insert(TreeInsertOp {
text: "request2".to_string(),
tenant: "http://worker2:8000".to_string(),
});
manager1
.sync_tree_operation("model1".to_string(), op2)
.unwrap();
// Node2 receives tree state (simulated)
let tree_state = manager1.get_tree_state("model1").unwrap();
manager2.apply_remote_tree_operation(
"model1".to_string(),
tree_state,
Some("node1".to_string()),
);
// Verify Node2 has the tree state
let tree_state2 = manager2.get_tree_state("model1");
assert!(tree_state2.is_some());
let tree = tree_state2.unwrap();
assert_eq!(tree.operations.len(), 2);
}
#[tokio::test]
async fn test_version_conflict_resolution() {
let manager1 = create_test_sync_manager("node1".to_string());
let manager2 = create_test_sync_manager("node2".to_string());
// Both nodes update the same worker with different versions
manager1.sync_worker_state(
"worker1".to_string(),
"model1".to_string(),
"http://localhost:8000".to_string(),
true,
0.5,
);
// Node2 tries to apply an older version
let old_state = WorkerState {
worker_id: "worker1".to_string(),
model_id: "model1".to_string(),
url: "http://localhost:8000".to_string(),
health: false,
load: 0.8,
version: 0, // Older version
};
manager2.apply_remote_worker_state(old_state, Some("node2".to_string()));
// Node2 should not have the state (version too old)
// But if it does, it should have version 0
let state2 = manager2.get_worker_state("worker1");
if let Some(s) = state2 {
// If state exists, it should be from node1 (version 1)
assert!(s.version >= 1);
}
// Node1 applies newer version to Node2
let new_state = manager1.get_worker_state("worker1").unwrap();
manager2.apply_remote_worker_state(new_state, Some("node1".to_string()));
// Now Node2 should have the correct state
let final_state = manager2.get_worker_state("worker1").unwrap();
assert_eq!(final_state.version, 1);
assert!(final_state.health);
}
#[tokio::test]
async fn test_concurrent_updates() {
let manager1 = create_test_sync_manager("node1".to_string());
let manager2 = create_test_sync_manager("node2".to_string());
let manager3 = create_test_sync_manager("node3".to_string());
// All nodes update different workers concurrently
manager1.sync_worker_state(
"worker1".to_string(),
"model1".to_string(),
"http://localhost:8000".to_string(),
true,
0.5,
);
manager2.sync_worker_state(
"worker2".to_string(),
"model1".to_string(),
"http://localhost:8001".to_string(),
true,
0.6,
);
manager3.sync_worker_state(
"worker3".to_string(),
"model1".to_string(),
"http://localhost:8002".to_string(),
true,
0.7,
);
// Simulate synchronization: all nodes receive all updates
let worker1_state = manager1.get_worker_state("worker1").unwrap();
let worker2_state = manager2.get_worker_state("worker2").unwrap();
let worker3_state = manager3.get_worker_state("worker3").unwrap();
manager2.apply_remote_worker_state(worker1_state.clone(), Some("node1".to_string()));
manager3.apply_remote_worker_state(worker1_state, Some("node1".to_string()));
manager1.apply_remote_worker_state(worker2_state.clone(), Some("node2".to_string()));
manager3.apply_remote_worker_state(worker2_state, Some("node2".to_string()));
manager1.apply_remote_worker_state(worker3_state.clone(), Some("node3".to_string()));
manager2.apply_remote_worker_state(worker3_state, Some("node3".to_string()));
// All nodes should have all workers
assert_eq!(manager1.get_all_worker_states().len(), 3);
assert_eq!(manager2.get_all_worker_states().len(), 3);
assert_eq!(manager3.get_all_worker_states().len(), 3);
}
#[tokio::test]
async fn test_rate_limit_window_reset() {
let manager = create_test_sync_manager("node1".to_string());
// Setup membership
manager.update_rate_limit_membership();
// Setup config through stores (for testing)
let stores = create_test_stores("node1".to_string());
let config = RateLimitConfig {
limit_per_second: 100,
};
let serialized = serde_json::to_vec(&config).unwrap();
let key = SKey::new(GLOBAL_RATE_LIMIT_KEY.to_string());
stores.app.insert(
key,
AppState {
key: GLOBAL_RATE_LIMIT_KEY.to_string(),
value: serialized,
version: 1,
},
"node1".to_string(),
);
// Recreate manager with updated stores
let manager = Arc::new(MeshSyncManager::new(stores, "node1".to_string()));
// Increment counter (if owner)
manager.sync_rate_limit_inc(GLOBAL_RATE_LIMIT_COUNTER_KEY.to_string(), 50);
let value_before = manager.get_rate_limit_value(GLOBAL_RATE_LIMIT_COUNTER_KEY);
// Value may be None if not owner, or Some if owner
if let Some(val) = value_before {
assert!(val > 0);
// Reset counter
manager.reset_global_rate_limit_counter();
let value_after = manager.get_rate_limit_value(GLOBAL_RATE_LIMIT_COUNTER_KEY);
// Should be reset
assert!(value_after.is_none() || value_after.unwrap_or(0) <= 0);
}
}