Copied from api_gateway, removed REST handlers (port 8080), added tonic-web + CORS for grpc-web browser access. Binary renamed: api-gateway → api Changes: - Package name: api-gateway → api - Deleted src/handlers/ (REST ML endpoints on port 8080) - Added tonic-web 0.13 + tower-http CORS layer - Server::builder().accept_http1(true) for grpc-web - CORS_ORIGINS env var (default http://localhost:5173) - Metrics server on port 9091 (axum) preserved - All 95 lib tests pass, 0 clippy warnings - Added services/api to workspace members Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
231 lines
7.5 KiB
Rust
231 lines
7.5 KiB
Rust
//! ML Inference Service Proxy - Zero-copy gRPC forwarding for ml.MLService
|
|
//!
|
|
//! Forwards to trading-service backend (ML inference lives in same process).
|
|
|
|
use futures::Stream;
|
|
use std::pin::Pin;
|
|
use std::time::Duration;
|
|
use tonic::{Request, Response, Status};
|
|
use tracing::{error, instrument};
|
|
|
|
use crate::ml_inference::ml_service_client::MlServiceClient;
|
|
use crate::ml_inference::ml_service_server::MlService;
|
|
use crate::ml_inference::{
|
|
GetAvailableModelsRequest, GetAvailableModelsResponse, GetEnsembleVoteRequest,
|
|
GetEnsembleVoteResponse, GetFeatureImportanceRequest, GetFeatureImportanceResponse,
|
|
GetModelPerformanceRequest, GetModelPerformanceResponse, GetModelStatusRequest,
|
|
GetModelStatusResponse, GetPredictionRequest, GetPredictionResponse, ModelMetricsEvent,
|
|
PredictionEvent, RetrainModelRequest, RetrainModelResponse, SignalStrengthEvent,
|
|
StreamModelMetricsRequest, StreamModelStatusRequest, StreamPredictionsRequest,
|
|
StreamSignalStrengthRequest,
|
|
};
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct MlServiceProxy {
|
|
client: MlServiceClient<tonic::transport::Channel>,
|
|
}
|
|
|
|
impl MlServiceProxy {
|
|
pub fn new(client: MlServiceClient<tonic::transport::Channel>) -> Self {
|
|
Self { client }
|
|
}
|
|
}
|
|
|
|
#[tonic::async_trait]
|
|
impl MlService for MlServiceProxy {
|
|
type StreamPredictionsStream =
|
|
Pin<Box<dyn Stream<Item = Result<PredictionEvent, Status>> + Send>>;
|
|
type StreamModelMetricsStream =
|
|
Pin<Box<dyn Stream<Item = Result<ModelMetricsEvent, Status>> + Send>>;
|
|
type StreamSignalStrengthStream =
|
|
Pin<Box<dyn Stream<Item = Result<SignalStrengthEvent, Status>> + Send>>;
|
|
type StreamModelStatusStream =
|
|
Pin<Box<dyn Stream<Item = Result<GetModelStatusResponse, Status>> + Send>>;
|
|
|
|
#[instrument(skip(self, request), err)]
|
|
async fn get_prediction(
|
|
&self,
|
|
request: Request<GetPredictionRequest>,
|
|
) -> Result<Response<GetPredictionResponse>, Status> {
|
|
self.client.clone().get_prediction(request).await.map_err(|e| {
|
|
error!("Backend GetPrediction failed: {}", e);
|
|
e
|
|
})
|
|
}
|
|
|
|
#[instrument(skip(self, request), err)]
|
|
async fn stream_predictions(
|
|
&self,
|
|
request: Request<StreamPredictionsRequest>,
|
|
) -> Result<Response<Self::StreamPredictionsStream>, Status> {
|
|
let stream = self
|
|
.client
|
|
.clone()
|
|
.stream_predictions(request)
|
|
.await
|
|
.map_err(|e| {
|
|
error!("Backend StreamPredictions failed: {}", e);
|
|
e
|
|
})?;
|
|
Ok(Response::new(Box::pin(stream.into_inner())))
|
|
}
|
|
|
|
#[instrument(skip(self, request), err)]
|
|
async fn get_ensemble_vote(
|
|
&self,
|
|
request: Request<GetEnsembleVoteRequest>,
|
|
) -> Result<Response<GetEnsembleVoteResponse>, Status> {
|
|
self.client
|
|
.clone()
|
|
.get_ensemble_vote(request)
|
|
.await
|
|
.map_err(|e| {
|
|
error!("Backend GetEnsembleVote failed: {}", e);
|
|
e
|
|
})
|
|
}
|
|
|
|
#[instrument(skip(self, request), err)]
|
|
async fn get_model_status(
|
|
&self,
|
|
request: Request<GetModelStatusRequest>,
|
|
) -> Result<Response<GetModelStatusResponse>, Status> {
|
|
self.client
|
|
.clone()
|
|
.get_model_status(request)
|
|
.await
|
|
.map_err(|e| {
|
|
error!("Backend GetModelStatus failed: {}", e);
|
|
e
|
|
})
|
|
}
|
|
|
|
#[instrument(skip(self, request), err)]
|
|
async fn get_available_models(
|
|
&self,
|
|
request: Request<GetAvailableModelsRequest>,
|
|
) -> Result<Response<GetAvailableModelsResponse>, Status> {
|
|
self.client
|
|
.clone()
|
|
.get_available_models(request)
|
|
.await
|
|
.map_err(|e| {
|
|
error!("Backend GetAvailableModels failed: {}", e);
|
|
e
|
|
})
|
|
}
|
|
|
|
#[instrument(skip(self, request), err)]
|
|
async fn retrain_model(
|
|
&self,
|
|
request: Request<RetrainModelRequest>,
|
|
) -> Result<Response<RetrainModelResponse>, Status> {
|
|
self.client
|
|
.clone()
|
|
.retrain_model(request)
|
|
.await
|
|
.map_err(|e| {
|
|
error!("Backend RetrainModel failed: {}", e);
|
|
e
|
|
})
|
|
}
|
|
|
|
#[instrument(skip(self, request), err)]
|
|
async fn get_model_performance(
|
|
&self,
|
|
request: Request<GetModelPerformanceRequest>,
|
|
) -> Result<Response<GetModelPerformanceResponse>, Status> {
|
|
self.client
|
|
.clone()
|
|
.get_model_performance(request)
|
|
.await
|
|
.map_err(|e| {
|
|
error!("Backend GetModelPerformance failed: {}", e);
|
|
e
|
|
})
|
|
}
|
|
|
|
#[instrument(skip(self, request), err)]
|
|
async fn stream_model_metrics(
|
|
&self,
|
|
request: Request<StreamModelMetricsRequest>,
|
|
) -> Result<Response<Self::StreamModelMetricsStream>, Status> {
|
|
let stream = self
|
|
.client
|
|
.clone()
|
|
.stream_model_metrics(request)
|
|
.await
|
|
.map_err(|e| {
|
|
error!("Backend StreamModelMetrics failed: {}", e);
|
|
e
|
|
})?;
|
|
Ok(Response::new(Box::pin(stream.into_inner())))
|
|
}
|
|
|
|
#[instrument(skip(self, request), err)]
|
|
async fn get_feature_importance(
|
|
&self,
|
|
request: Request<GetFeatureImportanceRequest>,
|
|
) -> Result<Response<GetFeatureImportanceResponse>, Status> {
|
|
self.client
|
|
.clone()
|
|
.get_feature_importance(request)
|
|
.await
|
|
.map_err(|e| {
|
|
error!("Backend GetFeatureImportance failed: {}", e);
|
|
e
|
|
})
|
|
}
|
|
|
|
#[instrument(skip(self, request), err)]
|
|
async fn stream_signal_strength(
|
|
&self,
|
|
request: Request<StreamSignalStrengthRequest>,
|
|
) -> Result<Response<Self::StreamSignalStrengthStream>, Status> {
|
|
let stream = self
|
|
.client
|
|
.clone()
|
|
.stream_signal_strength(request)
|
|
.await
|
|
.map_err(|e| {
|
|
error!("Backend StreamSignalStrength failed: {}", e);
|
|
e
|
|
})?;
|
|
Ok(Response::new(Box::pin(stream.into_inner())))
|
|
}
|
|
|
|
#[instrument(skip(self, request), err)]
|
|
async fn stream_model_status(
|
|
&self,
|
|
request: Request<StreamModelStatusRequest>,
|
|
) -> Result<Response<Self::StreamModelStatusStream>, Status> {
|
|
let req = request.into_inner();
|
|
let interval_secs = if req.interval_seconds == 0 {
|
|
5
|
|
} else {
|
|
req.interval_seconds.clamp(1, 60)
|
|
};
|
|
let model_name = req.model_name;
|
|
let mut client = self.client.clone();
|
|
|
|
let stream = async_stream::stream! {
|
|
let mut interval = tokio::time::interval(Duration::from_secs(u64::from(interval_secs)));
|
|
loop {
|
|
interval.tick().await;
|
|
match client.get_model_status(Request::new(GetModelStatusRequest {
|
|
model_name: if model_name.is_empty() { None } else { Some(model_name.clone()) },
|
|
})).await {
|
|
Ok(resp) => yield Ok(resp.into_inner()),
|
|
Err(e) => {
|
|
error!("Backend GetModelStatus failed: {}", e);
|
|
yield Err(e);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
Ok(Response::new(Box::pin(stream)))
|
|
}
|
|
}
|