Files
sglang/sgl-model-gateway/src/mesh/ping_server.rs
T
Tony LuandKun 5938c3b06a [model-gateway] HA - Lightweight State Layer + gRPC Mesh (#14108)
Signed-off-by: Kun(llfl) <i@imux.top>
Signed-off-by: Tony Lu <tonylu@linux.alibaba.com>
Co-authored-by: Kun(llfl) <i@imux.top>
2026-01-13 17:03:39 -08:00

965 lines
50 KiB
Rust

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
))
}
}