use std::collections::HashMap;
use std::time::Instant;
pub type RemoteResult<T> = Result<T, RemoteError>;
#[derive(Debug, Clone, PartialEq)]
pub enum RemoteError {
ConnectionFailed { host: String, reason: String },
AuthenticationFailed { host: String },
CommandFailed {
host: String,
exit_code: i32,
stderr: String,
},
Timeout { host: String, timeout_ms: u64 },
HostNotFound { host: String },
AllHostsFailed { failures: Vec<String> },
InvalidConfig { reason: String },
AggregationFailed { reason: String },
}
impl std::fmt::Display for RemoteError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ConnectionFailed { host, reason } => {
write!(f, "Connection to {} failed: {}", host, reason)
}
Self::AuthenticationFailed { host } => {
write!(f, "Authentication failed for {}", host)
}
Self::CommandFailed {
host,
exit_code,
stderr,
} => {
write!(
f,
"Command failed on {} (exit {}): {}",
host, exit_code, stderr
)
}
Self::Timeout { host, timeout_ms } => {
write!(f, "Timeout after {}ms waiting for {}", timeout_ms, host)
}
Self::HostNotFound { host } => {
write!(f, "Host {} not found in pool", host)
}
Self::AllHostsFailed { failures } => {
write!(f, "All hosts failed: {:?}", failures)
}
Self::InvalidConfig { reason } => {
write!(f, "Invalid configuration: {}", reason)
}
Self::AggregationFailed { reason } => {
write!(f, "Result aggregation failed: {}", reason)
}
}
}
}
impl std::error::Error for RemoteError {}
#[derive(Debug, Clone)]
pub enum AuthMethod {
Key {
key_path: String,
passphrase_env: Option<String>,
},
Agent,
PasswordEnv {
env_var: String,
},
}
impl Default for AuthMethod {
fn default() -> Self {
Self::Agent
}
}
#[derive(Debug, Clone)]
pub struct HostConfig {
pub host: String,
pub port: u16,
pub username: String,
pub auth: AuthMethod,
pub connect_timeout_ms: u64,
pub command_timeout_ms: u64,
pub architecture: Option<String>,
pub labels: HashMap<String, String>,
}
impl HostConfig {
pub fn new(host: impl Into<String>, username: impl Into<String>) -> Self {
Self {
host: host.into(),
port: 22,
username: username.into(),
auth: AuthMethod::default(),
connect_timeout_ms: 10_000,
command_timeout_ms: 60_000,
architecture: None,
labels: HashMap::new(),
}
}
pub fn with_port(mut self, port: u16) -> Self {
self.port = port;
self
}
pub fn with_auth(mut self, auth: AuthMethod) -> Self {
self.auth = auth;
self
}
pub fn with_connect_timeout_ms(mut self, timeout_ms: u64) -> Self {
self.connect_timeout_ms = timeout_ms;
self
}
pub fn with_command_timeout_ms(mut self, timeout_ms: u64) -> Self {
self.command_timeout_ms = timeout_ms;
self
}
pub fn with_architecture(mut self, arch: impl Into<String>) -> Self {
self.architecture = Some(arch.into());
self
}
pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.labels.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum HostHealth {
Healthy,
Degraded { latency_ms: u64 },
Unreachable { last_error: String },
Unknown,
}
impl Default for HostHealth {
fn default() -> Self {
Self::Unknown
}
}
#[derive(Debug, Clone)]
pub struct HostState {
pub config: HostConfig,
pub health: HostHealth,
pub last_success: Option<Instant>,
pub failure_count: u32,
pub commands_executed: u64,
pub avg_latency_ms: f64,
}
impl HostState {
pub fn new(config: HostConfig) -> Self {
Self {
config,
health: HostHealth::Unknown,
last_success: None,
failure_count: 0,
commands_executed: 0,
avg_latency_ms: 0.0,
}
}
pub fn is_available(&self) -> bool {
matches!(
self.health,
HostHealth::Healthy | HostHealth::Degraded { .. } | HostHealth::Unknown
)
}
pub fn record_success(&mut self, latency_ms: u64) {
self.last_success = Some(Instant::now());
self.failure_count = 0;
self.commands_executed += 1;
let n = self.commands_executed as f64;
self.avg_latency_ms = self.avg_latency_ms * ((n - 1.0) / n) + (latency_ms as f64) / n;
self.health = if latency_ms > 5000 {
HostHealth::Degraded { latency_ms }
} else {
HostHealth::Healthy
};
}
pub fn record_failure(&mut self, error: &str) {
self.failure_count += 1;
if self.failure_count >= 3 {
self.health = HostHealth::Unreachable {
last_error: error.to_string(),
};
}
}
}
#[derive(Debug, Clone)]
pub struct CommandResult {
pub host: String,
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
pub duration_ms: u64,
}
impl CommandResult {
pub fn success(&self) -> bool {
self.exit_code == 0
}
}
#[derive(Debug, Clone)]
pub struct AggregatedResult {
pub host_results: Vec<HostBenchmark>,
pub throughput_geomean: f64,
pub latency_p50_mean_us: f64,
pub latency_p99_max_us: f64,
pub hosts_succeeded: usize,
pub hosts_failed: usize,
pub collection_time_ms: u64,
}
impl AggregatedResult {
pub fn success_rate(&self) -> f64 {
let total = self.hosts_succeeded + self.hosts_failed;
if total == 0 {
0.0
} else {
self.hosts_succeeded as f64 / total as f64
}
}
}
#[derive(Debug, Clone)]
pub struct HostBenchmark {
pub host: String,
pub architecture: String,
pub throughput_ops: f64,
pub latency_p50_us: f64,
pub latency_p99_us: f64,
pub memory_bytes: u64,
pub gpu_utilization: Option<f64>,
pub timestamp_ns: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AggregationStrategy {
GeometricMean,
Median,
Minimum,
Maximum,
}
impl Default for AggregationStrategy {
fn default() -> Self {
Self::GeometricMean
}
}
#[derive(Debug, Clone)]
pub struct RemoteAgentConfig {
pub max_concurrent: usize,
pub retry_count: u32,
pub retry_delay_ms: u64,
pub health_check_interval_sec: u64,
pub aggregation: AggregationStrategy,
pub remote_binary_path: String,
}
impl Default for RemoteAgentConfig {
fn default() -> Self {
Self {
max_concurrent: 10,
retry_count: 3,
retry_delay_ms: 1000,
health_check_interval_sec: 60,
aggregation: AggregationStrategy::default(),
remote_binary_path: "/usr/local/bin/cbtop".to_string(),
}
}
}
pub const DEFAULT_RETRY_DELAY_MS: u64 = 1000;
pub const DEFAULT_MAX_CONCURRENT: usize = 10;
pub const DEFAULT_HEALTH_CHECK_INTERVAL_SEC: u64 = 60;