forked from pantsbuild/pants
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[internal] grpc_util: add layer to capture network-level timing data …
…(Cherry-pick of pantsbuild#15978) (pantsbuild#15999) Our REAPI client uses `concurrency_limit` to ensure only a certain maximum number of client calls run concurrently. This causes any timing metrics computed outside of that concurrency limit to include the queuing time in addition to the actual network RPC time. This PR defines a layer below the concurrency limit that will capture just the network RPC time. Timings are captured based on the URI path which maps to a specific observation metric. Units are microseconds.
- Loading branch information
Tom Dyas
authored
Jun 29, 2022
1 parent
0857d57
commit d28041a
Showing
5 changed files
with
208 additions
and
4 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
// Copyright 2022 Pants project contributors (see CONTRIBUTORS.md). | ||
// Licensed under the Apache License, Version 2.0 (see LICENSE). | ||
|
||
use std::collections::HashMap; | ||
use std::fmt; | ||
use std::pin::Pin; | ||
use std::sync::Arc; | ||
use std::task::{Context, Poll}; | ||
use std::time::Instant; | ||
|
||
use futures::ready; | ||
use futures::Future; | ||
use http::{Request, Response}; | ||
use pin_project::pin_project; | ||
use tower_layer::Layer; | ||
use tower_service::Service; | ||
use workunit_store::{get_workunit_store_handle, ObservationMetric}; | ||
|
||
#[derive(Clone, Debug)] | ||
pub struct NetworkMetricsLayer { | ||
metric_for_path: Arc<HashMap<String, ObservationMetric>>, | ||
} | ||
|
||
impl<S> Layer<S> for NetworkMetricsLayer { | ||
type Service = NetworkMetrics<S>; | ||
|
||
fn layer(&self, inner: S) -> Self::Service { | ||
NetworkMetrics::new(inner, self.metric_for_path.clone()) | ||
} | ||
} | ||
|
||
impl NetworkMetricsLayer { | ||
pub fn new(metric_for_path: &Arc<HashMap<String, ObservationMetric>>) -> Self { | ||
Self { | ||
metric_for_path: Arc::clone(metric_for_path), | ||
} | ||
} | ||
} | ||
|
||
#[derive(Clone)] | ||
pub struct NetworkMetrics<S> { | ||
inner: S, | ||
metric_for_path: Arc<HashMap<String, ObservationMetric>>, | ||
} | ||
|
||
impl<S> NetworkMetrics<S> { | ||
pub fn new(inner: S, metric_for_path: Arc<HashMap<String, ObservationMetric>>) -> Self { | ||
Self { | ||
inner, | ||
metric_for_path, | ||
} | ||
} | ||
} | ||
|
||
impl<S> fmt::Debug for NetworkMetrics<S> | ||
where | ||
S: fmt::Debug, | ||
{ | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
f.debug_struct("NetworkMetrics") | ||
.field("inner", &self.inner) | ||
.finish() | ||
} | ||
} | ||
|
||
#[pin_project] | ||
pub struct NetworkMetricsFuture<F> { | ||
#[pin] | ||
inner: F, | ||
metric_data: Option<(ObservationMetric, Instant)>, | ||
} | ||
|
||
impl<F, B, E> Future for NetworkMetricsFuture<F> | ||
where | ||
F: Future<Output = Result<Response<B>, E>>, | ||
{ | ||
type Output = Result<Response<B>, E>; | ||
|
||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { | ||
let metric_data = self.metric_data; | ||
let this = self.project(); | ||
let result = ready!(this.inner.poll(cx)); | ||
if let Some((metric, start)) = metric_data { | ||
let workunit_store_handle = get_workunit_store_handle(); | ||
if let Some(workunit_store_handle) = workunit_store_handle { | ||
workunit_store_handle | ||
.store | ||
.record_observation(metric, start.elapsed().as_micros() as u64) | ||
} | ||
} | ||
Poll::Ready(result) | ||
} | ||
} | ||
|
||
impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for NetworkMetrics<S> | ||
where | ||
S: Service<Request<ReqBody>, Response = Response<ResBody>>, | ||
{ | ||
type Response = S::Response; | ||
type Error = S::Error; | ||
type Future = NetworkMetricsFuture<S::Future>; | ||
|
||
#[inline] | ||
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 metric_data = self | ||
.metric_for_path | ||
.get(req.uri().path()) | ||
.cloned() | ||
.map(|metric| (metric, Instant::now())); | ||
NetworkMetricsFuture { | ||
inner: self.inner.call(req), | ||
metric_data, | ||
} | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use std::collections::HashMap; | ||
use std::convert::Infallible; | ||
use std::sync::Arc; | ||
|
||
use hyper::{Body, Request, Response}; | ||
use tower::{ServiceBuilder, ServiceExt}; | ||
use workunit_store::{Level, ObservationMetric, WorkunitStore}; | ||
|
||
use super::NetworkMetricsLayer; | ||
|
||
async fn handler(_: Request<Body>) -> Result<Response<Body>, Infallible> { | ||
Ok(Response::new(Body::empty())) | ||
} | ||
|
||
#[tokio::test] | ||
async fn collects_network_metrics() { | ||
let ws = WorkunitStore::new(true, Level::Debug); | ||
ws.init_thread_state(None); | ||
|
||
let metric_for_path: Arc<HashMap<String, ObservationMetric>> = { | ||
let mut m = HashMap::new(); | ||
m.insert( | ||
"/this-is-a-metric-path".to_string(), | ||
ObservationMetric::TestObservation, | ||
); | ||
Arc::new(m) | ||
}; | ||
|
||
let svc = ServiceBuilder::new() | ||
.layer(NetworkMetricsLayer::new(&metric_for_path)) | ||
.service_fn(handler); | ||
|
||
let req = Request::builder() | ||
.uri("/not-a-metric-path") | ||
.body(Body::empty()) | ||
.unwrap(); | ||
|
||
let _ = svc.clone().oneshot(req).await.unwrap(); | ||
let observations = ws.encode_observations().unwrap(); | ||
assert_eq!(observations.len(), 0); // there should be no observations for `/not-a-metric-path` | ||
|
||
let req = Request::builder() | ||
.uri("/this-is-a-metric-path") | ||
.body(Body::empty()) | ||
.unwrap(); | ||
|
||
let _ = svc.clone().oneshot(req).await.unwrap(); | ||
let observations = ws.encode_observations().unwrap(); | ||
assert_eq!(observations.len(), 1); // there should be an observation for `/this-is-a-metric-path` | ||
assert_eq!( | ||
observations.into_keys().collect::<Vec<_>>(), | ||
vec!["test_observation"] | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters