use std::cell::RefCell;
use std::future::Future;
use std::rc::Rc;
use futures::future::{FutureExt, LocalBoxFuture, Shared};
use futures::stream::{LocalBoxStream, Stream, StreamExt};
use h2ts_client::{
ConnectOptions, H2Connection, RequestBody, RequestInit, Response, Trailers, Transport,
};
use crate::codec::{encode_message, Deframer};
use crate::metadata::Metadata;
use crate::state::{ConnectivityState, StateWatch};
use crate::status::{Code, Status};
pub type Connector =
Rc<dyn Fn() -> LocalBoxFuture<'static, Result<H2Connection, Status>>>;
type SharedDial = Shared<LocalBoxFuture<'static, Result<Rc<H2Connection>, Status>>>;
const CONTENT_TYPE: &str = "application/grpc+proto";
#[derive(Debug, Clone, Default)]
pub struct CallOptions {
pub metadata: Metadata,
pub timeout: Option<std::time::Duration>,
pub max_message_bytes: Option<usize>,
}
impl CallOptions {
pub fn new() -> CallOptions {
CallOptions::default()
}
pub fn with_metadata(mut self, metadata: Metadata) -> Self {
self.metadata = metadata;
self
}
pub fn with_timeout(mut self, timeout: std::time::Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn with_max_message_bytes(mut self, max: usize) -> Self {
self.max_message_bytes = Some(max);
self
}
}
#[derive(Clone)]
pub struct Client {
inner: Rc<Inner>,
}
struct Inner {
connector: Option<Connector>,
tunnel: RefCell<Option<Rc<H2Connection>>>,
dialing: RefCell<Option<SharedDial>>,
state: StateWatch,
authority: String,
}
impl Client {
pub fn from_connection(conn: H2Connection, authority: impl Into<String>) -> Client {
let state = StateWatch::default();
state.set(ConnectivityState::Ready);
Client {
inner: Rc::new(Inner {
connector: None,
tunnel: RefCell::new(Some(Rc::new(conn))),
dialing: RefCell::new(None),
state,
authority: authority.into(),
}),
}
}
pub fn with_connector(connector: Connector, authority: impl Into<String>) -> Client {
Client {
inner: Rc::new(Inner {
connector: Some(connector),
tunnel: RefCell::new(None),
dialing: RefCell::new(None),
state: StateWatch::default(),
authority: authority.into(),
}),
}
}
pub fn over_transport(
transport: Transport,
authority: impl Into<String>,
options: ConnectOptions,
) -> (Client, impl Future<Output = ()>) {
let (conn, driver) = h2ts_client::connect(transport, options);
(Client::from_connection(conn, authority), driver)
}
pub fn state(&self) -> ConnectivityState {
if matches!(self.inner.state.get(), ConnectivityState::Ready)
&& self.inner.tunnel.borrow().as_ref().is_none_or(|c| c.is_closed())
{
self.inner.state.set(ConnectivityState::Idle);
}
self.inner.state.get()
}
pub fn state_changes(&self) -> impl Stream<Item = ConnectivityState> + 'static {
self.inner.state.watch()
}
pub fn is_closed(&self) -> bool {
self.inner.tunnel.borrow().as_ref().is_none_or(|c| c.is_closed())
}
async fn tunnel(&self) -> Result<Rc<H2Connection>, Status> {
let cached_is_dead = {
let tunnel = self.inner.tunnel.borrow();
match tunnel.as_ref() {
Some(conn) if !conn.is_closed() => return Ok(conn.clone()),
Some(_) => true,
None => false,
}
};
if cached_is_dead {
self.inner.tunnel.borrow_mut().take();
self.inner.state.set(ConnectivityState::Idle);
}
let Some(connector) = self.inner.connector.clone() else {
return Err(Status::unavailable(
"the tunnel is closed and this client cannot redial \
(built over a caller-supplied transport)",
));
};
let dial = {
let mut dialing = self.inner.dialing.borrow_mut();
match dialing.as_ref() {
Some(shared) => shared.clone(),
None => {
self.inner.state.set(ConnectivityState::Connecting);
let shared: SharedDial =
async move { connector().await.map(Rc::new) }.boxed_local().shared();
*dialing = Some(shared.clone());
shared
}
}
};
let result = dial.await;
self.inner.dialing.borrow_mut().take();
match result {
Ok(conn) => {
self.inner.state.set(ConnectivityState::Ready);
*self.inner.tunnel.borrow_mut() = Some(conn.clone());
Ok(conn)
}
Err(e) => {
self.inner.state.set(ConnectivityState::TransientFailure);
Err(e)
}
}
}
fn forget(&self, dead: &Rc<H2Connection>) {
let mut tunnel = self.inner.tunnel.borrow_mut();
if tunnel.as_ref().is_some_and(|c| Rc::ptr_eq(c, dead)) {
tunnel.take();
self.inner.state.set(ConnectivityState::Idle);
}
}
pub async fn unary(
&self,
path: &str,
request: Vec<u8>,
options: CallOptions,
) -> Result<UnaryResponse, Status> {
self.single_response(path, RequestBody::Bytes(encode_message(&request)), &options).await
}
pub async fn client_streaming<S>(
&self,
path: &str,
requests: S,
options: CallOptions,
) -> Result<UnaryResponse, Status>
where
S: Stream<Item = Vec<u8>> + 'static,
{
let body = RequestBody::stream(requests.map(|m| encode_message(&m)));
self.single_response(path, body, &options).await
}
pub async fn server_streaming(
&self,
path: &str,
request: Vec<u8>,
options: CallOptions,
) -> Result<Streaming, Status> {
let body = RequestBody::Bytes(encode_message(&request));
self.open_stream(path, body, &options).await
}
pub async fn bidi_streaming<S>(
&self,
path: &str,
requests: S,
options: CallOptions,
) -> Result<Streaming, Status>
where
S: Stream<Item = Vec<u8>> + 'static,
{
let body = RequestBody::stream(requests.map(|m| encode_message(&m)));
self.open_stream(path, body, &options).await
}
async fn open_stream(
&self,
path: &str,
body: RequestBody,
options: &CallOptions,
) -> Result<Streaming, Status> {
use futures::future::{select, Either};
let Some(timeout) = options.timeout else {
return Ok(Streaming::new(self.request(path, body, options).await?, options, None));
};
let mut timer = futures_timer::Delay::new(timeout);
let opened = {
let open = self.request(path, body, options);
futures::pin_mut!(open);
match select(open, &mut timer).await {
Either::Left((response, _)) => Some(response),
Either::Right(((), _)) => None,
}
};
match opened {
Some(response) => Ok(Streaming::new(response?, options, Some(timer))),
None => Err(Status::new(Code::DeadlineExceeded, "deadline exceeded")),
}
}
async fn single_response(
&self,
path: &str,
body: RequestBody,
options: &CallOptions,
) -> Result<UnaryResponse, Status> {
match options.timeout {
Some(timeout) => {
deadline(timeout, self.single_response_inner(path, body, options)).await
}
None => self.single_response_inner(path, body, options).await,
}
}
async fn single_response_inner(
&self,
path: &str,
body: RequestBody,
options: &CallOptions,
) -> Result<UnaryResponse, Status> {
let mut response = self.request(path, body, options).await?;
let bytes = response
.bytes()
.await
.map_err(|e| Status::unavailable(format!("response body failed: {e}")))?;
let mut deframer = Deframer::new(options.max_message_bytes);
let messages = deframer.push(&bytes)?;
if deframer.pending() > 0 {
return Err(Status::new(Code::Internal, "response body ended mid-message (truncated)"));
}
let status = match response.trailers() {
Some(trailers) => Status::from_headers(&trailers).unwrap_or_else(|| {
Status::new(Code::Internal, "response trailers carried no grpc-status")
}),
None => Status::new(Code::Internal, "response ended without trailers"),
};
if !status.is_ok() {
return Err(status);
}
let message = messages
.into_iter()
.next()
.ok_or_else(|| Status::new(Code::Internal, "response carried no message"))?;
Ok(UnaryResponse {
message,
headers: Metadata::from_headers(&response.headers),
trailers: status.metadata,
})
}
async fn request(
&self,
path: &str,
body: RequestBody,
options: &CallOptions,
) -> Result<Response, Status> {
let conn = self.tunnel().await?;
let mut headers = vec![
("content-type".to_string(), CONTENT_TYPE.to_string()),
("te".to_string(), "trailers".to_string()),
];
if let Some(timeout) = options.timeout {
headers.push(("grpc-timeout".to_string(), format!("{}m", options_millis(timeout))));
}
headers.extend(options.metadata.to_headers());
let response = match conn
.request(RequestInit {
method: Some("POST".to_string()),
path: Some(path.to_string()),
authority: Some(self.inner.authority.clone()),
scheme: Some("http".to_string()),
headers,
body,
})
.await
{
Ok(response) => response,
Err(e) => {
if conn.is_closed() {
self.forget(&conn);
}
return Err(Status::unavailable(format!("request failed: {e}")));
}
};
if response.status != 200 {
return Err(Status::unavailable(format!("HTTP {}", response.status)));
}
if let Some(status) = Status::from_headers(&response.headers) {
return Err(if status.is_ok() {
Status::new(Code::Internal, "trailers-only response reported OK with no message")
} else {
status
});
}
Ok(response)
}
}
fn options_millis(timeout: std::time::Duration) -> u128 {
timeout.as_millis().max(1)
}
async fn deadline<T>(
timeout: std::time::Duration,
work: impl std::future::Future<Output = Result<T, Status>>,
) -> Result<T, Status> {
use futures::future::{select, Either};
let timer = futures_timer::Delay::new(timeout);
futures::pin_mut!(work);
futures::pin_mut!(timer);
match select(work, timer).await {
Either::Left((result, _)) => result,
Either::Right(((), _)) => {
Err(Status::new(Code::DeadlineExceeded, "deadline exceeded"))
}
}
}
pub struct Streaming {
pub headers: Metadata,
body: LocalBoxStream<'static, Result<Vec<u8>, h2ts_client::H2Error>>,
trailers: Trailers,
deframer: Deframer,
ready: std::collections::VecDeque<Vec<u8>>,
ended: bool,
failed: Option<Status>,
deadline: Option<futures_timer::Delay>,
}
impl std::fmt::Debug for Streaming {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Streaming")
.field("headers", &self.headers)
.field("ended", &self.ended)
.field("failed", &self.failed)
.finish_non_exhaustive()
}
}
impl Streaming {
fn new(
response: Response,
options: &CallOptions,
deadline: Option<futures_timer::Delay>,
) -> Streaming {
let headers = Metadata::from_headers(&response.headers);
let (body, trailers) = response.into_parts();
Streaming {
headers,
body: body.boxed_local(),
trailers,
deframer: Deframer::new(options.max_message_bytes),
ready: Default::default(),
ended: false,
failed: None,
deadline,
}
}
pub async fn message(&mut self) -> Result<Option<Vec<u8>>, Status> {
loop {
if let Some(message) = self.ready.pop_front() {
return Ok(Some(message));
}
if let Some(status) = self.failed.clone() {
return Err(status);
}
if self.ended {
return Ok(None);
}
let chunk = {
use futures::future::{select, Either};
let body = &mut self.body;
match self.deadline.as_mut() {
Some(timer) => match select(body.next(), timer).await {
Either::Left((chunk, _)) => Some(chunk),
Either::Right(((), _)) => None,
},
None => Some(body.next().await),
}
};
let Some(chunk) = chunk else {
return Err(self.fail(Status::new(Code::DeadlineExceeded, "deadline exceeded")));
};
match chunk {
Some(Ok(chunk)) => match self.deframer.push(&chunk) {
Ok(messages) => self.ready.extend(messages),
Err(status) => return Err(self.fail(status)),
},
Some(Err(e)) => {
return Err(self.fail(Status::unavailable(format!("stream failed: {e}"))))
}
None => {
self.ended = true;
if self.deframer.pending() > 0 {
return Err(self.fail(Status::new(
Code::Internal,
"response body ended mid-message (truncated)",
)));
}
}
}
}
}
pub fn status(&self) -> Status {
if let Some(status) = &self.failed {
return status.clone();
}
match self.trailers.get() {
Some(trailers) => Status::from_headers(&trailers).unwrap_or_else(|| {
Status::new(Code::Internal, "response trailers carried no grpc-status")
}),
None => Status::new(Code::Internal, "response ended without trailers"),
}
}
pub async fn finish(&mut self) -> Status {
loop {
match self.message().await {
Ok(Some(_)) => continue,
Ok(None) => return self.status(),
Err(status) => return status,
}
}
}
fn fail(&mut self, status: Status) -> Status {
self.ended = true;
self.failed = Some(status.clone());
status
}
}
#[derive(Debug, Clone)]
pub struct UnaryResponse {
pub message: Vec<u8>,
pub headers: Metadata,
pub trailers: Metadata,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_sub_millisecond_timeout_never_becomes_zero() {
assert_eq!(options_millis(std::time::Duration::from_micros(1)), 1);
assert_eq!(options_millis(std::time::Duration::from_millis(250)), 250);
}
}