use crate::error::Result;
use crate::partition::{PartitionConfig, PartitionId, Partitioner};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::mpsc;
pub type WorkerId = String;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandEnvelope {
pub id: String,
pub command_type: String,
pub lane_id: String,
pub partition_id: PartitionId,
pub payload: serde_json::Value,
pub retry_count: u32,
pub created_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandResult {
pub command_id: String,
pub result: std::result::Result<serde_json::Value, String>,
pub worker_id: WorkerId,
pub duration_ms: u64,
}
#[async_trait]
pub trait DistributedQueue: Send + Sync {
async fn enqueue(&self, envelope: CommandEnvelope) -> Result<()>;
async fn dequeue(&self, partition_id: PartitionId) -> Result<Option<CommandEnvelope>>;
async fn complete(&self, result: CommandResult) -> Result<()>;
fn num_partitions(&self) -> usize;
fn worker_id(&self) -> &WorkerId;
fn is_coordinator(&self) -> bool;
fn is_worker(&self) -> bool;
}
pub struct LocalDistributedQueue {
worker_id: WorkerId,
partition_config: PartitionConfig,
partitioner: Arc<dyn Partitioner>,
partition_senders: Vec<mpsc::Sender<CommandEnvelope>>,
partition_receivers: Vec<Arc<tokio::sync::Mutex<mpsc::Receiver<CommandEnvelope>>>>,
result_sender: mpsc::Sender<CommandResult>,
result_receiver: Arc<tokio::sync::Mutex<mpsc::Receiver<CommandResult>>>,
}
impl LocalDistributedQueue {
pub fn new(partition_config: PartitionConfig) -> Self {
let num_partitions = partition_config.num_partitions;
let partitioner = partition_config.create_partitioner();
let mut partition_senders = Vec::with_capacity(num_partitions);
let mut partition_receivers = Vec::with_capacity(num_partitions);
for _ in 0..num_partitions {
let (tx, rx) = mpsc::channel(1000); partition_senders.push(tx);
partition_receivers.push(Arc::new(tokio::sync::Mutex::new(rx)));
}
let (result_tx, result_rx) = mpsc::channel(1000);
Self {
worker_id: format!("local-{}", uuid::Uuid::new_v4()),
partition_config,
partitioner,
partition_senders,
partition_receivers,
result_sender: result_tx,
result_receiver: Arc::new(tokio::sync::Mutex::new(result_rx)),
}
}
pub fn auto() -> Self {
Self::new(PartitionConfig::auto())
}
pub fn partitioner(&self) -> &Arc<dyn Partitioner> {
&self.partitioner
}
pub fn partition_config(&self) -> &PartitionConfig {
&self.partition_config
}
pub fn partition_receiver(
&self,
partition_id: PartitionId,
) -> Option<Arc<tokio::sync::Mutex<mpsc::Receiver<CommandEnvelope>>>> {
self.partition_receivers.get(partition_id).cloned()
}
pub fn result_receiver(&self) -> Arc<tokio::sync::Mutex<mpsc::Receiver<CommandResult>>> {
Arc::clone(&self.result_receiver)
}
pub fn result_sender(&self) -> mpsc::Sender<CommandResult> {
self.result_sender.clone()
}
}
#[async_trait]
impl DistributedQueue for LocalDistributedQueue {
async fn enqueue(&self, envelope: CommandEnvelope) -> Result<()> {
let partition_id = envelope.partition_id;
if partition_id >= self.partition_senders.len() {
return Err(crate::error::LaneError::Other(format!(
"Invalid partition ID: {}",
partition_id
)));
}
self.partition_senders[partition_id]
.send(envelope)
.await
.map_err(|e| crate::error::LaneError::Other(format!("Failed to enqueue: {}", e)))?;
Ok(())
}
async fn dequeue(&self, partition_id: PartitionId) -> Result<Option<CommandEnvelope>> {
if partition_id >= self.partition_receivers.len() {
return Err(crate::error::LaneError::Other(format!(
"Invalid partition ID: {}",
partition_id
)));
}
let mut receiver = self.partition_receivers[partition_id].lock().await;
match receiver.try_recv() {
Ok(envelope) => Ok(Some(envelope)),
Err(mpsc::error::TryRecvError::Empty) => Ok(None),
Err(mpsc::error::TryRecvError::Disconnected) => Ok(None),
}
}
async fn complete(&self, result: CommandResult) -> Result<()> {
self.result_sender
.send(result)
.await
.map_err(|e| crate::error::LaneError::Other(format!("Failed to send result: {}", e)))?;
Ok(())
}
fn num_partitions(&self) -> usize {
self.partition_config.num_partitions
}
fn worker_id(&self) -> &WorkerId {
&self.worker_id
}
fn is_coordinator(&self) -> bool {
true }
fn is_worker(&self) -> bool {
true }
}
pub struct WorkerPool {
distributed_queue: Arc<dyn DistributedQueue>,
worker_handles: Vec<tokio::task::JoinHandle<()>>,
shutdown: Arc<std::sync::atomic::AtomicBool>,
}
impl WorkerPool {
pub fn new(distributed_queue: Arc<dyn DistributedQueue>) -> Self {
Self {
distributed_queue,
worker_handles: Vec::new(),
shutdown: Arc::new(std::sync::atomic::AtomicBool::new(false)),
}
}
pub fn start<F, Fut>(&mut self, command_executor: F)
where
F: Fn(CommandEnvelope) -> Fut + Send + Sync + Clone + 'static,
Fut: std::future::Future<Output = std::result::Result<serde_json::Value, String>>
+ Send
+ 'static,
{
let num_partitions = self.distributed_queue.num_partitions();
for partition_id in 0..num_partitions {
let queue = Arc::clone(&self.distributed_queue);
let shutdown = Arc::clone(&self.shutdown);
let executor = command_executor.clone();
let handle = tokio::spawn(async move {
loop {
if shutdown.load(std::sync::atomic::Ordering::Relaxed) {
break;
}
match queue.dequeue(partition_id).await {
Ok(Some(envelope)) => {
let command_id = envelope.id.clone();
let start = std::time::Instant::now();
let result = executor(envelope).await;
let duration_ms = start.elapsed().as_millis() as u64;
let command_result = CommandResult {
command_id,
result,
worker_id: queue.worker_id().clone(),
duration_ms,
};
let _ = queue.complete(command_result).await;
}
Ok(None) => {
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
}
Err(_) => {
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
}
}
}
});
self.worker_handles.push(handle);
}
}
pub async fn shutdown(&mut self) {
self.shutdown
.store(true, std::sync::atomic::Ordering::Relaxed);
for handle in self.worker_handles.drain(..) {
let _ = handle.await;
}
}
pub fn is_running(&self) -> bool {
!self.worker_handles.is_empty() && !self.shutdown.load(std::sync::atomic::Ordering::Relaxed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_local_distributed_queue_creation() {
let queue = LocalDistributedQueue::auto();
assert!(queue.num_partitions() > 0);
assert!(queue.is_coordinator());
assert!(queue.is_worker());
}
#[tokio::test]
async fn test_local_distributed_queue_enqueue_dequeue() {
let queue = LocalDistributedQueue::new(PartitionConfig::new(
2,
crate::partition::PartitionStrategy::RoundRobin,
));
let envelope = CommandEnvelope {
id: "cmd1".to_string(),
command_type: "test".to_string(),
lane_id: "query".to_string(),
partition_id: 0,
payload: serde_json::json!({"data": "test"}),
retry_count: 0,
created_at: chrono::Utc::now(),
};
queue.enqueue(envelope.clone()).await.unwrap();
let dequeued = queue.dequeue(0).await.unwrap();
assert!(dequeued.is_some());
let dequeued = dequeued.unwrap();
assert_eq!(dequeued.id, "cmd1");
assert_eq!(dequeued.command_type, "test");
let dequeued = queue.dequeue(0).await.unwrap();
assert!(dequeued.is_none());
}
#[tokio::test]
async fn test_local_distributed_queue_complete() {
let queue = LocalDistributedQueue::new(PartitionConfig::new(
2,
crate::partition::PartitionStrategy::RoundRobin,
));
let result = CommandResult {
command_id: "cmd1".to_string(),
result: Ok(serde_json::json!({"success": true})),
worker_id: "worker1".to_string(),
duration_ms: 100,
};
queue.complete(result).await.unwrap();
let receiver_arc = queue.result_receiver();
let mut receiver = receiver_arc.lock().await;
let received = receiver.try_recv();
assert!(received.is_ok());
let received = received.unwrap();
assert_eq!(received.command_id, "cmd1");
}
#[tokio::test]
async fn test_worker_pool() {
let queue = Arc::new(LocalDistributedQueue::new(PartitionConfig::new(
2,
crate::partition::PartitionStrategy::RoundRobin,
)));
let mut pool = WorkerPool::new(queue.clone());
pool.start(|envelope| async move { Ok(serde_json::json!({"processed": envelope.id})) });
assert!(pool.is_running());
let envelope = CommandEnvelope {
id: "cmd1".to_string(),
command_type: "test".to_string(),
lane_id: "query".to_string(),
partition_id: 0,
payload: serde_json::json!({}),
retry_count: 0,
created_at: chrono::Utc::now(),
};
queue.enqueue(envelope).await.unwrap();
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
let receiver_arc = queue.result_receiver();
let mut receiver = receiver_arc.lock().await;
let result = receiver.try_recv();
assert!(result.is_ok());
let result = result.unwrap();
assert_eq!(result.command_id, "cmd1");
assert!(result.result.is_ok());
pool.shutdown().await;
assert!(!pool.is_running());
}
#[test]
fn test_command_envelope_serialization() {
let envelope = CommandEnvelope {
id: "cmd1".to_string(),
command_type: "test".to_string(),
lane_id: "query".to_string(),
partition_id: 0,
payload: serde_json::json!({"key": "value"}),
retry_count: 2,
created_at: chrono::Utc::now(),
};
let json = serde_json::to_string(&envelope).unwrap();
let parsed: CommandEnvelope = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.id, "cmd1");
assert_eq!(parsed.command_type, "test");
assert_eq!(parsed.partition_id, 0);
assert_eq!(parsed.retry_count, 2);
}
#[test]
fn test_command_result_serialization() {
let result = CommandResult {
command_id: "cmd1".to_string(),
result: Ok(serde_json::json!({"success": true})),
worker_id: "worker1".to_string(),
duration_ms: 150,
};
let json = serde_json::to_string(&result).unwrap();
let parsed: CommandResult = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.command_id, "cmd1");
assert_eq!(parsed.worker_id, "worker1");
assert_eq!(parsed.duration_ms, 150);
}
}