use bytes::{Buf, BufMut, Bytes, BytesMut};
use h2::client::{self, SendRequest};
use http::{Request, Uri};
use std::sync::Arc;
use tokio::net::TcpStream;
use tokio::sync::Mutex;
use crate::error::{QdrantError, QdrantResult};
const GRPC_CONTENT_TYPE: &str = "application/grpc";
#[allow(dead_code)]
const POINTS_SERVICE: &str = "qdrant.Points";
const METHOD_SEARCH: &str = "/qdrant.Points/Search";
const METHOD_UPSERT: &str = "/qdrant.Points/Upsert";
const METHOD_DELETE: &str = "/qdrant.Points/Delete";
const METHOD_GET: &str = "/qdrant.Points/Get";
const METHOD_SCROLL: &str = "/qdrant.Points/Scroll";
const METHOD_RECOMMEND: &str = "/qdrant.Points/Recommend";
const METHOD_CREATE_COLLECTION: &str = "/qdrant.Collections/Create";
const METHOD_DELETE_COLLECTION: &str = "/qdrant.Collections/Delete";
pub struct GrpcClient {
sender: Arc<Mutex<SendRequest<Bytes>>>,
#[allow(dead_code)]
uri: Uri,
#[allow(dead_code)]
buffer: Mutex<BytesMut>,
}
impl GrpcClient {
pub async fn connect(host: &str, port: u16) -> QdrantResult<Self> {
let addr = format!("{}:{}", host, port);
let stream = TcpStream::connect(&addr)
.await
.map_err(|e| QdrantError::Connection(format!("TCP connect failed: {}", e)))?;
let (sender, connection) = client::handshake(stream)
.await
.map_err(|e| QdrantError::Connection(format!("H2 handshake failed: {}", e)))?;
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("gRPC connection error: {}", e);
}
});
let uri: Uri = format!("http://{}:{}", host, port)
.parse()
.map_err(|e| QdrantError::Connection(format!("Invalid URI: {}", e)))?;
Ok(Self {
sender: Arc::new(Mutex::new(sender)),
uri,
buffer: Mutex::new(BytesMut::with_capacity(8192)),
})
}
pub async fn call(&self, method: &str, body: Bytes) -> QdrantResult<Bytes> {
let framed = grpc_frame(body);
let request = Request::builder()
.method("POST")
.uri(method)
.header("content-type", GRPC_CONTENT_TYPE)
.header("te", "trailers")
.body(())
.map_err(|e| QdrantError::Encode(format!("Request build failed: {}", e)))?;
let sender = self.sender.lock().await;
let mut ready_sender = sender.clone().ready().await
.map_err(|e| QdrantError::Grpc(format!("Connection not ready: {}", e)))?;
let (response, mut send_body) = ready_sender
.send_request(request, false)
.map_err(|e| QdrantError::Grpc(format!("Send request failed: {}", e)))?;
drop(sender);
send_body
.send_data(framed, true)
.map_err(|e| QdrantError::Grpc(format!("Send body failed: {}", e)))?;
let (head, mut body) = response
.await
.map_err(|e| QdrantError::Grpc(format!("Response failed: {}", e)))?
.into_parts();
if head.status != http::StatusCode::OK {
return Err(QdrantError::Grpc(format!(
"gRPC error: HTTP {}",
head.status
)));
}
let mut response_buf = BytesMut::new();
while let Some(chunk) = body.data().await {
let chunk = chunk.map_err(|e| QdrantError::Decode(format!("Body read failed: {}", e)))?;
response_buf.extend_from_slice(&chunk);
let _ = body.flow_control().release_capacity(chunk.len());
}
let trailers = body.trailers().await
.map_err(|e| QdrantError::Grpc(format!("Trailers failed: {}", e)))?;
if let Some(trailers) = trailers
&& let Some(status) = trailers.get("grpc-status")
&& status != "0"
{
let message = trailers
.get("grpc-message")
.and_then(|v| v.to_str().ok())
.unwrap_or("Unknown error");
return Err(QdrantError::Grpc(format!(
"gRPC status {}: {}",
status.to_str().unwrap_or("?"),
message
)));
}
let response_bytes = grpc_unframe(response_buf.freeze())?;
Ok(response_bytes)
}
pub async fn search(&self, encoded_request: Bytes) -> QdrantResult<Bytes> {
self.call(METHOD_SEARCH, encoded_request).await
}
pub async fn upsert(&self, encoded_request: Bytes) -> QdrantResult<Bytes> {
self.call(METHOD_UPSERT, encoded_request).await
}
pub async fn delete(&self, encoded_request: Bytes) -> QdrantResult<Bytes> {
self.call(METHOD_DELETE, encoded_request).await
}
pub async fn get(&self, encoded_request: Bytes) -> QdrantResult<Bytes> {
self.call(METHOD_GET, encoded_request).await
}
pub async fn scroll(&self, encoded_request: Bytes) -> QdrantResult<Bytes> {
self.call(METHOD_SCROLL, encoded_request).await
}
pub async fn recommend(&self, encoded_request: Bytes) -> QdrantResult<Bytes> {
self.call(METHOD_RECOMMEND, encoded_request).await
}
pub async fn create_collection(&self, encoded_request: Bytes) -> QdrantResult<Bytes> {
self.call(METHOD_CREATE_COLLECTION, encoded_request).await
}
pub async fn delete_collection(&self, encoded_request: Bytes) -> QdrantResult<Bytes> {
self.call(METHOD_DELETE_COLLECTION, encoded_request).await
}
}
fn grpc_frame(message: Bytes) -> Bytes {
let len = message.len();
let mut frame = BytesMut::with_capacity(5 + len);
frame.put_u8(0);
frame.put_u32(len as u32);
frame.extend_from_slice(&message);
frame.freeze()
}
fn grpc_unframe(mut data: Bytes) -> QdrantResult<Bytes> {
if data.is_empty() {
return Ok(Bytes::new());
}
if data.len() < 5 {
return Err(QdrantError::Decode("Response too short for gRPC frame".to_string()));
}
let _compress = data.get_u8();
let len = data.get_u32() as usize;
if len == 0 {
return Ok(Bytes::new());
}
if data.len() < len {
return Err(QdrantError::Decode(format!(
"Response truncated: expected {} bytes, got {}",
len,
data.len()
)));
}
Ok(data.slice(0..len))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_grpc_frame() {
let message = Bytes::from_static(b"hello");
let framed = grpc_frame(message);
assert_eq!(framed.len(), 10);
assert_eq!(framed[0], 0); assert_eq!(&framed[1..5], &[0, 0, 0, 5]); assert_eq!(&framed[5..], b"hello");
}
#[test]
fn test_grpc_unframe() {
let mut data = BytesMut::new();
data.put_u8(0); data.put_u32(5); data.extend_from_slice(b"hello");
let result = grpc_unframe(data.freeze()).unwrap();
assert_eq!(&result[..], b"hello");
}
}