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>
139 lines
4.4 KiB
Rust
139 lines
4.4 KiB
Rust
//! Data Acquisition Service Proxy - Zero-copy gRPC forwarding
|
|
//!
|
|
//! Forwards to data-acquisition-service:50057 (separate service).
|
|
//! All RPCs are unary (no streaming) — simplest proxy.
|
|
|
|
use futures::Stream;
|
|
use std::pin::Pin;
|
|
use std::time::Duration;
|
|
use tonic::{Request, Response, Status};
|
|
use tracing::{error, instrument};
|
|
|
|
use crate::data_acquisition::data_acquisition_service_client::DataAcquisitionServiceClient;
|
|
use crate::data_acquisition::data_acquisition_service_server::DataAcquisitionService;
|
|
use crate::data_acquisition::{
|
|
CancelDownloadRequest, CancelDownloadResponse, GetDownloadStatusRequest,
|
|
GetDownloadStatusResponse, HealthCheckRequest, HealthCheckResponse,
|
|
ListDownloadJobsRequest, ListDownloadJobsResponse, ScheduleDownloadRequest,
|
|
ScheduleDownloadResponse, StreamDownloadStatusRequest,
|
|
};
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct DataAcquisitionProxy {
|
|
client: DataAcquisitionServiceClient<tonic::transport::Channel>,
|
|
}
|
|
|
|
impl DataAcquisitionProxy {
|
|
pub fn new(client: DataAcquisitionServiceClient<tonic::transport::Channel>) -> Self {
|
|
Self { client }
|
|
}
|
|
}
|
|
|
|
#[tonic::async_trait]
|
|
impl DataAcquisitionService for DataAcquisitionProxy {
|
|
#[instrument(skip(self, request), err)]
|
|
async fn schedule_download(
|
|
&self,
|
|
request: Request<ScheduleDownloadRequest>,
|
|
) -> Result<Response<ScheduleDownloadResponse>, Status> {
|
|
self.client
|
|
.clone()
|
|
.schedule_download(request)
|
|
.await
|
|
.map_err(|e| {
|
|
error!("Backend ScheduleDownload failed: {}", e);
|
|
e
|
|
})
|
|
}
|
|
|
|
#[instrument(skip(self, request), err)]
|
|
async fn get_download_status(
|
|
&self,
|
|
request: Request<GetDownloadStatusRequest>,
|
|
) -> Result<Response<GetDownloadStatusResponse>, Status> {
|
|
self.client
|
|
.clone()
|
|
.get_download_status(request)
|
|
.await
|
|
.map_err(|e| {
|
|
error!("Backend GetDownloadStatus failed: {}", e);
|
|
e
|
|
})
|
|
}
|
|
|
|
#[instrument(skip(self, request), err)]
|
|
async fn cancel_download(
|
|
&self,
|
|
request: Request<CancelDownloadRequest>,
|
|
) -> Result<Response<CancelDownloadResponse>, Status> {
|
|
self.client
|
|
.clone()
|
|
.cancel_download(request)
|
|
.await
|
|
.map_err(|e| {
|
|
error!("Backend CancelDownload failed: {}", e);
|
|
e
|
|
})
|
|
}
|
|
|
|
#[instrument(skip(self, request), err)]
|
|
async fn list_download_jobs(
|
|
&self,
|
|
request: Request<ListDownloadJobsRequest>,
|
|
) -> Result<Response<ListDownloadJobsResponse>, Status> {
|
|
self.client
|
|
.clone()
|
|
.list_download_jobs(request)
|
|
.await
|
|
.map_err(|e| {
|
|
error!("Backend ListDownloadJobs failed: {}", e);
|
|
e
|
|
})
|
|
}
|
|
|
|
#[instrument(skip(self, request), err)]
|
|
async fn health_check(
|
|
&self,
|
|
request: Request<HealthCheckRequest>,
|
|
) -> Result<Response<HealthCheckResponse>, Status> {
|
|
self.client.clone().health_check(request).await.map_err(|e| {
|
|
error!("Backend HealthCheck failed: {}", e);
|
|
e
|
|
})
|
|
}
|
|
|
|
type StreamDownloadStatusStream =
|
|
Pin<Box<dyn Stream<Item = Result<ListDownloadJobsResponse, Status>> + Send>>;
|
|
|
|
#[instrument(skip(self, request), err)]
|
|
async fn stream_download_status(
|
|
&self,
|
|
request: Request<StreamDownloadStatusRequest>,
|
|
) -> Result<Response<Self::StreamDownloadStatusStream>, Status> {
|
|
let req = request.into_inner();
|
|
let interval_secs = if req.interval_seconds == 0 {
|
|
5
|
|
} else {
|
|
req.interval_seconds.clamp(1, 60)
|
|
};
|
|
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.list_download_jobs(Request::new(ListDownloadJobsRequest::default())).await {
|
|
Ok(resp) => yield Ok(resp.into_inner()),
|
|
Err(e) => {
|
|
error!("Backend ListDownloadJobs failed: {}", e);
|
|
yield Err(e);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
Ok(Response::new(Box::pin(stream)))
|
|
}
|
|
}
|