- Fix all 98 clippy warnings (0 remaining with -D warnings) - Add 30s default gRPC timeout on all channels via Endpoint::timeout() - Replace readiness probe is_some() with actual gRPC health check (returns 503 when degraded) - Add responsive Tailwind breakpoints to all 5 dashboard pages - Add ComponentErrorBoundary for crash-prone chart components - Log JWT validation errors at debug level instead of silently discarding Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
102 lines
3.3 KiB
Rust
102 lines
3.3 KiB
Rust
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use tokio::sync::broadcast;
|
|
use tonic::transport::Channel;
|
|
|
|
use crate::config::GatewayConfig;
|
|
|
|
/// Default gRPC request deadline — prevents indefinite hangs on slow/unresponsive services
|
|
const GRPC_TIMEOUT: Duration = Duration::from_secs(30);
|
|
|
|
/// Shared application state passed to all handlers via Axum's State extractor
|
|
#[derive(Clone)]
|
|
pub struct AppState {
|
|
pub config: Arc<GatewayConfig>,
|
|
pub trading_channel: Option<Channel>,
|
|
pub backtesting_channel: Option<Channel>,
|
|
pub ml_training_channel: Option<Channel>,
|
|
/// Broadcast sender for WebSocket events
|
|
pub ws_broadcast: broadcast::Sender<String>,
|
|
}
|
|
|
|
impl AppState {
|
|
pub async fn new(config: GatewayConfig) -> anyhow::Result<Self> {
|
|
let (ws_broadcast, _) = broadcast::channel(4096);
|
|
|
|
// Create lazy gRPC channels (connect on first use) with default timeout
|
|
let trading_channel = Channel::from_shared(config.trading_service_url.clone())
|
|
.ok()
|
|
.map(|c| c.timeout(GRPC_TIMEOUT).connect_lazy());
|
|
|
|
let backtesting_channel = Channel::from_shared(config.backtesting_service_url.clone())
|
|
.ok()
|
|
.map(|c| c.timeout(GRPC_TIMEOUT).connect_lazy());
|
|
|
|
let ml_training_channel = Channel::from_shared(config.ml_training_service_url.clone())
|
|
.ok()
|
|
.map(|c| c.timeout(GRPC_TIMEOUT).connect_lazy());
|
|
|
|
Ok(Self {
|
|
config: Arc::new(config),
|
|
trading_channel,
|
|
backtesting_channel,
|
|
ml_training_channel,
|
|
ws_broadcast,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(clippy::unwrap_used)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn test_new_creates_state_with_valid_config() {
|
|
let config = GatewayConfig::default();
|
|
let state = AppState::new(config).await.unwrap();
|
|
assert!(state.trading_channel.is_some());
|
|
assert!(state.backtesting_channel.is_some());
|
|
assert!(state.ml_training_channel.is_some());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_new_with_empty_url_yields_none_channel() {
|
|
let config = GatewayConfig {
|
|
trading_service_url: String::new(),
|
|
..GatewayConfig::default()
|
|
};
|
|
let state = AppState::new(config).await.unwrap();
|
|
assert!(state.trading_channel.is_none());
|
|
// Other channels should still work
|
|
assert!(state.backtesting_channel.is_some());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_broadcast_channel_sends_and_receives() {
|
|
let config = GatewayConfig::default();
|
|
let state = AppState::new(config).await.unwrap();
|
|
let mut rx = state.ws_broadcast.subscribe();
|
|
state.ws_broadcast.send("hello".to_owned()).unwrap();
|
|
let msg = rx.recv().await.unwrap();
|
|
assert_eq!(msg, "hello");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_config_is_arc_wrapped() {
|
|
let config = GatewayConfig {
|
|
jwt_secret: "test-secret".to_owned(),
|
|
..GatewayConfig::default()
|
|
};
|
|
let state = AppState::new(config).await.unwrap();
|
|
assert_eq!(state.config.jwt_secret, "test-secret");
|
|
}
|
|
|
|
#[test]
|
|
fn test_state_is_clone() {
|
|
// AppState must be Clone for Axum's State extractor
|
|
fn assert_clone<T: Clone>() {}
|
|
assert_clone::<AppState>();
|
|
}
|
|
}
|