use std::fmt;
use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::error::Result;
#[cfg(test)]
use crate::error::EcosystemError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CommandId(pub u64);
impl CommandId {
pub fn new(id: u64) -> Self {
Self(id)
}
pub fn as_u64(&self) -> u64 {
self.0
}
}
impl fmt::Display for CommandId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "cmd-{}", self.0)
}
}
impl From<u64> for CommandId {
fn from(id: u64) -> Self {
Self(id)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PersistentCommand {
RunSteps {
count: u64,
},
Pause,
Resume,
Inject {
position: (u32, u32, u32),
value: f32,
},
GetProgress,
GetStats,
Terminate,
Custom {
type_id: u32,
payload: Vec<u8>,
},
}
impl PersistentCommand {
pub fn run_steps(count: u64) -> Self {
Self::RunSteps { count }
}
pub fn inject(x: u32, y: u32, z: u32, value: f32) -> Self {
Self::Inject {
position: (x, y, z),
value,
}
}
pub fn custom(type_id: u32, payload: Vec<u8>) -> Self {
Self::Custom { type_id, payload }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PersistentResponse {
Ack {
cmd_id: CommandId,
},
Progress {
cmd_id: CommandId,
current_step: u64,
remaining: u64,
},
Stats {
cmd_id: CommandId,
stats: PersistentStats,
},
Error {
cmd_id: CommandId,
code: u32,
message: String,
},
Terminated {
final_step: u64,
},
Custom {
cmd_id: CommandId,
type_id: u32,
payload: Vec<u8>,
},
}
impl PersistentResponse {
pub fn command_id(&self) -> Option<CommandId> {
match self {
Self::Ack { cmd_id }
| Self::Progress { cmd_id, .. }
| Self::Stats { cmd_id, .. }
| Self::Error { cmd_id, .. }
| Self::Custom { cmd_id, .. } => Some(*cmd_id),
Self::Terminated { .. } => None,
}
}
pub fn is_error(&self) -> bool {
matches!(self, Self::Error { .. })
}
pub fn is_terminated(&self) -> bool {
matches!(self, Self::Terminated { .. })
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PersistentStats {
pub current_step: u64,
pub steps_remaining: u64,
pub messages_processed: u64,
pub total_energy: f32,
pub k2k_sent: u64,
pub k2k_received: u64,
pub is_running: bool,
pub has_terminated: bool,
pub pending_commands: u32,
pub pending_responses: u32,
}
impl PersistentStats {
pub fn is_idle(&self) -> bool {
self.is_running && self.steps_remaining == 0
}
pub fn is_processing(&self) -> bool {
self.is_running && self.steps_remaining > 0
}
}
#[derive(Debug, Clone)]
pub struct PersistentConfig {
pub h2k_capacity: usize,
pub k2h_capacity: usize,
pub progress_interval: u64,
pub poll_interval: Duration,
pub command_timeout: Duration,
pub max_in_flight: usize,
}
impl Default for PersistentConfig {
fn default() -> Self {
Self {
h2k_capacity: 256,
k2h_capacity: 256,
progress_interval: 100,
poll_interval: Duration::from_micros(100),
command_timeout: Duration::from_secs(30),
max_in_flight: 1000,
}
}
}
impl PersistentConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_h2k_capacity(mut self, capacity: usize) -> Self {
self.h2k_capacity = capacity.next_power_of_two();
self
}
pub fn with_k2h_capacity(mut self, capacity: usize) -> Self {
self.k2h_capacity = capacity.next_power_of_two();
self
}
pub fn with_progress_interval(mut self, interval: u64) -> Self {
self.progress_interval = interval;
self
}
pub fn with_poll_interval(mut self, interval: Duration) -> Self {
self.poll_interval = interval;
self
}
pub fn with_command_timeout(mut self, timeout: Duration) -> Self {
self.command_timeout = timeout;
self
}
pub fn with_max_in_flight(mut self, max: usize) -> Self {
self.max_in_flight = max;
self
}
}
#[async_trait::async_trait]
pub trait PersistentHandle: Send + Sync + 'static {
fn kernel_id(&self) -> &str;
fn is_running(&self) -> bool;
fn send_command(&self, cmd: PersistentCommand) -> Result<CommandId>;
fn poll_responses(&self) -> Vec<PersistentResponse>;
fn stats(&self) -> PersistentStats;
async fn wait_for_command(&self, cmd_id: CommandId, timeout: Duration) -> Result<()>;
async fn shutdown(&self) -> Result<()>;
fn config(&self) -> &PersistentConfig;
}
#[derive(Debug)]
pub struct CommandTracker {
next_id: std::sync::atomic::AtomicU64,
pending: parking_lot::RwLock<
std::collections::HashMap<CommandId, tokio::sync::oneshot::Sender<PersistentResponse>>,
>,
}
impl CommandTracker {
pub fn new() -> Self {
Self {
next_id: std::sync::atomic::AtomicU64::new(1),
pending: parking_lot::RwLock::new(std::collections::HashMap::new()),
}
}
pub fn next_id(&self) -> CommandId {
CommandId(
self.next_id
.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
)
}
pub fn register(
&self,
cmd_id: CommandId,
) -> tokio::sync::oneshot::Receiver<PersistentResponse> {
let (tx, rx) = tokio::sync::oneshot::channel();
self.pending.write().insert(cmd_id, tx);
rx
}
pub fn notify(&self, response: &PersistentResponse) -> bool {
if let Some(cmd_id) = response.command_id() {
if let Some(tx) = self.pending.write().remove(&cmd_id) {
let _ = tx.send(response.clone());
return true;
}
}
false
}
pub fn pending_count(&self) -> usize {
self.pending.read().len()
}
}
impl Default for CommandTracker {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
pub mod mock {
use super::*;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
pub struct MockPersistentHandle {
kernel_id: String,
running: AtomicBool,
cmd_counter: AtomicU64,
step_counter: AtomicU64,
config: PersistentConfig,
responses: parking_lot::Mutex<Vec<PersistentResponse>>,
}
impl MockPersistentHandle {
pub fn new(kernel_id: impl Into<String>) -> Self {
Self {
kernel_id: kernel_id.into(),
running: AtomicBool::new(true),
cmd_counter: AtomicU64::new(1),
step_counter: AtomicU64::new(0),
config: PersistentConfig::default(),
responses: parking_lot::Mutex::new(Vec::new()),
}
}
pub fn queue_response(&self, response: PersistentResponse) {
self.responses.lock().push(response);
}
pub fn set_running(&self, running: bool) {
self.running.store(running, Ordering::Release);
}
}
#[async_trait::async_trait]
impl PersistentHandle for MockPersistentHandle {
fn kernel_id(&self) -> &str {
&self.kernel_id
}
fn is_running(&self) -> bool {
self.running.load(Ordering::Acquire)
}
fn send_command(&self, cmd: PersistentCommand) -> Result<CommandId> {
if !self.is_running() {
return Err(EcosystemError::ServiceUnavailable(
"Kernel not running".to_string(),
));
}
let cmd_id = CommandId(self.cmd_counter.fetch_add(1, Ordering::Relaxed));
self.responses
.lock()
.push(PersistentResponse::Ack { cmd_id });
match cmd {
PersistentCommand::RunSteps { count } => {
self.step_counter.fetch_add(count, Ordering::Relaxed);
}
PersistentCommand::Terminate => {
self.running.store(false, Ordering::Release);
self.responses.lock().push(PersistentResponse::Terminated {
final_step: self.step_counter.load(Ordering::Relaxed),
});
}
_ => {}
}
Ok(cmd_id)
}
fn poll_responses(&self) -> Vec<PersistentResponse> {
std::mem::take(&mut *self.responses.lock())
}
fn stats(&self) -> PersistentStats {
PersistentStats {
current_step: self.step_counter.load(Ordering::Relaxed),
is_running: self.is_running(),
has_terminated: !self.is_running(),
..Default::default()
}
}
async fn wait_for_command(&self, cmd_id: CommandId, timeout: Duration) -> Result<()> {
let start = std::time::Instant::now();
loop {
for response in self.poll_responses() {
if response.command_id() == Some(cmd_id) {
if response.is_error() {
if let PersistentResponse::Error { message, .. } = response {
return Err(EcosystemError::Internal(message));
}
}
return Ok(());
}
}
if start.elapsed() > timeout {
return Err(EcosystemError::Timeout(timeout));
}
tokio::time::sleep(Duration::from_millis(1)).await;
}
}
async fn shutdown(&self) -> Result<()> {
let cmd_id = self.send_command(PersistentCommand::Terminate)?;
self.wait_for_command(cmd_id, Duration::from_secs(5)).await
}
fn config(&self) -> &PersistentConfig {
&self.config
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_command_id() {
let id = CommandId::new(42);
assert_eq!(id.as_u64(), 42);
assert_eq!(format!("{}", id), "cmd-42");
}
#[test]
fn test_persistent_command() {
let cmd = PersistentCommand::run_steps(100);
assert!(matches!(cmd, PersistentCommand::RunSteps { count: 100 }));
let cmd = PersistentCommand::inject(1, 2, 3, 4.5);
assert!(matches!(
cmd,
PersistentCommand::Inject {
position: (1, 2, 3),
value
} if (value - 4.5).abs() < f32::EPSILON
));
}
#[test]
fn test_persistent_response_command_id() {
let response = PersistentResponse::Ack {
cmd_id: CommandId(1),
};
assert_eq!(response.command_id(), Some(CommandId(1)));
let response = PersistentResponse::Terminated { final_step: 100 };
assert_eq!(response.command_id(), None);
}
#[test]
fn test_persistent_stats() {
let mut stats = PersistentStats::default();
stats.is_running = true;
stats.steps_remaining = 0;
assert!(stats.is_idle());
stats.steps_remaining = 100;
assert!(stats.is_processing());
}
#[test]
fn test_persistent_config_builder() {
let config = PersistentConfig::new()
.with_h2k_capacity(100)
.with_k2h_capacity(200)
.with_progress_interval(50)
.with_command_timeout(Duration::from_secs(60));
assert_eq!(config.h2k_capacity, 128); assert_eq!(config.k2h_capacity, 256); assert_eq!(config.progress_interval, 50);
assert_eq!(config.command_timeout, Duration::from_secs(60));
}
#[test]
fn test_command_tracker() {
let tracker = CommandTracker::new();
let id1 = tracker.next_id();
let id2 = tracker.next_id();
assert_ne!(id1, id2);
assert_eq!(tracker.pending_count(), 0);
}
#[tokio::test]
async fn test_mock_persistent_handle() {
let handle = mock::MockPersistentHandle::new("test-kernel");
assert!(handle.is_running());
assert_eq!(handle.kernel_id(), "test-kernel");
let cmd_id = handle
.send_command(PersistentCommand::RunSteps { count: 100 })
.unwrap();
let responses = handle.poll_responses();
assert!(!responses.is_empty());
assert!(matches!(
responses[0],
PersistentResponse::Ack { cmd_id: id } if id == cmd_id
));
let stats = handle.stats();
assert_eq!(stats.current_step, 100);
assert!(stats.is_running);
}
#[tokio::test]
async fn test_mock_shutdown() {
let handle = mock::MockPersistentHandle::new("test-kernel");
handle.shutdown().await.unwrap();
assert!(!handle.is_running());
assert!(handle.stats().has_terminated);
}
}