use super::{InferenceMessage, InferenceResponse, NetworkAdapter};
use crate::error::{InferenceError, InferenceResult};
use crate::streaming::StreamingEngine;
use futures::StreamExt as _;
use http_body::Body as HttpBody;
use prost::Message as ProstMessage;
use std::future::Future;
use std::net::SocketAddr;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use tokio::sync::{mpsc, RwLock};
use tokio_stream::wrappers::ReceiverStream;
use tonic::body::Body as TonicBody;
use tonic::codec::{BufferSettings, Codec, DecodeBuf, Decoder, EncodeBuf, Encoder};
use tonic::server::{Grpc, NamedService};
use tonic::{Request, Response, Status, Streaming};
use tower_service::Service;
use tracing::{debug, info};
use ::http::{Request as HttpRequest, Response as HttpResponse};
pub mod proto {
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct InferenceRequest {
#[prost(string, tag = "1")]
pub request_id: String,
#[prost(float, repeated, tag = "2")]
pub input: Vec<f32>,
#[prost(float, optional, tag = "3")]
pub temperature: Option<f32>,
#[prost(uint32, optional, tag = "4")]
pub top_k: Option<u32>,
#[prost(float, optional, tag = "5")]
pub top_p: Option<f32>,
#[prost(uint32, optional, tag = "6")]
pub max_tokens: Option<u32>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct InferenceReply {
#[prost(string, tag = "1")]
pub request_id: String,
#[prost(float, repeated, tag = "2")]
pub output: Vec<f32>,
#[prost(double, tag = "3")]
pub latency_ms: f64,
#[prost(uint32, tag = "4")]
pub num_tokens: u32,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct HealthRequest {}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct HealthReply {
#[prost(string, tag = "1")]
pub status: String,
#[prost(string, tag = "2")]
pub engine_state: String,
}
}
impl From<InferenceMessage> for proto::InferenceRequest {
fn from(msg: InferenceMessage) -> Self {
Self {
request_id: msg.request_id,
input: msg.input,
temperature: msg.config.temperature,
top_k: msg.config.top_k.map(|k| k as u32),
top_p: msg.config.top_p,
max_tokens: msg.config.max_tokens.map(|m| m as u32),
}
}
}
impl From<proto::InferenceRequest> for InferenceMessage {
fn from(req: proto::InferenceRequest) -> Self {
let mut msg = InferenceMessage::new(req.request_id, req.input);
msg.config.temperature = req.temperature;
msg.config.top_k = req.top_k.map(|k| k as usize);
msg.config.top_p = req.top_p;
msg.config.max_tokens = req.max_tokens.map(|m| m as usize);
msg
}
}
impl From<InferenceResponse> for proto::InferenceReply {
fn from(resp: InferenceResponse) -> Self {
Self {
request_id: resp.request_id,
output: resp.output,
latency_ms: resp.latency_ms,
num_tokens: resp.num_tokens as u32,
}
}
}
#[derive(Debug, Clone, Default)]
struct ProstCodec<E, D> {
_phantom: std::marker::PhantomData<(E, D)>,
}
struct ProstEncoder<T>(std::marker::PhantomData<T>);
impl<T: ProstMessage + Default> Encoder for ProstEncoder<T> {
type Item = T;
type Error = Status;
fn encode(&mut self, item: Self::Item, dst: &mut EncodeBuf<'_>) -> Result<(), Self::Error> {
item.encode(dst)
.map_err(|e| Status::internal(format!("prost encode error: {e}")))
}
fn buffer_settings(&self) -> BufferSettings {
BufferSettings::default()
}
}
struct ProstDecoder<T>(std::marker::PhantomData<T>);
impl<T: ProstMessage + Default> Decoder for ProstDecoder<T> {
type Item = T;
type Error = Status;
fn decode(&mut self, src: &mut DecodeBuf<'_>) -> Result<Option<Self::Item>, Self::Error> {
use bytes::buf::Buf as _;
let item = T::decode(src.chunk())
.map_err(|e| Status::internal(format!("prost decode error: {e}")))?;
let len = src.remaining();
src.advance(len);
Ok(Some(item))
}
fn buffer_settings(&self) -> BufferSettings {
BufferSettings::default()
}
}
impl<E, D> Codec for ProstCodec<E, D>
where
E: ProstMessage + Default + Send + 'static,
D: ProstMessage + Default + Send + 'static,
{
type Encode = E;
type Decode = D;
type Encoder = ProstEncoder<E>;
type Decoder = ProstDecoder<D>;
fn encoder(&mut self) -> Self::Encoder {
ProstEncoder(std::marker::PhantomData)
}
fn decoder(&mut self) -> Self::Decoder {
ProstDecoder(std::marker::PhantomData)
}
}
#[derive(Debug, Clone)]
pub struct GrpcServerConfig {
pub addr: String,
pub max_concurrent_streams: u32,
pub timeout_ms: u64,
pub max_frame_size: u32,
}
impl Default for GrpcServerConfig {
fn default() -> Self {
Self {
addr: "127.0.0.1:50051".to_string(),
max_concurrent_streams: 256,
timeout_ms: 30_000,
max_frame_size: 16_384,
}
}
}
struct InferenceServiceInner {
engine: Arc<RwLock<StreamingEngine>>,
}
impl InferenceServiceInner {
fn new(engine: StreamingEngine) -> Self {
Self {
engine: Arc::new(RwLock::new(engine)),
}
}
async fn infer(
&self,
request: Request<proto::InferenceRequest>,
) -> Result<Response<proto::InferenceReply>, Status> {
let req = request.into_inner();
debug!("Received inference request: {}", req.request_id);
let msg: InferenceMessage = req.into();
let input = msg.to_array();
let request_id = msg.request_id.clone();
let start = std::time::Instant::now();
let output = {
let engine = self.engine.write().await;
engine
.step_async(input)
.await
.map_err(|e| Status::internal(format!("Inference error: {e}")))?
};
let latency_ms = start.elapsed().as_secs_f64() * 1000.0;
let response =
InferenceResponse::new(request_id, output.to_vec(), latency_ms, output.len());
Ok(Response::new(response.into()))
}
async fn infer_stream_impl(
&self,
request: Request<Streaming<proto::InferenceRequest>>,
) -> Result<Response<ReceiverStream<Result<proto::InferenceReply, Status>>>, Status> {
let mut stream = request.into_inner();
let (tx, rx) = mpsc::channel::<Result<proto::InferenceReply, Status>>(32);
let engine = self.engine.clone();
tokio::spawn(async move {
while let Some(item) = stream.next().await {
match item {
Ok(req) => {
let msg: InferenceMessage = req.into();
let input = msg.to_array();
let request_id = msg.request_id.clone();
let start = std::time::Instant::now();
let result = {
let eng = engine.write().await;
eng.step_async(input).await
};
let send_result = match result {
Ok(output) => {
let latency_ms = start.elapsed().as_secs_f64() * 1000.0;
let resp = InferenceResponse::new(
request_id,
output.to_vec(),
latency_ms,
output.len(),
);
tx.send(Ok(resp.into())).await
}
Err(e) => {
tx.send(Err(Status::internal(format!("Inference error: {e}"))))
.await
}
};
if send_result.is_err() {
break;
}
}
Err(e) => {
let _ = tx.send(Err(e)).await;
break;
}
}
}
});
Ok(Response::new(ReceiverStream::new(rx)))
}
async fn health(
&self,
_request: Request<proto::HealthRequest>,
) -> Result<Response<proto::HealthReply>, Status> {
Ok(Response::new(proto::HealthReply {
status: "healthy".to_string(),
engine_state: "ready".to_string(),
}))
}
}
pub struct InferenceService {
inner: Arc<InferenceServiceInner>,
}
impl InferenceService {
pub fn new(engine: StreamingEngine) -> Self {
Self {
inner: Arc::new(InferenceServiceInner::new(engine)),
}
}
pub async fn infer(
&self,
request: Request<proto::InferenceRequest>,
) -> Result<Response<proto::InferenceReply>, Status> {
self.inner.infer(request).await
}
pub async fn infer_stream(
&self,
request: Request<Streaming<proto::InferenceRequest>>,
) -> Result<Response<ReceiverStream<Result<proto::InferenceReply, Status>>>, Status> {
self.inner.infer_stream_impl(request).await
}
pub async fn health(
&self,
request: Request<proto::HealthRequest>,
) -> Result<Response<proto::HealthReply>, Status> {
self.inner.health(request).await
}
}
const SERVICE_NAME: &str = "kizzasi.inference.InferenceService";
const PATH_INFER: &str = "/kizzasi.inference.InferenceService/Infer";
const PATH_INFER_STREAM: &str = "/kizzasi.inference.InferenceService/InferStream";
const PATH_HEALTH: &str = "/kizzasi.inference.InferenceService/Health";
#[derive(Clone)]
pub struct InferenceServiceServer {
inner: Arc<InferenceServiceInner>,
}
impl InferenceServiceServer {
pub fn new(svc: InferenceService) -> Self {
Self { inner: svc.inner }
}
}
impl NamedService for InferenceServiceServer {
const NAME: &'static str = SERVICE_NAME;
}
type StdBoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
type InferServerFuture = Pin<
Box<
dyn Future<Output = Result<HttpResponse<TonicBody>, std::convert::Infallible>>
+ Send
+ 'static,
>,
>;
impl<B> Service<HttpRequest<B>> for InferenceServiceServer
where
B: HttpBody + Send + 'static,
B::Data: Send,
B::Error: Into<StdBoxError> + Send,
{
type Response = HttpResponse<TonicBody>;
type Error = std::convert::Infallible;
type Future = InferServerFuture;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: HttpRequest<B>) -> Self::Future {
let inner = self.inner.clone();
let path = req.uri().path().to_owned();
Box::pin(async move {
match path.as_str() {
PATH_INFER => {
struct UnaryHandler(Arc<InferenceServiceInner>);
impl tonic::server::UnaryService<proto::InferenceRequest> for UnaryHandler {
type Response = proto::InferenceReply;
type Future = Pin<
Box<
dyn Future<Output = Result<Response<proto::InferenceReply>, Status>>
+ Send,
>,
>;
fn call(
&mut self,
request: Request<proto::InferenceRequest>,
) -> Self::Future {
let inner = self.0.clone();
Box::pin(async move { inner.infer(request).await })
}
}
let mut grpc: Grpc<ProstCodec<proto::InferenceReply, proto::InferenceRequest>> =
Grpc::new(ProstCodec::default());
let resp = grpc.unary(UnaryHandler(inner), req).await;
Ok(resp)
}
PATH_INFER_STREAM => {
struct StreamHandler(Arc<InferenceServiceInner>);
impl tonic::server::ClientStreamingService<proto::InferenceRequest> for StreamHandler {
type Response = proto::InferenceReply;
type Future = Pin<
Box<
dyn Future<Output = Result<Response<proto::InferenceReply>, Status>>
+ Send,
>,
>;
fn call(
&mut self,
request: Request<Streaming<proto::InferenceRequest>>,
) -> Self::Future {
let inner = self.0.clone();
Box::pin(async move {
let mut stream = request.into_inner();
let mut last_reply: Option<proto::InferenceReply> = None;
while let Some(item) = stream.next().await {
let req = item?;
let msg: InferenceMessage = req.into();
let input = msg.to_array();
let request_id = msg.request_id.clone();
let start = std::time::Instant::now();
let output = {
let eng = inner.engine.write().await;
eng.step_async(input).await.map_err(|e| {
Status::internal(format!("Inference error: {e}"))
})?
};
let latency_ms = start.elapsed().as_secs_f64() * 1000.0;
let resp = InferenceResponse::new(
request_id,
output.to_vec(),
latency_ms,
output.len(),
);
last_reply = Some(resp.into());
}
let reply = last_reply.unwrap_or_default();
Ok(Response::new(reply))
})
}
}
let mut grpc: Grpc<ProstCodec<proto::InferenceReply, proto::InferenceRequest>> =
Grpc::new(ProstCodec::default());
let resp = grpc.client_streaming(StreamHandler(inner), req).await;
Ok(resp)
}
PATH_HEALTH => {
struct HealthHandler(Arc<InferenceServiceInner>);
impl tonic::server::UnaryService<proto::HealthRequest> for HealthHandler {
type Response = proto::HealthReply;
type Future = Pin<
Box<
dyn Future<Output = Result<Response<proto::HealthReply>, Status>>
+ Send,
>,
>;
fn call(&mut self, request: Request<proto::HealthRequest>) -> Self::Future {
let inner = self.0.clone();
Box::pin(async move { inner.health(request).await })
}
}
let mut grpc: Grpc<ProstCodec<proto::HealthReply, proto::HealthRequest>> =
Grpc::new(ProstCodec::default());
let resp = grpc.unary(HealthHandler(inner), req).await;
Ok(resp)
}
_ => {
let mut response = HttpResponse::new(TonicBody::empty());
let headers = response.headers_mut();
headers.insert(
::http::header::CONTENT_TYPE,
tonic::metadata::GRPC_CONTENT_TYPE,
);
headers.insert(
tonic::Status::GRPC_STATUS,
::http::HeaderValue::from_static("12"), );
Ok(response)
}
}
})
}
}
pub struct GrpcAdapter {
service: InferenceService,
addr: SocketAddr,
running: Arc<RwLock<bool>>,
config: GrpcServerConfig,
}
impl GrpcAdapter {
pub fn new(addr: impl Into<SocketAddr>, engine: StreamingEngine) -> Self {
Self {
service: InferenceService::new(engine),
addr: addr.into(),
running: Arc::new(RwLock::new(false)),
config: GrpcServerConfig::default(),
}
}
pub fn with_config(
addr: impl Into<SocketAddr>,
engine: StreamingEngine,
config: GrpcServerConfig,
) -> Self {
Self {
service: InferenceService::new(engine),
addr: addr.into(),
running: Arc::new(RwLock::new(false)),
config,
}
}
pub fn service(&self) -> &InferenceService {
&self.service
}
pub async fn serve(&self) -> InferenceResult<()> {
info!("gRPC server listening on {}", self.addr);
*self.running.write().await = true;
let inner = self.service.inner.clone();
let svc = InferenceServiceServer { inner };
let addr = self.addr;
let mut builder = tonic::transport::Server::builder();
if self.config.max_concurrent_streams > 0 {
builder = builder.max_concurrent_streams(self.config.max_concurrent_streams);
}
if self.config.timeout_ms > 0 {
builder = builder.timeout(std::time::Duration::from_millis(self.config.timeout_ms));
}
builder
.add_service(svc)
.serve(addr)
.await
.map_err(|e| InferenceError::NetworkError(format!("gRPC transport error: {e}")))?;
*self.running.write().await = false;
Ok(())
}
pub async fn serve_with_shutdown<F>(&self, signal: F) -> InferenceResult<()>
where
F: Future<Output = ()>,
{
info!("gRPC server (with shutdown) listening on {}", self.addr);
*self.running.write().await = true;
let inner = self.service.inner.clone();
let svc = InferenceServiceServer { inner };
let addr = self.addr;
let mut builder = tonic::transport::Server::builder();
if self.config.max_concurrent_streams > 0 {
builder = builder.max_concurrent_streams(self.config.max_concurrent_streams);
}
if self.config.timeout_ms > 0 {
builder = builder.timeout(std::time::Duration::from_millis(self.config.timeout_ms));
}
builder
.add_service(svc)
.serve_with_shutdown(addr, signal)
.await
.map_err(|e| InferenceError::NetworkError(format!("gRPC transport error: {e}")))?;
*self.running.write().await = false;
Ok(())
}
}
impl NetworkAdapter for GrpcAdapter {
async fn start(&mut self) -> InferenceResult<()> {
self.serve().await
}
async fn stop(&mut self) -> InferenceResult<()> {
*self.running.write().await = false;
info!("gRPC server stopped");
Ok(())
}
fn is_running(&self) -> bool {
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async { *self.running.read().await })
})
}
}
pub struct GrpcServer {
adapter: GrpcAdapter,
}
impl GrpcServer {
pub fn new(adapter: GrpcAdapter) -> Self {
Self { adapter }
}
pub fn from_config(config: GrpcServerConfig, engine: StreamingEngine) -> InferenceResult<Self> {
let addr: SocketAddr = config
.addr
.parse()
.map_err(|e| InferenceError::InvalidConfiguration(format!("invalid addr: {e}")))?;
let adapter = GrpcAdapter::with_config(addr, engine, config);
Ok(Self { adapter })
}
pub async fn serve_until_shutdown(&self) -> InferenceResult<()> {
self.adapter.serve().await
}
pub async fn serve_with_graceful_shutdown<F>(&self, signal: F) -> InferenceResult<()>
where
F: Future<Output = ()>,
{
self.adapter.serve_with_shutdown(signal).await
}
pub fn adapter(&self) -> &GrpcAdapter {
&self.adapter
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::streaming::StreamConfig;
fn make_engine() -> StreamingEngine {
StreamingEngine::new(StreamConfig::default()).expect("StreamingEngine::new should succeed")
}
#[test]
fn test_grpc_server_config_defaults() {
let config = GrpcServerConfig::default();
assert!(
config.max_concurrent_streams > 0,
"max_concurrent_streams must be > 0"
);
assert!(config.timeout_ms > 0, "timeout_ms must be > 0");
assert!(config.max_frame_size > 0, "max_frame_size must be > 0");
}
#[test]
fn test_proto_conversion() {
let msg = InferenceMessage::new("test-1", vec![1.0, 2.0, 3.0]);
let proto_req: proto::InferenceRequest = msg.clone().into();
assert_eq!(proto_req.request_id, "test-1");
assert_eq!(proto_req.input, vec![1.0, 2.0, 3.0]);
let back: InferenceMessage = proto_req.into();
assert_eq!(back.request_id, msg.request_id);
assert_eq!(back.input, msg.input);
}
#[test]
fn test_response_proto_conversion() {
let resp = InferenceResponse::new("resp-1", vec![4.0, 5.0], 10.5, 2);
let proto_reply: proto::InferenceReply = resp.clone().into();
assert_eq!(proto_reply.request_id, "resp-1");
assert_eq!(proto_reply.output, vec![4.0, 5.0]);
assert_eq!(proto_reply.latency_ms, 10.5);
assert_eq!(proto_reply.num_tokens, 2);
}
#[tokio::test]
async fn test_inference_service_health() {
let service = InferenceService::new(make_engine());
let request = Request::new(proto::HealthRequest {});
let response = service.health(request).await;
assert!(response.is_ok(), "health() must succeed");
let reply = response.expect("health response").into_inner();
assert_eq!(reply.status, "healthy");
assert_eq!(reply.engine_state, "ready");
}
#[tokio::test]
async fn test_inference_service_unary() {
let service = InferenceService::new(make_engine());
let req = proto::InferenceRequest {
request_id: "unit-test-1".to_string(),
input: vec![0.1, 0.2, 0.3],
temperature: None,
top_k: None,
top_p: None,
max_tokens: None,
};
let response = service.infer(Request::new(req)).await;
match response {
Ok(reply) => {
assert_eq!(reply.into_inner().request_id, "unit-test-1");
}
Err(status) => {
assert!(
status.code() == tonic::Code::Internal,
"unexpected status code: {:?}",
status.code()
);
}
}
}
#[tokio::test]
async fn test_infer_stream_processes_requests() {
let inner = Arc::new(InferenceServiceInner::new(make_engine()));
let requests: Vec<proto::InferenceRequest> = (0u32..3)
.map(|i| proto::InferenceRequest {
request_id: format!("stream-{i}"),
input: vec![i as f32 * 0.1],
..Default::default()
})
.collect();
let (tx, rx) = mpsc::channel::<Result<proto::InferenceReply, Status>>(16);
let inner_clone = inner.clone();
tokio::spawn(async move {
for req in requests {
let result = inner_clone.infer(Request::new(req)).await;
match result {
Ok(resp) => {
if tx.send(Ok(resp.into_inner())).await.is_err() {
break;
}
}
Err(status) => {
let _ = tx.send(Err(status)).await;
break;
}
}
}
});
let mut count = 0usize;
let mut error_count = 0usize;
let mut receiver = ReceiverStream::new(rx);
while let Some(item) = tokio_stream::StreamExt::next(&mut receiver).await {
match item {
Ok(_reply) => count += 1,
Err(_status) => error_count += 1,
}
}
assert!(
count + error_count >= 1,
"expected at least 1 response (got {count} ok + {error_count} err)"
);
assert!(
count + error_count <= 3,
"expected at most 3 responses (got {count} ok + {error_count} err)"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_grpc_server_starts_and_stops() {
use std::net::TcpListener;
let ephemeral_addr = {
let listener = TcpListener::bind("127.0.0.1:0").expect("OS should assign a free port");
listener.local_addr().expect("local_addr must be set")
};
let engine = make_engine();
let adapter = GrpcAdapter::new(ephemeral_addr, engine);
let server = GrpcServer::new(adapter);
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
let server_handle = tokio::spawn(async move {
server
.serve_with_graceful_shutdown(async move {
let _ = shutdown_rx.await;
})
.await
});
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
let _ = shutdown_tx.send(());
let result = tokio::time::timeout(std::time::Duration::from_secs(5), server_handle).await;
match result {
Ok(join_result) => match join_result {
Ok(Ok(())) => {
}
Ok(Err(e)) => {
let msg = format!("{e}");
assert!(
msg.contains("transport")
|| msg.contains("Network")
|| msg.contains("address"),
"unexpected serve error: {msg}"
);
}
Err(e) => panic!("server task panicked: {e:?}"),
},
Err(_) => {
eprintln!("WARNING: test_grpc_server_starts_and_stops timed out — server may not have shut down cleanly");
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_grpc_adapter_creation() {
let engine = make_engine();
let addr: SocketAddr = "127.0.0.1:50051".parse().expect("valid socket addr");
let adapter = GrpcAdapter::new(addr, engine);
assert!(!adapter.is_running());
}
}