use serde_json::Value;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use tracing::info;
use crate::server::brp::{builtin_methods, BrpRequest, BrpResponse, DebugCommand};
use crate::server::brp_command_handler::{CommandHandlerRegistry, CoreBrpHandler, BrpCommandHandler};
use crate::server::config::Config;
use crate::server::debug_command_processor::{DebugCommandRequest, DebugCommandRouter};
use crate::server::error::{Error, Result};
use crate::server::resource_manager::{ResourceConfig, ResourceManager};
pub struct BrpClient {
config: Config,
http: reqwest::Client,
url: String,
connected: AtomicBool,
retry_count: AtomicU64,
request_id: AtomicU64,
resource_manager: Option<Arc<ResourceManager>>,
command_registry: Arc<CommandHandlerRegistry>,
debug_router: Option<Arc<DebugCommandRouter>>,
}
impl std::fmt::Debug for BrpClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BrpClient")
.field("config", &self.config)
.field("url", &self.url)
.field("connected", &self.connected.load(Ordering::Relaxed))
.field("has_resource_manager", &self.resource_manager.is_some())
.field("has_debug_router", &self.debug_router.is_some())
.finish()
}
}
impl BrpClient {
pub fn new(config: &Config) -> Self {
let command_registry = Arc::new(CommandHandlerRegistry::new());
BrpClient {
config: config.clone(),
http: reqwest::Client::new(),
url: format!(
"http://{}:{}",
config.bevy_brp_host, config.bevy_brp_port
),
connected: AtomicBool::new(false),
retry_count: AtomicU64::new(0),
request_id: AtomicU64::new(1),
resource_manager: None,
command_registry,
debug_router: None,
}
}
pub async fn init(&self) -> Result<()> {
let core_handler = Arc::new(CoreBrpHandler);
self.command_registry.register(core_handler).await;
Ok(())
}
pub fn with_resource_manager(mut self, resource_manager: Arc<ResourceManager>) -> Self {
self.resource_manager = Some(resource_manager);
self
}
pub fn with_debug_router(mut self, debug_router: Arc<DebugCommandRouter>) -> Self {
self.debug_router = Some(debug_router);
self
}
pub async fn is_connected(&self) -> bool {
self.connected.load(Ordering::Relaxed)
}
pub async fn retry_count(&self) -> u32 {
self.retry_count.load(Ordering::Relaxed) as u32
}
pub async fn connect(&self) -> Result<()> {
let max_retries = self.config.resilience.retry.max_attempts;
for attempt in 0..=max_retries {
let probe = BrpRequest {
method: builtin_methods::RPC_DISCOVER_METHOD.to_string(),
id: Some(Value::from(self.request_id.fetch_add(1, Ordering::Relaxed))),
params: None,
};
match self.http.post(&self.url).json(&probe).send().await {
Ok(response) if response.status().is_success() => {
info!("Connected to BRP server at {}", self.url);
self.connected.store(true, Ordering::Relaxed);
self.retry_count.store(0, Ordering::Relaxed);
return Ok(());
}
Ok(response) => {
self.retry_count
.store((attempt + 1) as u64, Ordering::Relaxed);
if attempt >= max_retries {
return Err(Error::Connection(format!(
"BRP health check returned HTTP {}",
response.status()
)));
}
}
Err(e) => {
self.retry_count
.store((attempt + 1) as u64, Ordering::Relaxed);
if attempt >= max_retries {
return Err(Error::Connection(format!("Connection failed: {e}")));
}
}
}
let delay = std::time::Duration::from_millis(
self.config.resilience.retry.initial_delay.as_millis() as u64
* (1u64 << attempt.min(5)),
);
tokio::time::sleep(delay).await;
}
Err(Error::Connection("Max retries exceeded".to_string()))
}
pub async fn disconnect(&self) -> Result<()> {
self.connected.store(false, Ordering::Relaxed);
info!("Disconnected from BRP server");
Ok(())
}
pub async fn send_request(&self, request: BrpRequest) -> Result<BrpResponse> {
let mut request = request;
if request.id.is_none() {
request.id = Some(Value::from(self.request_id.fetch_add(1, Ordering::Relaxed)));
}
let response = self
.http
.post(&self.url)
.json(&request)
.send()
.await
.map_err(|e| {
self.connected.store(false, Ordering::Relaxed);
Error::Connection(format!("BRP HTTP request failed: {e}"))
})?;
if !response.status().is_success() {
self.connected.store(false, Ordering::Relaxed);
return Err(Error::Connection(format!(
"BRP server returned HTTP {}",
response.status()
)));
}
self.connected.store(true, Ordering::Relaxed);
response.json::<BrpResponse>().await.map_err(Error::from)
}
pub async fn send_debug_command(&self, command: DebugCommand) -> Result<BrpResponse> {
if let Some(debug_router) = &self.debug_router {
let request = DebugCommandRequest::new(command, uuid::Uuid::new_v4().to_string(), None);
debug_router.queue_command(request).await?;
match debug_router.process_next().await {
Some(Ok((id, response))) => {
let value = serde_json::to_value(response).map_err(Error::Json)?;
Ok(BrpResponse::new(Some(Value::from(id.to_string())), Ok(value)))
}
Some(Err(e)) => Err(e),
None => Err(Error::Brp("No response from debug command processor".to_string())),
}
} else {
Err(Error::Brp("Debug commands not supported without a debug router".to_string()))
}
}
pub async fn get_connection_stats(&self) -> ConnectionStats {
ConnectionStats {
connected: self.connected.load(Ordering::Relaxed),
retry_count: self.retry_count.load(Ordering::Relaxed) as u32,
queue_size: 0,
}
}
pub fn get_resource_manager(&self) -> Option<&Arc<ResourceManager>> {
self.resource_manager.as_ref()
}
pub async fn register_handler(&self, handler: Arc<dyn BrpCommandHandler>) {
self.command_registry.register(handler).await;
}
}
#[derive(Debug, Clone)]
pub struct ConnectionStats {
pub connected: bool,
pub retry_count: u32,
pub queue_size: usize,
}
pub async fn create_brp_client(config: &Config) -> Result<Arc<BrpClient>> {
let client = BrpClient::new(config);
client.init().await?;
Ok(Arc::new(client))
}
pub async fn create_brp_client_with_manager(
config: &Config,
resource_manager: Arc<ResourceManager>,
) -> Result<Arc<BrpClient>> {
let client = BrpClient::new(config)
.with_resource_manager(resource_manager);
client.init().await?;
Ok(Arc::new(client))
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_brp_client_creation() {
let config = Config::default();
let client = BrpClient::new(&config);
assert!(!client.is_connected().await);
assert_eq!(client.retry_count().await, 0);
}
#[tokio::test]
async fn test_brp_client_with_resource_manager() {
let config = Config::default();
let resource_manager = Arc::new(ResourceManager::new(ResourceConfig::default()));
let client = create_brp_client_with_manager(&config, resource_manager).await.unwrap();
assert!(client.get_resource_manager().is_some());
}
#[tokio::test]
async fn test_connection_stats() {
let config = Config::default();
let client = BrpClient::new(&config);
let stats = client.get_connection_stats().await;
assert!(!stats.connected);
assert_eq!(stats.retry_count, 0);
assert_eq!(stats.queue_size, 0);
}
}