use std::sync::Arc;
use std::task::Context;
use std::task::Poll;
use std::time::Duration;
use opentelemetry::metrics::MeterProvider;
use opentelemetry::metrics::ObservableGauge;
use tower::Layer;
use tower::Service;
use crate::metrics::meter_provider;
fn start_epoch_advance_loop() -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
loop {
if let Err(e) = tikv_jemalloc_ctl::epoch::advance() {
tracing::warn!("Failed to advance jemalloc epoch: {}", e);
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
})
}
macro_rules! create_jemalloc_gauge {
($name:ident, $description:expr) => {
meter_provider()
.meter("apollo/router")
.u64_observable_gauge(concat!("apollo.router.jemalloc.", stringify!($name)))
.with_description($description)
.with_unit("bytes")
.with_callback(|gauge| {
if let Ok(value) = tikv_jemalloc_ctl::stats::$name::read() {
gauge.observe(value as u64, &[]);
} else {
tracing::warn!("Failed to read jemalloc {} stats", stringify!($name));
}
})
.build()
};
}
fn create_gauges() -> Vec<ObservableGauge<u64>> {
vec![
create_jemalloc_gauge!(active, "Total active bytes in jemalloc"),
create_jemalloc_gauge!(allocated, "Total bytes allocated by jemalloc"),
create_jemalloc_gauge!(metadata, "Total metadata bytes in jemalloc"),
create_jemalloc_gauge!(mapped, "Total mapped bytes in jemalloc"),
create_jemalloc_gauge!(resident, "Total resident bytes in jemalloc"),
create_jemalloc_gauge!(retained, "Total retained bytes in jemalloc"),
]
}
struct JemallocInstruments {
_epoch_advance_loop: tokio::task::JoinHandle<()>,
_gauges: Vec<ObservableGauge<u64>>,
}
impl JemallocInstruments {
fn new() -> Self {
Self {
_epoch_advance_loop: start_epoch_advance_loop(),
_gauges: create_gauges(),
}
}
}
impl Drop for JemallocInstruments {
fn drop(&mut self) {
self._epoch_advance_loop.abort();
}
}
#[derive(Clone)]
pub(crate) struct JemallocMetricsLayer {
instruments: Arc<JemallocInstruments>,
}
impl JemallocMetricsLayer {
pub(crate) fn new() -> Self {
Self {
instruments: Arc::new(JemallocInstruments::new()),
}
}
}
impl<S> Layer<S> for JemallocMetricsLayer {
type Service = JemallocMetricsService<S>;
fn layer(&self, inner: S) -> Self::Service {
JemallocMetricsService {
inner,
_instruments: self.instruments.clone(),
}
}
}
#[derive(Clone)]
pub(crate) struct JemallocMetricsService<S> {
inner: S,
_instruments: Arc<JemallocInstruments>,
}
impl<S, Request> Service<Request> for JemallocMetricsService<S>
where
S: Service<Request>,
{
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, req: Request) -> Self::Future {
self.inner.call(req)
}
}
#[cfg(test)]
mod tests {
use tower::ServiceExt;
use super::*;
#[tokio::test]
async fn it_passes_calls_through_to_the_inner_service() {
let layer = JemallocMetricsLayer::new();
let (inner, mut handle) = tower_test::mock::pair::<u32, u32>();
handle.allow(1);
let driver = tokio::spawn(async move {
let (request, respond) = handle
.next_request()
.await
.expect("service should be called");
respond.send_response(request * 2);
});
let mut service = layer.layer(inner);
let response = service.ready().await.unwrap().call(21).await.unwrap();
assert_eq!(response, 42);
crate::plugin::test::await_mock_driver(driver).await;
}
#[tokio::test]
async fn it_stops_the_epoch_advance_loop_once_every_clone_is_dropped() {
let layer = JemallocMetricsLayer::new();
let abort_handle = layer.instruments._epoch_advance_loop.abort_handle();
let (inner, handle) = tower_test::mock::pair::<u32, u32>();
let service = layer.layer(inner);
let cloned_service = service.clone();
drop(layer);
drop(service);
assert!(
!abort_handle.is_finished(),
"the loop should still be running while a clone of the service is alive"
);
drop(cloned_service);
tokio::task::yield_now().await;
assert!(
abort_handle.is_finished(),
"the loop should stop once every instance sharing the instruments is dropped"
);
crate::plugin::test::assert_no_mock_calls(handle).await;
}
}