Skip to content

Commit fed7da4

Browse files
feat(dogstatsd): add shared_runtime buffered sink
1 parent b905f96 commit fed7da4

4 files changed

Lines changed: 234 additions & 1 deletion

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libdd-dogstatsd-client/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@ serde = { version = "1.0", features = ["derive", "rc"] }
1818
tracing = { version = "0.1", default-features = false }
1919
anyhow = { version = "1.0" }
2020
http = "1.1"
21+
libdd-shared-runtime = { version = "2.0.0", path = "../libdd-shared-runtime", optional = true }
22+
tokio = { version = "1.23", features = ["sync"], optional = true }
23+
async-trait = { version = "0.1", optional = true }
2124

2225
[features]
2326
default = ["https"]
2427
https = ["libdd-common/https"]
2528
fips = ["libdd-common/fips"]
26-
29+
shared-runtime = ["dep:libdd-shared-runtime", "dep:tokio", "dep:async-trait"]
2730

2831
[dev-dependencies]
2932
tokio = {version = "1.23", features = ["rt", "time", "test-util", "rt-multi-thread"], default-features = false}

libdd-dogstatsd-client/src/client/mod.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use cadence::prelude::*;
66
use cadence::{Metric, MetricBuilder, QueuingMetricSink, StatsdClient};
77
use libdd_common::tag::Tag;
88
use libdd_common::Endpoint;
9+
#[cfg(feature = "shared-runtime")]
10+
use libdd_shared_runtime::{SharedRuntime, WorkerHandle};
911
use sink::create_udp_sink;
1012
#[cfg(unix)]
1113
use sink::create_unix_sink;
@@ -15,6 +17,10 @@ use tracing::error;
1517
/// Provides transport sink which can be wrapped in buffered sink
1618
mod sink;
1719

20+
/// Provides a buffered sink running on a [libdd_shared_runtime::SharedRuntime]
21+
#[cfg(feature = "shared-runtime")]
22+
mod shared_runtime_sink;
23+
1824
// Queue with a maximum capacity of 32K elements
1925
const QUEUE_SIZE: usize = 32 * 1024;
2026

@@ -47,6 +53,31 @@ impl DogStatsDClient {
4753
})
4854
}
4955

56+
/// Create a [`Client`] backed by a [`MetricSinkWorker`] running on the
57+
/// provided [`SharedRuntime`].
58+
///
59+
/// Returns the client and a [`WorkerHandle`] that can be used to stop the
60+
/// worker independently of the runtime.
61+
///
62+
/// # Errors
63+
/// Returns an error if the endpoint is invalid or the worker cannot be spawned.
64+
#[cfg(feature = "shared-runtime")]
65+
pub fn new_with_shared_runtime(
66+
endpoint: Endpoint,
67+
runtime: &impl SharedRuntime,
68+
) -> anyhow::Result<(Self, WorkerHandle)> {
69+
let (sink, handle) = shared_runtime_sink::create_shared_runtime_sink(&endpoint, runtime)?;
70+
71+
let client = Self {
72+
inner: Arc::new(InnerClient {
73+
client: OnceLock::from(StatsdClient::from_sink("", sink)),
74+
endpoint,
75+
}),
76+
};
77+
78+
Ok((client, handle))
79+
}
80+
5081
/// Send a vector of DogStatsDActionOwned, this is the same as `send` except it uses the
5182
/// "owned" version of DogStatsDAction. See the docs for DogStatsDActionOwned for details.
5283
pub fn send_owned(&self, actions: Vec<DogStatsDActionOwned>) {
@@ -243,4 +274,51 @@ mod test {
243274
task.await.unwrap();
244275
}
245276
}
277+
278+
#[test]
279+
#[cfg_attr(miri, ignore)]
280+
#[cfg(feature = "shared-runtime")]
281+
fn test_shared_runtime_flusher() {
282+
use libdd_shared_runtime::{BasicRuntime, BlockingRuntime, SharedRuntime};
283+
284+
let socket = net::UdpSocket::bind("127.0.0.1:0").expect("failed to bind host socket");
285+
let _ = socket.set_read_timeout(Some(Duration::from_millis(500)));
286+
287+
let runtime = BasicRuntime::new().unwrap();
288+
289+
let (flusher, _handle) = DogStatsDClient::new_with_shared_runtime(
290+
Endpoint::from_slice(socket.local_addr().unwrap().to_string().as_str()),
291+
&runtime,
292+
)
293+
.unwrap();
294+
295+
flusher.send(vec![
296+
Count("test_count", 3, &vec![tag!("foo", "bar")]),
297+
Count("test_neg_count", -2, &vec![]),
298+
Distribution("test_distribution", 4.2, &vec![]),
299+
Gauge("test_gauge", 7.6, &vec![]),
300+
Histogram("test_histogram", 8.0, &vec![]),
301+
Set("test_set", 9, &vec![tag!("the", "end")]),
302+
Set("test_neg_set", -1, &vec![]),
303+
]);
304+
305+
runtime
306+
.block_on(runtime.shutdown_async())
307+
.expect("Failed to shutdown runtime");
308+
309+
fn read(socket: &net::UdpSocket) -> String {
310+
let mut buf = [0; 100];
311+
socket.recv(&mut buf).expect("No data");
312+
let datagram = String::from_utf8_lossy(buf.strip_suffix(&[0]).unwrap());
313+
datagram.trim_matches(char::from(0)).to_string()
314+
}
315+
316+
assert_eq!("test_count:3|c|#foo:bar", read(&socket));
317+
assert_eq!("test_neg_count:-2|c", read(&socket));
318+
assert_eq!("test_distribution:4.2|d", read(&socket));
319+
assert_eq!("test_gauge:7.6|g", read(&socket));
320+
assert_eq!("test_histogram:8|h", read(&socket));
321+
assert_eq!("test_set:9|s|#the:end", read(&socket));
322+
assert_eq!("test_neg_set:-1|s", read(&socket));
323+
}
246324
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use anyhow::anyhow;
5+
use async_trait::async_trait;
6+
use cadence::{MetricSink, SinkStats};
7+
use libdd_common::Endpoint;
8+
use libdd_shared_runtime::{worker::Worker, SharedRuntime, WorkerHandle};
9+
use std::fmt;
10+
use std::io;
11+
use std::panic::RefUnwindSafe;
12+
use std::sync::Arc;
13+
use tokio::sync::mpsc;
14+
use tracing::error;
15+
16+
use super::{sink, QUEUE_SIZE};
17+
18+
/// A [`MetricSink`] that offloads sent metrics to a [`SharedRuntime`].
19+
#[derive(Clone)]
20+
pub struct SharedRuntimeMetricSink {
21+
sender: mpsc::Sender<String>,
22+
sink: Arc<dyn MetricSink + Send + Sync + RefUnwindSafe>,
23+
}
24+
25+
impl fmt::Debug for SharedRuntimeMetricSink {
26+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27+
write!(f, "SharedRuntimeMetricSink {{ {:?} }}", self.sender)
28+
}
29+
}
30+
31+
impl MetricSink for SharedRuntimeMetricSink {
32+
fn emit(&self, metric: &str) -> io::Result<usize> {
33+
let len = metric.len();
34+
self.sender
35+
.try_send(metric.to_owned())
36+
.map(|_| len)
37+
.map_err(|e| match e {
38+
mpsc::error::TrySendError::Full(_) => {
39+
io::Error::new(io::ErrorKind::WouldBlock, "dogstatsd channel full")
40+
}
41+
mpsc::error::TrySendError::Closed(_) => {
42+
io::Error::new(io::ErrorKind::BrokenPipe, "dogstatsd channel closed")
43+
}
44+
})
45+
}
46+
47+
fn flush(&self) -> Result<(), std::io::Error> {
48+
self.sink.flush()
49+
}
50+
51+
fn stats(&self) -> SinkStats {
52+
self.sink.stats()
53+
}
54+
}
55+
56+
/// A [`Worker`] that drains metrics from a channel and forwards them to a
57+
/// wrapped [`MetricSink`] (e.g. `UdpMetricSink`).
58+
pub struct MetricSinkWorker {
59+
receiver: mpsc::Receiver<String>,
60+
sink: Arc<dyn MetricSink + Send + Sync + RefUnwindSafe>,
61+
pending: Option<String>,
62+
}
63+
64+
impl std::fmt::Debug for MetricSinkWorker {
65+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66+
f.debug_struct("MetricSinkWorker")
67+
.field("pending", &self.pending)
68+
.finish_non_exhaustive()
69+
}
70+
}
71+
72+
#[async_trait]
73+
impl Worker for MetricSinkWorker {
74+
/// Awaits the next metric from the channel, storing it in `pending`.
75+
///
76+
/// If the channel is closed this future never resolves, allowing the
77+
/// SharedRuntime to cancel it cleanly at the next yield point.
78+
async fn trigger(&mut self) {
79+
match self.receiver.recv().await {
80+
Some(metric) => self.pending = Some(metric),
81+
None => {
82+
// Channel closed — park until cancelled by the runtime.
83+
std::future::pending::<()>().await;
84+
}
85+
}
86+
println!("Received metric: {:#?}", self.pending);
87+
}
88+
89+
/// Forwards the single metric stored by `trigger` to the wrapped sink.
90+
async fn run(&mut self) {
91+
if let Some(metric) = self.pending.take() {
92+
if let Err(e) = self.sink.emit(&metric) {
93+
error!(?e, "MetricSinkWorker: failed to emit metric");
94+
}
95+
}
96+
}
97+
98+
/// Drains any remaining queued metrics and flushes the wrapped sink.
99+
async fn shutdown(&mut self) {
100+
// Forward the metric that was sitting in `pending`, if any.
101+
if let Some(metric) = self.pending.take() {
102+
if let Err(e) = self.sink.emit(&metric) {
103+
error!(?e, "MetricSinkWorker: failed to emit metric on shutdown");
104+
}
105+
}
106+
// Drain the channel.
107+
while let Ok(metric) = self.receiver.try_recv() {
108+
if let Err(e) = self.sink.emit(&metric) {
109+
error!(?e, "MetricSinkWorker: failed to emit metric on shutdown");
110+
}
111+
}
112+
if let Err(e) = self.sink.flush() {
113+
error!(?e, "MetricSinkWorker: failed to flush sink on shutdown");
114+
}
115+
}
116+
117+
/// Reset the worker in the child process after a fork.
118+
fn reset(&mut self) {
119+
self.pending = None;
120+
// Drain the channel
121+
while self.receiver.try_recv().is_ok() {}
122+
}
123+
}
124+
125+
pub fn create_shared_runtime_sink(
126+
endpoint: &Endpoint,
127+
runtime: &impl SharedRuntime,
128+
) -> anyhow::Result<(SharedRuntimeMetricSink, WorkerHandle)> {
129+
let (tx, rx) = mpsc::channel(QUEUE_SIZE);
130+
131+
let sink: Arc<dyn MetricSink + Send + Sync + RefUnwindSafe> = match endpoint.url.scheme_str() {
132+
#[cfg(unix)]
133+
Some("unix") => Arc::new(sink::create_unix_sink(endpoint)?),
134+
_ => Arc::new(sink::create_udp_sink(endpoint)?),
135+
};
136+
137+
let sink_worker = MetricSinkWorker {
138+
receiver: rx,
139+
sink: sink.clone(),
140+
pending: None,
141+
};
142+
143+
let handle = runtime
144+
.spawn_worker(sink_worker, true)
145+
.map_err(|e| anyhow!("failed to spawn MetricSinkWorker: {e}"))?;
146+
147+
let shared_sink = SharedRuntimeMetricSink { sender: tx, sink };
148+
149+
Ok((shared_sink, handle))
150+
}

0 commit comments

Comments
 (0)