use crate::operation::{current_operation, with_operation, OPERATION_HEADER};
use http::{HeaderValue, Request, Response};
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use tower::{Layer, Service};
#[derive(Debug, Clone, Default)]
pub struct AttachOperationLayer;
impl<S> Layer<S> for AttachOperationLayer {
type Service = AttachOperationService<S>;
fn layer(&self, inner: S) -> Self::Service {
AttachOperationService { inner }
}
}
#[derive(Debug, Clone)]
pub struct AttachOperationService<S> {
inner: S,
}
impl<S, ReqBody> Service<Request<ReqBody>> for AttachOperationService<S>
where
S: Service<Request<ReqBody>>,
{
type Response = S::Response;
type Error = S::Error;
type Future = S::Future;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, mut req: Request<ReqBody>) -> Self::Future {
if let Some(operation_id) = current_operation() {
if let Ok(value) = HeaderValue::from_str(&operation_id) {
req.headers_mut().insert(OPERATION_HEADER, value);
}
}
self.inner.call(req)
}
}
#[derive(Debug, Clone, Default)]
pub struct PropagateOperationLayer;
impl<S> Layer<S> for PropagateOperationLayer {
type Service = PropagateOperationService<S>;
fn layer(&self, inner: S) -> Self::Service {
PropagateOperationService { inner }
}
}
#[derive(Debug, Clone)]
pub struct PropagateOperationService<S> {
inner: S,
}
impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for PropagateOperationService<S>
where
S: Service<Request<ReqBody>, Response = Response<ResBody>> + Clone + Send + 'static,
S::Future: Send + 'static,
ReqBody: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
let operation_id = req
.headers()
.get(OPERATION_HEADER)
.and_then(|v| v.to_str().ok())
.map(str::to_string);
let clone = self.inner.clone();
let mut inner = std::mem::replace(&mut self.inner, clone);
Box::pin(async move { with_operation(operation_id, inner.call(req)).await })
}
}
#[cfg(test)]
mod test_grpc {
use super::*;
use crate::operation::with_operation;
use http_body_util::Empty;
use std::convert::Infallible;
use tower::service_fn;
use tower::ServiceExt;
fn empty_request() -> Request<Empty<bytes::Bytes>> {
Request::new(Empty::new())
}
#[tokio::test]
async fn attach_sets_the_header_from_the_current_task() {
let recorded = std::sync::Arc::new(std::sync::Mutex::new(None));
let recorded_clone = recorded.clone();
let inner = service_fn(move |req: Request<Empty<bytes::Bytes>>| {
let recorded = recorded_clone.clone();
async move {
*recorded.lock().unwrap() = req
.headers()
.get(OPERATION_HEADER)
.and_then(|v| v.to_str().ok())
.map(str::to_string);
Ok::<_, Infallible>(Response::new(Empty::<bytes::Bytes>::new()))
}
});
let mut svc = AttachOperationLayer.layer(inner);
with_operation(Some("op-123".to_string()), async {
svc.ready()
.await
.unwrap()
.call(empty_request())
.await
.unwrap();
})
.await;
assert_eq!(*recorded.lock().unwrap(), Some("op-123".to_string()));
}
#[tokio::test]
async fn attach_leaves_the_header_absent_without_a_bound_operation() {
let recorded = std::sync::Arc::new(std::sync::Mutex::new(Some("untouched".to_string())));
let recorded_clone = recorded.clone();
let inner = service_fn(move |req: Request<Empty<bytes::Bytes>>| {
let recorded = recorded_clone.clone();
async move {
*recorded.lock().unwrap() = req
.headers()
.get(OPERATION_HEADER)
.and_then(|v| v.to_str().ok())
.map(str::to_string);
Ok::<_, Infallible>(Response::new(Empty::<bytes::Bytes>::new()))
}
});
let mut svc = AttachOperationLayer.layer(inner);
svc.ready()
.await
.unwrap()
.call(empty_request())
.await
.unwrap();
assert_eq!(*recorded.lock().unwrap(), None);
}
#[tokio::test]
async fn propagate_opens_the_scope_the_handler_reads() {
let inner = service_fn(|_req: Request<Empty<bytes::Bytes>>| async move {
let seen = current_operation();
Ok::<_, Infallible>(Response::new(Empty::<bytes::Bytes>::new()).map(|_| seen))
});
let mut svc = PropagateOperationLayer.layer(inner);
let mut req = empty_request();
req.headers_mut().insert(
OPERATION_HEADER,
HeaderValue::from_static("op-from-metadata"),
);
let resp = svc.ready().await.unwrap().call(req).await.unwrap();
assert_eq!(resp.into_body(), Some("op-from-metadata".to_string()));
}
#[tokio::test]
async fn propagate_binds_none_without_the_header() {
let inner = service_fn(|_req: Request<Empty<bytes::Bytes>>| async move {
let seen = current_operation();
Ok::<_, Infallible>(Response::new(Empty::<bytes::Bytes>::new()).map(|_| seen))
});
let mut svc = PropagateOperationLayer.layer(inner);
let resp = svc
.ready()
.await
.unwrap()
.call(empty_request())
.await
.unwrap();
assert_eq!(resp.into_body(), None);
}
#[tokio::test]
async fn propagate_then_attach_forwards_the_operation_across_a_hop() {
let downstream_recorded = std::sync::Arc::new(std::sync::Mutex::new(None));
let downstream_recorded_clone = downstream_recorded.clone();
let downstream = service_fn(move |req: Request<Empty<bytes::Bytes>>| {
let recorded = downstream_recorded_clone.clone();
async move {
*recorded.lock().unwrap() = req
.headers()
.get(OPERATION_HEADER)
.and_then(|v| v.to_str().ok())
.map(str::to_string);
Ok::<_, Infallible>(Response::new(Empty::<bytes::Bytes>::new()))
}
});
let downstream_client = AttachOperationLayer.layer(downstream);
let handler = tower::service_fn(move |_req: Request<Empty<bytes::Bytes>>| {
let mut downstream_client = downstream_client.clone();
async move {
downstream_client
.ready()
.await
.unwrap()
.call(Request::new(Empty::new()))
.await
.unwrap();
Ok::<_, Infallible>(Response::new(Empty::<bytes::Bytes>::new()))
}
});
let mut incoming_server = PropagateOperationLayer.layer(handler);
let mut req = empty_request();
req.headers_mut().insert(
OPERATION_HEADER,
HeaderValue::from_static("op-hopping-through"),
);
incoming_server
.ready()
.await
.unwrap()
.call(req)
.await
.unwrap();
assert_eq!(
*downstream_recorded.lock().unwrap(),
Some("op-hopping-through".to_string())
);
}
}