From 40bc97a5af7ec40b88fd63f5e698e9b6b26352b5 Mon Sep 17 00:00:00 2001 From: Lewis Date: Wed, 24 Dec 2025 14:36:52 -0500 Subject: [PATCH 01/21] Add named pipe to dogstasd BufferReader. --- .idea/workspace.xml | 99 ++++++++++++++++++++ crates/datadog-serverless-compat/src/main.rs | 31 +++++- crates/dogstatsd/Cargo.toml | 2 +- crates/dogstatsd/src/dogstatsd.rs | 53 +++++++++-- crates/dogstatsd/tests/integration_test.rs | 1 + 5 files changed, 173 insertions(+), 13 deletions(-) create mode 100644 .idea/workspace.xml diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..2e6aaa8 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1766593491693 + + + + + + + false + true + + \ No newline at end of file diff --git a/crates/datadog-serverless-compat/src/main.rs b/crates/datadog-serverless-compat/src/main.rs index f763627..93cd6d3 100644 --- a/crates/datadog-serverless-compat/src/main.rs +++ b/crates/datadog-serverless-compat/src/main.rs @@ -65,10 +65,24 @@ pub async fn main() { }; let dd_api_key: Option = env::var("DD_API_KEY").ok(); - let dd_dogstatsd_port: u16 = env::var("DD_DOGSTATSD_PORT") - .ok() - .and_then(|port| port.parse::().ok()) - .unwrap_or(DEFAULT_DOGSTATSD_PORT); + let dd_dogstatsd_windows_pipe_name: Option = { + #[cfg(windows)] + { + env::var("DD_DOGSTATSD_WINDOWS_PIPE_NAME").ok() + } + #[cfg(not(windows))] + { + None + } + }; + let dd_dogstatsd_port: u16 = if dd_dogstatsd_windows_pipe_name.is_some() { + 0 // Override to 0 when using Windows named pipe + } else { + env::var("DD_DOGSTATSD_PORT") + .ok() + .and_then(|port| port.parse::().ok()) + .unwrap_or(DEFAULT_DOGSTATSD_PORT) + }; let dd_site = env::var("DD_SITE").unwrap_or_else(|_| "datadoghq.com".to_string()); let dd_use_dogstatsd = env::var("DD_USE_DOGSTATSD") .map(|val| val.to_lowercase() != "false") @@ -152,9 +166,14 @@ pub async fn main() { https_proxy, dogstatsd_tags, dd_statsd_metric_namespace, + dd_dogstatsd_windows_pipe_name.clone(), ) .await; - info!("dogstatsd-udp: starting to listen on port {dd_dogstatsd_port}"); + if let Some(ref windows_pipe_name) = dd_dogstatsd_windows_pipe_name { + info!("dogstatsd-pipe: starting to listen on pipe {windows_pipe_name}"); + } else { + info!("dogstatsd-udp: starting to listen on port {dd_dogstatsd_port}"); + } (metrics_flusher, Some(aggregator_handle)) } else { info!("dogstatsd disabled"); @@ -181,6 +200,7 @@ async fn start_dogstatsd( https_proxy: Option, dogstatsd_tags: &str, metric_namespace: Option, + windows_pipe_name: Option, ) -> (CancellationToken, Option, AggregatorHandle) { // 1. Create the aggregator service #[allow(clippy::expect_used)] @@ -197,6 +217,7 @@ async fn start_dogstatsd( host: AGENT_HOST.to_string(), port, metric_namespace, + windows_pipe_name, }; let dogstatsd_cancel_token = tokio_util::sync::CancellationToken::new(); diff --git a/crates/dogstatsd/Cargo.toml b/crates/dogstatsd/Cargo.toml index 2b36710..4ec6e09 100644 --- a/crates/dogstatsd/Cargo.toml +++ b/crates/dogstatsd/Cargo.toml @@ -19,7 +19,7 @@ reqwest = { version = "0.12.4", features = ["json", "http2"], default-features = serde = { version = "1.0.197", default-features = false, features = ["derive"] } serde_json = { version = "1.0.116", default-features = false, features = ["alloc"] } thiserror = { version = "1.0.58", default-features = false } -tokio = { version = "1.37.0", default-features = false, features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.37.0", default-features = false, features = ["macros", "rt-multi-thread", "net", "io-util"] } tokio-util = { version = "0.7.11", default-features = false } tracing = { version = "0.1.40", default-features = false } regex = { version = "1.10.6", default-features = false } diff --git a/crates/dogstatsd/src/dogstatsd.rs b/crates/dogstatsd/src/dogstatsd.rs index 6f81479..b4dbcb3 100644 --- a/crates/dogstatsd/src/dogstatsd.rs +++ b/crates/dogstatsd/src/dogstatsd.rs @@ -20,12 +20,15 @@ pub struct DogStatsDConfig { pub host: String, pub port: u16, pub metric_namespace: Option, + pub windows_pipe_name: Option, } enum BufferReader { UdpSocketReader(tokio::net::UdpSocket), #[allow(dead_code)] MirrorReader(Vec, SocketAddr), + #[cfg(windows)] + NamedPipeReader(tokio::net::windows::named_pipe::NamedPipeServer), } impl BufferReader { @@ -45,6 +48,21 @@ impl BufferReader { Ok((buf[..amt].to_owned(), src)) } BufferReader::MirrorReader(data, socket) => Ok((data.clone(), *socket)), + #[cfg(windows)] + BufferReader::NamedPipeReader(pipe) => { + use tokio::io::AsyncReadExt; + let mut buf = [0; 8192]; + + #[allow(clippy::expect_used)] + let amt = pipe + .read(&mut buf) + .await + .expect("didn't receive data from named pipe"); + + // Named pipes don't have a source address, use a dummy localhost address + let dummy_addr = "127.0.0.1:0".parse().expect("valid address"); + Ok((buf[..amt].to_owned(), dummy_addr)) + } } } } @@ -56,17 +74,38 @@ impl DogStatsD { aggregator_handle: AggregatorHandle, cancel_token: tokio_util::sync::CancellationToken, ) -> DogStatsD { - let addr = format!("{}:{}", config.host, config.port); + #[cfg_attr(not(windows), allow(unused_variables))] + let buffer_reader = if let Some(windows_pipe_name) = &config.windows_pipe_name { + // Named pipe is only supported on Windows + #[cfg(windows)] + { + use tokio::net::windows::named_pipe::ServerOptions; + #[allow(clippy::expect_used)] + let pipe = ServerOptions::new() + .first_pipe_instance(true) + .create(windows_pipe_name) + .expect("couldn't create named pipe"); + BufferReader::NamedPipeReader(pipe) + } + #[cfg(not(windows))] + { + panic!("Named pipes are only supported on Windows"); + } + } else { + // UDP socket for all platforms + let addr = format!("{}:{}", config.host, config.port); + // TODO (UDS socket) + #[allow(clippy::expect_used)] + let socket = tokio::net::UdpSocket::bind(addr) + .await + .expect("couldn't bind to address"); + BufferReader::UdpSocketReader(socket) + }; - // TODO (UDS socket) - #[allow(clippy::expect_used)] - let socket = tokio::net::UdpSocket::bind(addr) - .await - .expect("couldn't bind to address"); DogStatsD { cancel_token, aggregator_handle, - buffer_reader: BufferReader::UdpSocketReader(socket), + buffer_reader, metric_namespace: config.metric_namespace.clone(), } } diff --git a/crates/dogstatsd/tests/integration_test.rs b/crates/dogstatsd/tests/integration_test.rs index 65d919c..72d8e4b 100644 --- a/crates/dogstatsd/tests/integration_test.rs +++ b/crates/dogstatsd/tests/integration_test.rs @@ -97,6 +97,7 @@ async fn start_dogstatsd(aggregator_handle: AggregatorHandle) -> CancellationTok host: "127.0.0.1".to_string(), port: 18125, metric_namespace: None, + windows_pipe_name: None, }; let dogstatsd_cancel_token = tokio_util::sync::CancellationToken::new(); let dogstatsd_client = DogStatsD::new( From f9ad9b72efb7e3574680d970a7193643d35399ce Mon Sep 17 00:00:00 2001 From: Lewis Date: Fri, 26 Dec 2025 12:13:16 -0500 Subject: [PATCH 02/21] dogstatsd: windows named pipe support now with connect + result type improvements --- .idea/workspace.xml | 99 --------------- crates/dogstatsd/src/dogstatsd.rs | 199 ++++++++++++++++++++++++++---- 2 files changed, 172 insertions(+), 126 deletions(-) delete mode 100644 .idea/workspace.xml diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index 2e6aaa8..0000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1766593491693 - - - - - - - false - true - - \ No newline at end of file diff --git a/crates/dogstatsd/src/dogstatsd.rs b/crates/dogstatsd/src/dogstatsd.rs index b4dbcb3..7be7be2 100644 --- a/crates/dogstatsd/src/dogstatsd.rs +++ b/crates/dogstatsd/src/dogstatsd.rs @@ -9,6 +9,38 @@ use crate::errors::ParseError::UnsupportedType; use crate::metric::{id, parse, Metric}; use tracing::{debug, error, trace}; +// Windows-specific imports and constants +#[cfg(windows)] +use { + std::cell::RefCell, + tokio::io::AsyncReadExt, + tokio::net::windows::named_pipe::ServerOptions, + tokio::time::{sleep, Duration}, +}; + +#[cfg(windows)] +const MAX_CONSECUTIVE_ERRORS: u32 = 10; + +/// Represents the source of a DogStatsD message +#[derive(Debug, Clone)] +pub enum MessageSource { + /// Message received from a network socket (UDP) + Network(SocketAddr), + /// Message received from a Windows named pipe + #[cfg(windows)] + NamedPipe(String), +} + +impl std::fmt::Display for MessageSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Network(addr) => write!(f, "{}", addr), + #[cfg(windows)] + Self::NamedPipe(name) => write!(f, "{}", name), + } + } +} + pub struct DogStatsD { cancel_token: tokio_util::sync::CancellationToken, aggregator_handle: AggregatorHandle, @@ -28,11 +60,73 @@ enum BufferReader { #[allow(dead_code)] MirrorReader(Vec, SocketAddr), #[cfg(windows)] - NamedPipeReader(tokio::net::windows::named_pipe::NamedPipeServer), + NamedPipeReader { + pipe_name: String, + current_pipe: RefCell>, + consecutive_errors: RefCell, + }, +} + +#[cfg(windows)] +async fn create_and_connect_pipe( + pipe_name: &str, + consecutive_errors: &mut u32, +) -> std::io::Result { + // Create pipe with retry logic + let pipe = loop { + match ServerOptions::new() + .first_pipe_instance(false) + .create(pipe_name) + { + Ok(p) => { + *consecutive_errors = 0; + break p; + } + Err(e) => { + error!("Failed to create named pipe: {}", e); + *consecutive_errors += 1; + + if *consecutive_errors >= MAX_CONSECUTIVE_ERRORS { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Too many consecutive pipe creation errors", + )); + } + + let backoff_ms = 10u64 * (1 << consecutive_errors.min(6)); + sleep(Duration::from_millis(backoff_ms)).await; + continue; + } + } + }; + + // Wait for client to connect + match pipe.connect().await { + Ok(()) => { + *consecutive_errors = 0; + debug!("Client connected to DogStatsD named pipe"); + Ok(pipe) + } + Err(e) => { + error!("Failed to accept connection on named pipe: {}", e); + *consecutive_errors += 1; + + if *consecutive_errors >= MAX_CONSECUTIVE_ERRORS { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Too many consecutive connection errors", + )); + } + + let backoff_ms = 10u64 * (1 << consecutive_errors.min(6)); + sleep(Duration::from_millis(backoff_ms)).await; + Err(e) + } + } } impl BufferReader { - async fn read(&self) -> std::io::Result<(Vec, SocketAddr)> { + async fn read(&self) -> std::io::Result<(Vec, MessageSource)> { match self { BufferReader::UdpSocketReader(socket) => { // TODO(astuyve) this should be dynamic @@ -45,23 +139,71 @@ impl BufferReader { .recv_from(&mut buf) .await .expect("didn't receive data"); - Ok((buf[..amt].to_owned(), src)) + Ok((buf[..amt].to_owned(), MessageSource::Network(src))) + } + BufferReader::MirrorReader(data, socket) => { + Ok((data.clone(), MessageSource::Network(*socket))) } - BufferReader::MirrorReader(data, socket) => Ok((data.clone(), *socket)), #[cfg(windows)] - BufferReader::NamedPipeReader(pipe) => { - use tokio::io::AsyncReadExt; - let mut buf = [0; 8192]; - - #[allow(clippy::expect_used)] - let amt = pipe - .read(&mut buf) - .await - .expect("didn't receive data from named pipe"); + BufferReader::NamedPipeReader { + pipe_name, + current_pipe, + consecutive_errors, + } => { + loop { + // Create pipe if needed + if current_pipe.borrow().is_none() { + let mut errors = consecutive_errors.borrow_mut(); + match create_and_connect_pipe(pipe_name, &mut errors).await { + Ok(new_pipe) => { + drop(errors); + *current_pipe.borrow_mut() = Some(new_pipe); + } + Err(e) => { + return Err(e); + } + } + } - // Named pipes don't have a source address, use a dummy localhost address - let dummy_addr = "127.0.0.1:0".parse().expect("valid address"); - Ok((buf[..amt].to_owned(), dummy_addr)) + // Read from the connected pipe + let mut buf = [0; 8192]; + let mut pipe_ref = current_pipe.borrow_mut(); + let pipe = pipe_ref.as_mut().unwrap(); + + match pipe.read(&mut buf).await { + Ok(0) => { + *pipe_ref = None; + drop(pipe_ref); + continue; + } + Ok(amt) => { + *consecutive_errors.borrow_mut() = 0; + return Ok(( + buf[..amt].to_vec(), + MessageSource::NamedPipe(pipe_name.to_string()), + )); + } + Err(e) => { + // Read error + error!("Error reading from named pipe: {}", e); + *consecutive_errors.borrow_mut() += 1; + *pipe_ref = None; + drop(pipe_ref); + + let current_errors = *consecutive_errors.borrow(); + if current_errors >= MAX_CONSECUTIVE_ERRORS { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Too many consecutive read errors", + )); + } + + let backoff_ms = 10u64 * (1 << current_errors.min(6)); + sleep(Duration::from_millis(backoff_ms)).await; + continue; + } + } + } } } } @@ -74,22 +216,25 @@ impl DogStatsD { aggregator_handle: AggregatorHandle, cancel_token: tokio_util::sync::CancellationToken, ) -> DogStatsD { - #[cfg_attr(not(windows), allow(unused_variables))] - let buffer_reader = if let Some(windows_pipe_name) = &config.windows_pipe_name { - // Named pipe is only supported on Windows + // Fail fast on non-Windows if pipe name is configured + #[cfg(not(windows))] + if config.windows_pipe_name.is_some() { + panic!("Named pipes are only supported on Windows"); + } + + #[allow(unused_variables)] // pipe_name unused on non-Windows + let buffer_reader = if let Some(ref pipe_name) = config.windows_pipe_name { #[cfg(windows)] { - use tokio::net::windows::named_pipe::ServerOptions; - #[allow(clippy::expect_used)] - let pipe = ServerOptions::new() - .first_pipe_instance(true) - .create(windows_pipe_name) - .expect("couldn't create named pipe"); - BufferReader::NamedPipeReader(pipe) + BufferReader::NamedPipeReader { + pipe_name: pipe_name.clone(), + current_pipe: RefCell::new(None), + consecutive_errors: RefCell::new(0), + } } #[cfg(not(windows))] { - panic!("Named pipes are only supported on Windows"); + unreachable!("Windows pipe on non-Windows (checked above)") } } else { // UDP socket for all platforms From 4ed8c3e0d24009debaee9ec31605bbc0025351ed Mon Sep 17 00:00:00 2001 From: Lewis Date: Fri, 26 Dec 2025 14:25:58 -0500 Subject: [PATCH 03/21] More consistent retry logic for NamedPipes --- crates/dogstatsd/src/dogstatsd.rs | 134 +++++++++++++++++++----------- 1 file changed, 85 insertions(+), 49 deletions(-) diff --git a/crates/dogstatsd/src/dogstatsd.rs b/crates/dogstatsd/src/dogstatsd.rs index 7be7be2..887b338 100644 --- a/crates/dogstatsd/src/dogstatsd.rs +++ b/crates/dogstatsd/src/dogstatsd.rs @@ -18,8 +18,11 @@ use { tokio::time::{sleep, Duration}, }; +// Maximum number of consecutive errors before giving up on named pipe operations. +// Backoff formula: 10ms * 2^error_count, capped at 2^6 +// With MAX = 5: backoffs are 20ms, 40ms, 80ms, 160ms (total: 300ms before giving up) #[cfg(windows)] -const MAX_CONSECUTIVE_ERRORS: u32 = 10; +const MAX_NAMED_PIPE_ERRORS: u32 = 5; /// Represents the source of a DogStatsD message #[derive(Debug, Clone)] @@ -64,62 +67,47 @@ enum BufferReader { pipe_name: String, current_pipe: RefCell>, consecutive_errors: RefCell, + cancel_token: tokio_util::sync::CancellationToken, }, } +// Creates a named pipe and waits for a client to connect. +// Retry logic is handled by the caller to maintain consistency across all error types. +// +// Note: If profiling shows that pipe creation errors dominate retry scenarios, +// we could optimize by adding targeted retry logic here for creation-specific failures. #[cfg(windows)] async fn create_and_connect_pipe( pipe_name: &str, - consecutive_errors: &mut u32, + cancel_token: &tokio_util::sync::CancellationToken, ) -> std::io::Result { - // Create pipe with retry logic - let pipe = loop { - match ServerOptions::new() - .first_pipe_instance(false) - .create(pipe_name) - { - Ok(p) => { - *consecutive_errors = 0; - break p; - } - Err(e) => { - error!("Failed to create named pipe: {}", e); - *consecutive_errors += 1; - - if *consecutive_errors >= MAX_CONSECUTIVE_ERRORS { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Too many consecutive pipe creation errors", - )); - } + // Create pipe (single attempt) + let pipe = ServerOptions::new() + .first_pipe_instance(false) + .create(pipe_name) + .map_err(|e| { + error!("Failed to create named pipe: {}", e); + e + })?; - let backoff_ms = 10u64 * (1 << consecutive_errors.min(6)); - sleep(Duration::from_millis(backoff_ms)).await; - continue; - } + // Wait for client to connect + let connect_result = tokio::select! { + result = pipe.connect() => result, + _ = cancel_token.cancelled() => { + return Err(std::io::Error::new( + std::io::ErrorKind::Interrupted, + "Operation cancelled", + )); } }; - // Wait for client to connect - match pipe.connect().await { + match connect_result { Ok(()) => { - *consecutive_errors = 0; debug!("Client connected to DogStatsD named pipe"); Ok(pipe) } Err(e) => { error!("Failed to accept connection on named pipe: {}", e); - *consecutive_errors += 1; - - if *consecutive_errors >= MAX_CONSECUTIVE_ERRORS { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Too many consecutive connection errors", - )); - } - - let backoff_ms = 10u64 * (1 << consecutive_errors.min(6)); - sleep(Duration::from_millis(backoff_ms)).await; Err(e) } } @@ -149,18 +137,47 @@ impl BufferReader { pipe_name, current_pipe, consecutive_errors, + cancel_token, } => { loop { + // Check if cancelled + if cancel_token.is_cancelled() { + return Err(std::io::Error::new( + std::io::ErrorKind::Interrupted, + "Operation cancelled", + )); + } + // Create pipe if needed if current_pipe.borrow().is_none() { - let mut errors = consecutive_errors.borrow_mut(); - match create_and_connect_pipe(pipe_name, &mut errors).await { + match create_and_connect_pipe(pipe_name, cancel_token).await { Ok(new_pipe) => { - drop(errors); + *consecutive_errors.borrow_mut() = 0; *current_pipe.borrow_mut() = Some(new_pipe); } Err(e) => { - return Err(e); + // Handle pipe creation/connection errors with retry logic + *consecutive_errors.borrow_mut() += 1; + let current_errors = *consecutive_errors.borrow(); + + if current_errors >= MAX_NAMED_PIPE_ERRORS { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Too many consecutive pipe errors: {}", e), + )); + } + + let backoff_ms = 10u64 * (1 << current_errors.min(6)); + tokio::select! { + _ = sleep(Duration::from_millis(backoff_ms)) => {}, + _ = cancel_token.cancelled() => { + return Err(std::io::Error::new( + std::io::ErrorKind::Interrupted, + "Operation cancelled", + )); + } + } + continue; } } } @@ -170,7 +187,17 @@ impl BufferReader { let mut pipe_ref = current_pipe.borrow_mut(); let pipe = pipe_ref.as_mut().unwrap(); - match pipe.read(&mut buf).await { + let read_result = tokio::select! { + result = pipe.read(&mut buf) => result, + _ = cancel_token.cancelled() => { + return Err(std::io::Error::new( + std::io::ErrorKind::Interrupted, + "Operation cancelled", + )); + } + }; + + match read_result { Ok(0) => { *pipe_ref = None; drop(pipe_ref); @@ -191,7 +218,7 @@ impl BufferReader { drop(pipe_ref); let current_errors = *consecutive_errors.borrow(); - if current_errors >= MAX_CONSECUTIVE_ERRORS { + if current_errors >= MAX_NAMED_PIPE_ERRORS { return Err(std::io::Error::new( std::io::ErrorKind::Other, "Too many consecutive read errors", @@ -199,7 +226,15 @@ impl BufferReader { } let backoff_ms = 10u64 * (1 << current_errors.min(6)); - sleep(Duration::from_millis(backoff_ms)).await; + tokio::select! { + _ = sleep(Duration::from_millis(backoff_ms)) => {}, + _ = cancel_token.cancelled() => { + return Err(std::io::Error::new( + std::io::ErrorKind::Interrupted, + "Operation cancelled", + )); + } + } continue; } } @@ -222,7 +257,7 @@ impl DogStatsD { panic!("Named pipes are only supported on Windows"); } - #[allow(unused_variables)] // pipe_name unused on non-Windows + #[allow(unused_variables)] // pipe_name unused on non-Windows let buffer_reader = if let Some(ref pipe_name) = config.windows_pipe_name { #[cfg(windows)] { @@ -230,11 +265,12 @@ impl DogStatsD { pipe_name: pipe_name.clone(), current_pipe: RefCell::new(None), consecutive_errors: RefCell::new(0), + cancel_token: cancel_token.clone(), } } #[cfg(not(windows))] { - unreachable!("Windows pipe on non-Windows (checked above)") + panic!("Named pipes are only supported on Windows") } } else { // UDP socket for all platforms From 44c5b2b988272b0e4d164c842be25ab7a52c923f Mon Sep 17 00:00:00 2001 From: Lewis Date: Fri, 26 Dec 2025 14:46:19 -0500 Subject: [PATCH 04/21] Improve naming & allow panic for consistent new() failures --- crates/dogstatsd/src/dogstatsd.rs | 38 +++++++++++++------------------ 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/crates/dogstatsd/src/dogstatsd.rs b/crates/dogstatsd/src/dogstatsd.rs index 887b338..d59fa7e 100644 --- a/crates/dogstatsd/src/dogstatsd.rs +++ b/crates/dogstatsd/src/dogstatsd.rs @@ -71,18 +71,17 @@ enum BufferReader { }, } -// Creates a named pipe and waits for a client to connect. -// Retry logic is handled by the caller to maintain consistency across all error types. +// Creates a Windows named pipe and waits for a client to connect. // -// Note: If profiling shows that pipe creation errors dominate retry scenarios, -// we could optimize by adding targeted retry logic here for creation-specific failures. +// Note: Windows named pipes have transient failures. Retries are centralized in the read function. +// If most retries are pipe creation errors, we could optimize with additional retry logic here. #[cfg(windows)] -async fn create_and_connect_pipe( +async fn create_and_connect_named_pipe( pipe_name: &str, cancel_token: &tokio_util::sync::CancellationToken, ) -> std::io::Result { - // Create pipe (single attempt) - let pipe = ServerOptions::new() + // Create named pipe (single attempt) + let named_pipe = ServerOptions::new() .first_pipe_instance(false) .create(pipe_name) .map_err(|e| { @@ -90,9 +89,9 @@ async fn create_and_connect_pipe( e })?; - // Wait for client to connect + // Wait for client to connect to named pipe let connect_result = tokio::select! { - result = pipe.connect() => result, + result = named_pipe.connect() => result, _ = cancel_token.cancelled() => { return Err(std::io::Error::new( std::io::ErrorKind::Interrupted, @@ -104,7 +103,7 @@ async fn create_and_connect_pipe( match connect_result { Ok(()) => { debug!("Client connected to DogStatsD named pipe"); - Ok(pipe) + Ok(named_pipe) } Err(e) => { error!("Failed to accept connection on named pipe: {}", e); @@ -129,9 +128,6 @@ impl BufferReader { .expect("didn't receive data"); Ok((buf[..amt].to_owned(), MessageSource::Network(src))) } - BufferReader::MirrorReader(data, socket) => { - Ok((data.clone(), MessageSource::Network(*socket))) - } #[cfg(windows)] BufferReader::NamedPipeReader { pipe_name, @@ -148,9 +144,9 @@ impl BufferReader { )); } - // Create pipe if needed + // Create pipe if needed (initial startup, after client disconnect, or after error) if current_pipe.borrow().is_none() { - match create_and_connect_pipe(pipe_name, cancel_token).await { + match create_and_connect_named_pipe(pipe_name, cancel_token).await { Ok(new_pipe) => { *consecutive_errors.borrow_mut() = 0; *current_pipe.borrow_mut() = Some(new_pipe); @@ -239,6 +235,9 @@ impl BufferReader { } } } + }, + BufferReader::MirrorReader(data, socket) => { + Ok((data.clone(), MessageSource::Network(*socket))) } } } @@ -251,12 +250,6 @@ impl DogStatsD { aggregator_handle: AggregatorHandle, cancel_token: tokio_util::sync::CancellationToken, ) -> DogStatsD { - // Fail fast on non-Windows if pipe name is configured - #[cfg(not(windows))] - if config.windows_pipe_name.is_some() { - panic!("Named pipes are only supported on Windows"); - } - #[allow(unused_variables)] // pipe_name unused on non-Windows let buffer_reader = if let Some(ref pipe_name) = config.windows_pipe_name { #[cfg(windows)] @@ -269,8 +262,9 @@ impl DogStatsD { } } #[cfg(not(windows))] + #[allow(clippy::panic)] { - panic!("Named pipes are only supported on Windows") + panic!("Named pipes are only supported on Windows.") } } else { // UDP socket for all platforms From 8fe4ad8f5f4825803b0f30bd29446c61ae307465 Mon Sep 17 00:00:00 2001 From: Lewis Date: Fri, 26 Dec 2025 15:35:16 -0500 Subject: [PATCH 05/21] Simplify named pipe read loop --- crates/dogstatsd/src/dogstatsd.rs | 99 +++++++------------------------ 1 file changed, 23 insertions(+), 76 deletions(-) diff --git a/crates/dogstatsd/src/dogstatsd.rs b/crates/dogstatsd/src/dogstatsd.rs index d59fa7e..994e57c 100644 --- a/crates/dogstatsd/src/dogstatsd.rs +++ b/crates/dogstatsd/src/dogstatsd.rs @@ -12,7 +12,6 @@ use tracing::{debug, error, trace}; // Windows-specific imports and constants #[cfg(windows)] use { - std::cell::RefCell, tokio::io::AsyncReadExt, tokio::net::windows::named_pipe::ServerOptions, tokio::time::{sleep, Duration}, @@ -65,8 +64,6 @@ enum BufferReader { #[cfg(windows)] NamedPipeReader { pipe_name: String, - current_pipe: RefCell>, - consecutive_errors: RefCell, cancel_token: tokio_util::sync::CancellationToken, }, } @@ -78,7 +75,6 @@ enum BufferReader { #[cfg(windows)] async fn create_and_connect_named_pipe( pipe_name: &str, - cancel_token: &tokio_util::sync::CancellationToken, ) -> std::io::Result { // Create named pipe (single attempt) let named_pipe = ServerOptions::new() @@ -90,26 +86,9 @@ async fn create_and_connect_named_pipe( })?; // Wait for client to connect to named pipe - let connect_result = tokio::select! { - result = named_pipe.connect() => result, - _ = cancel_token.cancelled() => { - return Err(std::io::Error::new( - std::io::ErrorKind::Interrupted, - "Operation cancelled", - )); - } - }; - - match connect_result { - Ok(()) => { - debug!("Client connected to DogStatsD named pipe"); - Ok(named_pipe) - } - Err(e) => { - error!("Failed to accept connection on named pipe: {}", e); - Err(e) - } - } + named_pipe.connect().await?; + debug!("Client connected to DogStatsD named pipe"); + Ok(named_pipe) } impl BufferReader { @@ -131,10 +110,12 @@ impl BufferReader { #[cfg(windows)] BufferReader::NamedPipeReader { pipe_name, - current_pipe, - consecutive_errors, cancel_token, } => { + let mut consecutive_errors = 0; + let mut current_pipe: Option = + None; + loop { // Check if cancelled if cancel_token.is_cancelled() { @@ -145,34 +126,25 @@ impl BufferReader { } // Create pipe if needed (initial startup, after client disconnect, or after error) - if current_pipe.borrow().is_none() { - match create_and_connect_named_pipe(pipe_name, cancel_token).await { + if current_pipe.is_none() { + match create_and_connect_named_pipe(pipe_name).await { Ok(new_pipe) => { - *consecutive_errors.borrow_mut() = 0; - *current_pipe.borrow_mut() = Some(new_pipe); + consecutive_errors = 0; + current_pipe = Some(new_pipe); } Err(e) => { // Handle pipe creation/connection errors with retry logic - *consecutive_errors.borrow_mut() += 1; - let current_errors = *consecutive_errors.borrow(); + consecutive_errors += 1; - if current_errors >= MAX_NAMED_PIPE_ERRORS { + if consecutive_errors >= MAX_NAMED_PIPE_ERRORS { return Err(std::io::Error::new( std::io::ErrorKind::Other, format!("Too many consecutive pipe errors: {}", e), )); } - let backoff_ms = 10u64 * (1 << current_errors.min(6)); - tokio::select! { - _ = sleep(Duration::from_millis(backoff_ms)) => {}, - _ = cancel_token.cancelled() => { - return Err(std::io::Error::new( - std::io::ErrorKind::Interrupted, - "Operation cancelled", - )); - } - } + let backoff_ms = 10u64 * (1 << consecutive_errors.min(6)); + sleep(Duration::from_millis(backoff_ms)).await; continue; } } @@ -180,27 +152,14 @@ impl BufferReader { // Read from the connected pipe let mut buf = [0; 8192]; - let mut pipe_ref = current_pipe.borrow_mut(); - let pipe = pipe_ref.as_mut().unwrap(); - - let read_result = tokio::select! { - result = pipe.read(&mut buf) => result, - _ = cancel_token.cancelled() => { - return Err(std::io::Error::new( - std::io::ErrorKind::Interrupted, - "Operation cancelled", - )); - } - }; + let pipe = current_pipe.as_mut().unwrap(); - match read_result { + match pipe.read(&mut buf).await { Ok(0) => { - *pipe_ref = None; - drop(pipe_ref); + current_pipe = None; continue; } Ok(amt) => { - *consecutive_errors.borrow_mut() = 0; return Ok(( buf[..amt].to_vec(), MessageSource::NamedPipe(pipe_name.to_string()), @@ -209,28 +168,18 @@ impl BufferReader { Err(e) => { // Read error error!("Error reading from named pipe: {}", e); - *consecutive_errors.borrow_mut() += 1; - *pipe_ref = None; - drop(pipe_ref); + consecutive_errors += 1; + current_pipe = None; - let current_errors = *consecutive_errors.borrow(); - if current_errors >= MAX_NAMED_PIPE_ERRORS { + if consecutive_errors >= MAX_NAMED_PIPE_ERRORS { return Err(std::io::Error::new( std::io::ErrorKind::Other, "Too many consecutive read errors", )); } - let backoff_ms = 10u64 * (1 << current_errors.min(6)); - tokio::select! { - _ = sleep(Duration::from_millis(backoff_ms)) => {}, - _ = cancel_token.cancelled() => { - return Err(std::io::Error::new( - std::io::ErrorKind::Interrupted, - "Operation cancelled", - )); - } - } + let backoff_ms = 10u64 * (1 << consecutive_errors.min(6)); + sleep(Duration::from_millis(backoff_ms)).await; continue; } } @@ -256,8 +205,6 @@ impl DogStatsD { { BufferReader::NamedPipeReader { pipe_name: pipe_name.clone(), - current_pipe: RefCell::new(None), - consecutive_errors: RefCell::new(0), cancel_token: cancel_token.clone(), } } From 0f6aa1aa3213260c4d3897f56de1d069dede0a08 Mon Sep 17 00:00:00 2001 From: Lewis Date: Fri, 26 Dec 2025 16:05:50 -0500 Subject: [PATCH 06/21] Reorganize code for readability and add comments --- crates/dogstatsd/src/dogstatsd.rs | 296 +++++++++++++++++------------- 1 file changed, 172 insertions(+), 124 deletions(-) diff --git a/crates/dogstatsd/src/dogstatsd.rs b/crates/dogstatsd/src/dogstatsd.rs index 994e57c..8dae1d7 100644 --- a/crates/dogstatsd/src/dogstatsd.rs +++ b/crates/dogstatsd/src/dogstatsd.rs @@ -1,6 +1,12 @@ // Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 +//! DogStatsD server implementation for receiving and processing metrics. +//! +//! This module implements a DogStatsD-compatible server that receives metric data from multiple +//! transport mechanisms (UDP sockets, Windows named pipes), parses the metrics, applies optional +//! namespacing, and forwards them to an aggregator for batching and shipping to Datadog. + use std::net::SocketAddr; use std::str::Split; @@ -9,28 +15,41 @@ use crate::errors::ParseError::UnsupportedType; use crate::metric::{id, parse, Metric}; use tracing::{debug, error, trace}; -// Windows-specific imports and constants +// Windows-specific imports #[cfg(windows)] use { + std::sync::Arc, tokio::io::AsyncReadExt, tokio::net::windows::named_pipe::ServerOptions, tokio::time::{sleep, Duration}, }; -// Maximum number of consecutive errors before giving up on named pipe operations. -// Backoff formula: 10ms * 2^error_count, capped at 2^6 -// With MAX = 5: backoffs are 20ms, 40ms, 80ms, 160ms (total: 300ms before giving up) -#[cfg(windows)] -const MAX_NAMED_PIPE_ERRORS: u32 = 5; +// DogStatsD buffer size for receiving metrics +// TODO(astuyve) buf should be dynamic +// Max buffer size is configurable in Go Agent with a default of 8KB +// https://github.com/DataDog/datadog-agent/blob/85939a62b5580b2a15549f6936f257e61c5aa153/pkg/config/config_template.yaml#L2154-L2158 +const BUFFER_SIZE: usize = 8192; + +/// Configuration for the DogStatsD server +pub struct DogStatsDConfig { + /// Host to bind UDP socket to (e.g., "127.0.0.1") + pub host: String, + /// Port to bind UDP socket to (e.g., 8125), will be 0 if we're using a Named Pipe + pub port: u16, + /// Optional namespace to prepend to all metric names (e.g., "myapp") + pub metric_namespace: Option, + /// Optional Windows named pipe path (e.g., "\\\\.\\pipe\\my_pipe") + pub windows_pipe_name: Option, +} -/// Represents the source of a DogStatsD message +/// Represents the source of a DogStatsD message. Varies by transport method. #[derive(Debug, Clone)] pub enum MessageSource { /// Message received from a network socket (UDP) Network(SocketAddr), - /// Message received from a Windows named pipe + /// Message received from a Windows named pipe (Arc for efficient cloning) #[cfg(windows)] - NamedPipe(String), + NamedPipe(Arc), } impl std::fmt::Display for MessageSource { @@ -43,42 +62,74 @@ impl std::fmt::Display for MessageSource { } } -pub struct DogStatsD { - cancel_token: tokio_util::sync::CancellationToken, - aggregator_handle: AggregatorHandle, - buffer_reader: BufferReader, - metric_namespace: Option, -} - -pub struct DogStatsDConfig { - pub host: String, - pub port: u16, - pub metric_namespace: Option, - pub windows_pipe_name: Option, -} - +// BufferReader abstracts transport methods for metric data. enum BufferReader { + /// UDP socket reader (cross-platform, default transport) UdpSocketReader(tokio::net::UdpSocket), + + /// Mirror reader for testing - replays a fixed buffer #[allow(dead_code)] MirrorReader(Vec, SocketAddr), + + /// Windows named pipe reader (Windows-only transport) #[cfg(windows)] NamedPipeReader { - pipe_name: String, + pipe_name: Arc, cancel_token: tokio_util::sync::CancellationToken, }, } -// Creates a Windows named pipe and waits for a client to connect. -// -// Note: Windows named pipes have transient failures. Retries are centralized in the read function. -// If most retries are pipe creation errors, we could optimize with additional retry logic here. +impl BufferReader { + /// This is the main entry point for receiving metric data. + /// Note: Different transports have different blocking behaviors. + async fn read(&self) -> std::io::Result<(Vec, MessageSource)> { + match self { + BufferReader::UdpSocketReader(socket) => { + // UDP socket: blocks until a packet arrives + let mut buf = [0; BUFFER_SIZE]; + + #[allow(clippy::expect_used)] + let (amt, src) = socket + .recv_from(&mut buf) + .await + .expect("didn't receive data"); + Ok((buf[..amt].to_owned(), MessageSource::Network(src))) + } + BufferReader::MirrorReader(data, socket) => { + // Mirror Reader: returns immediately with stored data + Ok((data.clone(), MessageSource::Network(*socket))) + } + #[cfg(windows)] + BufferReader::NamedPipeReader { + pipe_name, + cancel_token, + } => { + // Named Pipe Reader: retries with backoff due to expected transient errors + let data = read_from_named_pipe(pipe_name, cancel_token).await.expect("didn't receive data"); + Ok((data, MessageSource::NamedPipe(Arc::clone(pipe_name)))) + } + } + } +} + +// Maximum number of consecutive errors before giving up on named pipe operations. +// Backoff formula: 10ms * 2^error_count, capped at 2^6 +// With MAX = 5: backoffs are 20ms, 40ms, 80ms, 160ms (total: 300ms before giving up) +#[cfg(windows)] +const MAX_NAMED_PIPE_ERRORS: u32 = 5; + +/// Creates a Windows named pipe and waits for a client to connect. +/// +/// Note: Windows named pipes have transient failures. Retries are centralized in the read function. +/// If most retries are pipe creation errors, we could optimize with additional retry logic here? #[cfg(windows)] async fn create_and_connect_named_pipe( pipe_name: &str, ) -> std::io::Result { - // Create named pipe (single attempt) let named_pipe = ServerOptions::new() + .access_outbound(false) .first_pipe_instance(false) + .write_dac() .create(pipe_name) .map_err(|e| { error!("Failed to create named pipe: {}", e); @@ -91,108 +142,101 @@ async fn create_and_connect_named_pipe( Ok(named_pipe) } -impl BufferReader { - async fn read(&self) -> std::io::Result<(Vec, MessageSource)> { - match self { - BufferReader::UdpSocketReader(socket) => { - // TODO(astuyve) this should be dynamic - // Max buffer size is configurable in Go Agent and the default is 8KB - // https://github.com/DataDog/datadog-agent/blob/85939a62b5580b2a15549f6936f257e61c5aa153/pkg/config/config_template.yaml#L2154-L2158 - let mut buf = [0; 8192]; +/// Reads data from a Windows named pipe with retry logic. +/// +/// Windows named pipes can experience transient failures (client disconnect, pipe errors). +/// This function implements a retry loop with exponential backoff to handle these failures +/// gracefully while allowing responsive shutdown via the cancel_token. +#[cfg(windows)] +async fn read_from_named_pipe( + pipe_name: &str, + cancel_token: &tokio_util::sync::CancellationToken, +) -> std::io::Result> { + let mut consecutive_errors = 0; + let mut current_pipe: Option = None; + + loop { + // Check if cancelled between operations + if cancel_token.is_cancelled() { + return Err(std::io::Error::new( + std::io::ErrorKind::Interrupted, + "Operation cancelled", + )); + } - #[allow(clippy::expect_used)] - let (amt, src) = socket - .recv_from(&mut buf) - .await - .expect("didn't receive data"); - Ok((buf[..amt].to_owned(), MessageSource::Network(src))) - } - #[cfg(windows)] - BufferReader::NamedPipeReader { - pipe_name, - cancel_token, - } => { - let mut consecutive_errors = 0; - let mut current_pipe: Option = - None; + // Create pipe if needed (initial startup, after client disconnect, or after error) + if current_pipe.is_none() { + match create_and_connect_named_pipe(pipe_name).await { + Ok(new_pipe) => { + consecutive_errors = 0; + current_pipe = Some(new_pipe); + } + Err(e) => { + // Handle pipe creation/connection errors with retry logic + consecutive_errors += 1; - loop { - // Check if cancelled - if cancel_token.is_cancelled() { + if consecutive_errors >= MAX_NAMED_PIPE_ERRORS { return Err(std::io::Error::new( - std::io::ErrorKind::Interrupted, - "Operation cancelled", + std::io::ErrorKind::Other, + format!("Too many consecutive pipe errors: {}", e), )); } - // Create pipe if needed (initial startup, after client disconnect, or after error) - if current_pipe.is_none() { - match create_and_connect_named_pipe(pipe_name).await { - Ok(new_pipe) => { - consecutive_errors = 0; - current_pipe = Some(new_pipe); - } - Err(e) => { - // Handle pipe creation/connection errors with retry logic - consecutive_errors += 1; - - if consecutive_errors >= MAX_NAMED_PIPE_ERRORS { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - format!("Too many consecutive pipe errors: {}", e), - )); - } - - let backoff_ms = 10u64 * (1 << consecutive_errors.min(6)); - sleep(Duration::from_millis(backoff_ms)).await; - continue; - } - } - } + let backoff_ms = 10u64 * (1 << consecutive_errors.min(6)); + sleep(Duration::from_millis(backoff_ms)).await; + continue; + } + } + } - // Read from the connected pipe - let mut buf = [0; 8192]; - let pipe = current_pipe.as_mut().unwrap(); - - match pipe.read(&mut buf).await { - Ok(0) => { - current_pipe = None; - continue; - } - Ok(amt) => { - return Ok(( - buf[..amt].to_vec(), - MessageSource::NamedPipe(pipe_name.to_string()), - )); - } - Err(e) => { - // Read error - error!("Error reading from named pipe: {}", e); - consecutive_errors += 1; - current_pipe = None; - - if consecutive_errors >= MAX_NAMED_PIPE_ERRORS { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Too many consecutive read errors", - )); - } - - let backoff_ms = 10u64 * (1 << consecutive_errors.min(6)); - sleep(Duration::from_millis(backoff_ms)).await; - continue; - } - } + // Read from the connected pipe + let mut buf = [0; BUFFER_SIZE]; + let pipe = current_pipe.as_mut().unwrap(); + + match pipe.read(&mut buf).await { + Ok(0) => { + // Client disconnected gracefully + current_pipe = None; + continue; + } + Ok(amt) => { + // Successfully read data + return Ok(buf[..amt].to_vec()); + } + Err(e) => { + // Read error - retry with backoff + error!("Error reading from named pipe: {}", e); + consecutive_errors += 1; + current_pipe = None; + + if consecutive_errors >= MAX_NAMED_PIPE_ERRORS { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Too many consecutive read errors", + )); } - }, - BufferReader::MirrorReader(data, socket) => { - Ok((data.clone(), MessageSource::Network(*socket))) + + let backoff_ms = 10u64 * (1 << consecutive_errors.min(6)); + sleep(Duration::from_millis(backoff_ms)).await; + continue; } } } } +/// TDogStatsD server to receive, parse, and forward metrics. +pub struct DogStatsD { + cancel_token: tokio_util::sync::CancellationToken, + aggregator_handle: AggregatorHandle, + buffer_reader: BufferReader, + metric_namespace: Option, +} + impl DogStatsD { + /// Creates a new DogStatsD server instance. + /// + /// The server will bind to either a UDP socket or Windows named pipe based on the config. + /// Metrics received will be forwarded to the provided aggregator_handle. #[must_use] pub async fn new( config: &DogStatsDConfig, @@ -204,7 +248,7 @@ impl DogStatsD { #[cfg(windows)] { BufferReader::NamedPipeReader { - pipe_name: pipe_name.clone(), + pipe_name: Arc::new(pipe_name.clone()), cancel_token: cancel_token.clone(), } } @@ -232,6 +276,7 @@ impl DogStatsD { } } + /// Main event loop that continuously receives and processes metrics. pub async fn spin(self) { let mut spin_cancelled = false; while !spin_cancelled { @@ -240,6 +285,7 @@ impl DogStatsD { } } + /// Receive one batch of metrics from the transport layer and process them. async fn consume_statsd(&self) { #[allow(clippy::expect_used)] let (buf, src) = self @@ -255,12 +301,7 @@ impl DogStatsD { self.insert_metrics(statsd_metric_strings); } - fn prepend_namespace(namespace: &str, metric: &mut Metric) { - let new_name = format!("{}.{}", namespace, metric.name); - metric.name = ustr::Ustr::from(&new_name); - metric.id = id(metric.name, &metric.tags, metric.timestamp); - } - + /// Parses metrics from raw DogStatsD format and forwards them to the aggregator. fn insert_metrics(&self, msg: Split) { let namespace = self.metric_namespace.as_deref(); let all_valid_metrics: Vec = msg @@ -301,6 +342,13 @@ impl DogStatsD { } } } + + /// Prepends a namespace to a metric's name and updates its ID. + fn prepend_namespace(namespace: &str, metric: &mut Metric) { + let new_name = format!("{}.{}", namespace, metric.name); + metric.name = ustr::Ustr::from(&new_name); + metric.id = id(metric.name, &metric.tags, metric.timestamp); + } } #[cfg(test)] From 4812a8051d7c9b0796cb06a670911e8ee78ebd1c Mon Sep 17 00:00:00 2001 From: Lewis Date: Fri, 26 Dec 2025 16:51:49 -0500 Subject: [PATCH 07/21] Add test coverage for named pipe code --- crates/dogstatsd/src/dogstatsd.rs | 4 +- crates/dogstatsd/tests/integration_test.rs | 161 +++++++++++++++++++++ 2 files changed, 164 insertions(+), 1 deletion(-) diff --git a/crates/dogstatsd/src/dogstatsd.rs b/crates/dogstatsd/src/dogstatsd.rs index 8dae1d7..218a146 100644 --- a/crates/dogstatsd/src/dogstatsd.rs +++ b/crates/dogstatsd/src/dogstatsd.rs @@ -105,7 +105,9 @@ impl BufferReader { cancel_token, } => { // Named Pipe Reader: retries with backoff due to expected transient errors - let data = read_from_named_pipe(pipe_name, cancel_token).await.expect("didn't receive data"); + let data = read_from_named_pipe(pipe_name, cancel_token) + .await + .expect("didn't receive data"); Ok((data, MessageSource::NamedPipe(Arc::clone(pipe_name)))) } } diff --git a/crates/dogstatsd/tests/integration_test.rs b/crates/dogstatsd/tests/integration_test.rs index 72d8e4b..3ac17c2 100644 --- a/crates/dogstatsd/tests/integration_test.rs +++ b/crates/dogstatsd/tests/integration_test.rs @@ -20,6 +20,9 @@ use tokio::{ use tokio_util::sync::CancellationToken; use zstd::zstd_safe::CompressionLevel; +#[cfg(windows)] +use tokio::{io::AsyncWriteExt, net::windows::named_pipe::ClientOptions}; + #[cfg(test)] #[tokio::test] async fn dogstatsd_server_ships_series() { @@ -281,3 +284,161 @@ async fn test_send_with_retry_immediate_failure_after_one_attempt() { // Verify that the mock was called exactly once mock.assert_async().await; } + +#[cfg(test)] +#[cfg(windows)] +#[tokio::test] +async fn test_named_pipe_basic_communication() { + let pipe_name = r"\\.\pipe\test_dogstatsd_basic"; + let (service, handle) = AggregatorService::new(SortedTags::parse("test:value").unwrap(), 1_024) + .expect("aggregator service creation failed"); + tokio::spawn(service.run()); + + let cancel_token = CancellationToken::new(); + + // Start DogStatsD server + let dogstatsd_task = { + let handle = handle.clone(); + let cancel_token = cancel_token.clone(); + tokio::spawn(async move { + let dogstatsd = DogStatsD::new( + &DogStatsDConfig { + host: String::new(), + port: 0, + metric_namespace: None, + windows_pipe_name: Some(pipe_name.to_string()), + }, + handle, + cancel_token, + ) + .await; + dogstatsd.spin().await; + }) + }; + + sleep(Duration::from_millis(100)).await; + + // Connect client and send metric + let mut client = ClientOptions::new().open(pipe_name).expect("client open"); + client + .write_all(b"test.metric:42|c\n") + .await + .expect("write failed"); + client.flush().await.expect("flush failed"); + + sleep(Duration::from_millis(100)).await; + + // Verify metric was received + let response = handle.flush().await.expect("flush failed"); + assert_eq!(response.series.len(), 1); + assert_eq!(response.series[0].series[0].metric, "test.metric"); + + // Cleanup + cancel_token.cancel(); + drop(client); + let _ = timeout(Duration::from_millis(500), dogstatsd_task).await; + handle.shutdown().expect("shutdown failed"); +} + +#[cfg(test)] +#[cfg(windows)] +#[tokio::test] +async fn test_named_pipe_disconnect_reconnect() { + let pipe_name = r"\\.\pipe\test_dogstatsd_reconnect"; + let (service, handle) = AggregatorService::new(SortedTags::parse("test:value").unwrap(), 1_024) + .expect("aggregator service creation failed"); + tokio::spawn(service.run()); + + let cancel_token = CancellationToken::new(); + + // Start DogStatsD server + let dogstatsd_task = { + let handle = handle.clone(); + let cancel_token = cancel_token.clone(); + tokio::spawn(async move { + let dogstatsd = DogStatsD::new( + &DogStatsDConfig { + host: String::new(), + port: 0, + metric_namespace: None, + windows_pipe_name: Some(pipe_name.to_string()), + }, + handle, + cancel_token, + ) + .await; + dogstatsd.spin().await; + }) + }; + + sleep(Duration::from_millis(100)).await; + + // First client - connect, send, disconnect + { + let mut client1 = ClientOptions::new().open(pipe_name).expect("client1 open"); + client1.write_all(b"metric1:1|c\n").await.expect("write1"); + client1.flush().await.expect("flush1"); + } // client1 drops here (disconnect) + + sleep(Duration::from_millis(200)).await; + + // Second client - reconnect and send + let mut client2 = ClientOptions::new().open(pipe_name).expect("client2 open"); + client2.write_all(b"metric2:2|c\n").await.expect("write2"); + client2.flush().await.expect("flush2"); + + sleep(Duration::from_millis(100)).await; + + // Verify both metrics received + let response = handle.flush().await.expect("flush failed"); + assert_eq!(response.series.len(), 2); + + // Cleanup + cancel_token.cancel(); + drop(client2); + let _ = timeout(Duration::from_millis(500), dogstatsd_task).await; + handle.shutdown().expect("shutdown failed"); +} + +#[cfg(test)] +#[cfg(windows)] +#[tokio::test] +async fn test_named_pipe_cancellation() { + let pipe_name = r"\\.\pipe\test_dogstatsd_cancel"; + let (service, handle) = AggregatorService::new(SortedTags::parse("test:value").unwrap(), 1_024) + .expect("aggregator service creation failed"); + tokio::spawn(service.run()); + + let cancel_token = CancellationToken::new(); + + // Start DogStatsD server + let dogstatsd_task = { + let handle = handle.clone(); + let cancel_token = cancel_token.clone(); + tokio::spawn(async move { + let dogstatsd = DogStatsD::new( + &DogStatsDConfig { + host: String::new(), + port: 0, + metric_namespace: None, + windows_pipe_name: Some(pipe_name.to_string()), + }, + handle, + cancel_token, + ) + .await; + dogstatsd.spin().await; + }) + }; + + sleep(Duration::from_millis(100)).await; + + // Cancel immediately + cancel_token.cancel(); + + // Task should complete quickly + let result = timeout(Duration::from_millis(500), dogstatsd_task).await; + assert!(result.is_ok(), "task should complete after cancellation"); + + handle.shutdown().expect("shutdown failed"); +} From 5fe4b2b7d588ab49e32f34f5a4fd1dd86ddaf372 Mon Sep 17 00:00:00 2001 From: Lewis Date: Mon, 29 Dec 2025 09:47:26 -0500 Subject: [PATCH 08/21] Cancel token behavior for named pipe loops --- crates/dogstatsd/src/dogstatsd.rs | 101 +++++++++++++-------- crates/dogstatsd/tests/integration_test.rs | 43 ++++++--- 2 files changed, 93 insertions(+), 51 deletions(-) diff --git a/crates/dogstatsd/src/dogstatsd.rs b/crates/dogstatsd/src/dogstatsd.rs index 218a146..00c52f1 100644 --- a/crates/dogstatsd/src/dogstatsd.rs +++ b/crates/dogstatsd/src/dogstatsd.rs @@ -65,15 +65,15 @@ impl std::fmt::Display for MessageSource { // BufferReader abstracts transport methods for metric data. enum BufferReader { /// UDP socket reader (cross-platform, default transport) - UdpSocketReader(tokio::net::UdpSocket), + UdpSocket(tokio::net::UdpSocket), /// Mirror reader for testing - replays a fixed buffer #[allow(dead_code)] - MirrorReader(Vec, SocketAddr), + MirrorTest(Vec, SocketAddr), /// Windows named pipe reader (Windows-only transport) #[cfg(windows)] - NamedPipeReader { + NamedPipe { pipe_name: Arc, cancel_token: tokio_util::sync::CancellationToken, }, @@ -84,7 +84,7 @@ impl BufferReader { /// Note: Different transports have different blocking behaviors. async fn read(&self) -> std::io::Result<(Vec, MessageSource)> { match self { - BufferReader::UdpSocketReader(socket) => { + BufferReader::UdpSocket(socket) => { // UDP socket: blocks until a packet arrives let mut buf = [0; BUFFER_SIZE]; @@ -95,16 +95,17 @@ impl BufferReader { .expect("didn't receive data"); Ok((buf[..amt].to_owned(), MessageSource::Network(src))) } - BufferReader::MirrorReader(data, socket) => { + BufferReader::MirrorTest(data, socket) => { // Mirror Reader: returns immediately with stored data Ok((data.clone(), MessageSource::Network(*socket))) } #[cfg(windows)] - BufferReader::NamedPipeReader { + BufferReader::NamedPipe { pipe_name, cancel_token, } => { // Named Pipe Reader: retries with backoff due to expected transient errors + #[allow(clippy::expect_used)] let data = read_from_named_pipe(pipe_name, cancel_token) .await .expect("didn't receive data"); @@ -127,28 +128,35 @@ const MAX_NAMED_PIPE_ERRORS: u32 = 5; #[cfg(windows)] async fn create_and_connect_named_pipe( pipe_name: &str, + cancel_token: &tokio_util::sync::CancellationToken, ) -> std::io::Result { let named_pipe = ServerOptions::new() .access_outbound(false) .first_pipe_instance(false) - .write_dac() .create(pipe_name) .map_err(|e| { error!("Failed to create named pipe: {}", e); e })?; - // Wait for client to connect to named pipe - named_pipe.connect().await?; - debug!("Client connected to DogStatsD named pipe"); - Ok(named_pipe) + // Wait for client to connect to named pipe, or for cancellation + tokio::select! { + result = named_pipe.connect() => { + result?; + debug!("Client connected to DogStatsD named pipe"); + Ok(named_pipe) + } + _ = cancel_token.cancelled() => { + Err(std::io::Error::other("Server Shutdown, do not create a named pipe.")) + } + } } /// Reads data from a Windows named pipe with retry logic. /// /// Windows named pipes can experience transient failures (client disconnect, pipe errors). /// This function implements a retry loop with exponential backoff to handle these failures -/// gracefully while allowing responsive shutdown via the cancel_token. +/// and has additional logic to allow clean shutdown via cancel_token. #[cfg(windows)] async fn read_from_named_pipe( pipe_name: &str, @@ -157,31 +165,29 @@ async fn read_from_named_pipe( let mut consecutive_errors = 0; let mut current_pipe: Option = None; - loop { - // Check if cancelled between operations - if cancel_token.is_cancelled() { - return Err(std::io::Error::new( - std::io::ErrorKind::Interrupted, - "Operation cancelled", - )); - } - + // Let named pipes cancel cleanly when the server is shut down + 'reading_named_pipe: while !cancel_token.is_cancelled() { // Create pipe if needed (initial startup, after client disconnect, or after error) if current_pipe.is_none() { - match create_and_connect_named_pipe(pipe_name).await { + match create_and_connect_named_pipe(pipe_name, cancel_token).await { Ok(new_pipe) => { consecutive_errors = 0; current_pipe = Some(new_pipe); } Err(e) => { + // Check for cancellation before retrying + if cancel_token.is_cancelled() { + break 'reading_named_pipe; + } + // Handle pipe creation/connection errors with retry logic consecutive_errors += 1; if consecutive_errors >= MAX_NAMED_PIPE_ERRORS { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - format!("Too many consecutive pipe errors: {}", e), - )); + return Err(std::io::Error::other(format!( + "Too many consecutive pipe errors: {}", + e + ))); } let backoff_ms = 10u64 * (1 << consecutive_errors.min(6)); @@ -191,11 +197,24 @@ async fn read_from_named_pipe( } } - // Read from the connected pipe + // Read from the connected pipe. Use select! to allow cancellation during blocking read operation. let mut buf = [0; BUFFER_SIZE]; - let pipe = current_pipe.as_mut().unwrap(); - match pipe.read(&mut buf).await { + #[allow(clippy::expect_used)] + let pipe = current_pipe + .as_mut() + .expect("did not create and connect to a named pipe"); + + // Allow read operation to be interrupted by cancellation token for clean shutdown + let read_result = tokio::select! { + result = pipe.read(&mut buf) => result, + _ = cancel_token.cancelled() => { + // Server shutdown requested during read operation + return Err(std::io::Error::other("Server shutdown during pipe read")); + } + }; + + match read_result { Ok(0) => { // Client disconnected gracefully current_pipe = None; @@ -208,14 +227,19 @@ async fn read_from_named_pipe( Err(e) => { // Read error - retry with backoff error!("Error reading from named pipe: {}", e); - consecutive_errors += 1; current_pipe = None; + // Check for cancellation before retrying + if cancel_token.is_cancelled() { + break 'reading_named_pipe; + } + + consecutive_errors += 1; if consecutive_errors >= MAX_NAMED_PIPE_ERRORS { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Too many consecutive read errors", - )); + return Err(std::io::Error::other(format!( + "Too many consecutive read errors: {}", + e + ))); } let backoff_ms = 10u64 * (1 << consecutive_errors.min(6)); @@ -224,6 +248,9 @@ async fn read_from_named_pipe( } } } + + // If we exit due to cancellation, return an empty result. + Ok(Vec::new()) } /// TDogStatsD server to receive, parse, and forward metrics. @@ -249,7 +276,7 @@ impl DogStatsD { let buffer_reader = if let Some(ref pipe_name) = config.windows_pipe_name { #[cfg(windows)] { - BufferReader::NamedPipeReader { + BufferReader::NamedPipe { pipe_name: Arc::new(pipe_name.clone()), cancel_token: cancel_token.clone(), } @@ -267,7 +294,7 @@ impl DogStatsD { let socket = tokio::net::UdpSocket::bind(addr) .await .expect("couldn't bind to address"); - BufferReader::UdpSocketReader(socket) + BufferReader::UdpSocket(socket) }; DogStatsD { @@ -460,7 +487,7 @@ single_machine_performance.rouster.metrics_max_timestamp_latency:1376.90870216|d let dogstatsd = DogStatsD { cancel_token, aggregator_handle: handle.clone(), - buffer_reader: BufferReader::MirrorReader( + buffer_reader: BufferReader::MirrorTest( statsd_string.as_bytes().to_vec(), SocketAddr::new(IpAddr::V4(Ipv4Addr::new(111, 112, 113, 114)), 0), ), diff --git a/crates/dogstatsd/tests/integration_test.rs b/crates/dogstatsd/tests/integration_test.rs index 3ac17c2..ca791d9 100644 --- a/crates/dogstatsd/tests/integration_test.rs +++ b/crates/dogstatsd/tests/integration_test.rs @@ -46,7 +46,7 @@ async fn dogstatsd_server_ships_series() { // Start the service in a background task tokio::spawn(service.run()); - let _ = start_dogstatsd(handle.clone()).await; + let cancel_token = start_dogstatsd(handle.clone()).await; let api_key_factory = ApiKeyFactory::new("mock-api-key"); @@ -93,6 +93,9 @@ async fn dogstatsd_server_ships_series() { Ok(_) => mock.assert(), Err(_) => panic!("timed out before server received metric flush"), } + + // Cleanup + cancel_token.cancel(); } async fn start_dogstatsd(aggregator_handle: AggregatorHandle) -> CancellationToken { @@ -319,7 +322,12 @@ async fn test_named_pipe_basic_communication() { sleep(Duration::from_millis(100)).await; // Connect client and send metric - let mut client = ClientOptions::new().open(pipe_name).expect("client open"); + // Client opens with write-only access to match server's inbound-only configuration + let mut client = ClientOptions::new() + .read(false) + .write(true) + .open(pipe_name) + .expect("client open"); client .write_all(b"test.metric:42|c\n") .await @@ -331,12 +339,11 @@ async fn test_named_pipe_basic_communication() { // Verify metric was received let response = handle.flush().await.expect("flush failed"); assert_eq!(response.series.len(), 1); - assert_eq!(response.series[0].series[0].metric, "test.metric"); // Cleanup cancel_token.cancel(); - drop(client); - let _ = timeout(Duration::from_millis(500), dogstatsd_task).await; + let result = timeout(Duration::from_millis(500), dogstatsd_task).await; + assert!(result.is_ok(), "task should complete after cancellation"); handle.shutdown().expect("shutdown failed"); } @@ -354,7 +361,7 @@ async fn test_named_pipe_disconnect_reconnect() { // Start DogStatsD server let dogstatsd_task = { let handle = handle.clone(); - let cancel_token = cancel_token.clone(); + let cancel_token_clone = cancel_token.clone(); tokio::spawn(async move { let dogstatsd = DogStatsD::new( &DogStatsDConfig { @@ -364,7 +371,7 @@ async fn test_named_pipe_disconnect_reconnect() { windows_pipe_name: Some(pipe_name.to_string()), }, handle, - cancel_token, + cancel_token_clone.child_token(), ) .await; dogstatsd.spin().await; @@ -375,15 +382,23 @@ async fn test_named_pipe_disconnect_reconnect() { // First client - connect, send, disconnect { - let mut client1 = ClientOptions::new().open(pipe_name).expect("client1 open"); + let mut client1 = ClientOptions::new() + .read(false) + .write(true) + .open(pipe_name) + .expect("client1 open"); client1.write_all(b"metric1:1|c\n").await.expect("write1"); client1.flush().await.expect("flush1"); } // client1 drops here (disconnect) - sleep(Duration::from_millis(200)).await; + sleep(Duration::from_millis(100)).await; // Second client - reconnect and send - let mut client2 = ClientOptions::new().open(pipe_name).expect("client2 open"); + let mut client2 = ClientOptions::new() + .read(false) + .write(true) + .open(pipe_name) + .expect("client2 open"); client2.write_all(b"metric2:2|c\n").await.expect("write2"); client2.flush().await.expect("flush2"); @@ -395,8 +410,8 @@ async fn test_named_pipe_disconnect_reconnect() { // Cleanup cancel_token.cancel(); - drop(client2); - let _ = timeout(Duration::from_millis(500), dogstatsd_task).await; + let result = timeout(Duration::from_millis(500), dogstatsd_task).await; + assert!(result.is_ok(), "tasks should complete after cancellation"); handle.shutdown().expect("shutdown failed"); } @@ -414,7 +429,7 @@ async fn test_named_pipe_cancellation() { // Start DogStatsD server let dogstatsd_task = { let handle = handle.clone(); - let cancel_token = cancel_token.clone(); + let cancel_token_clone = cancel_token.clone(); tokio::spawn(async move { let dogstatsd = DogStatsD::new( &DogStatsDConfig { @@ -424,7 +439,7 @@ async fn test_named_pipe_cancellation() { windows_pipe_name: Some(pipe_name.to_string()), }, handle, - cancel_token, + cancel_token_clone.child_token(), ) .await; dogstatsd.spin().await; From f1f4ae073994cf487cb77341ddc0d59db6d401d0 Mon Sep 17 00:00:00 2001 From: Lewis Date: Mon, 29 Dec 2025 11:16:49 -0500 Subject: [PATCH 09/21] Looking at named pipe deletion and recreation --- .github/workflows/cargo.yml | 2 + .gitignore | 2 +- Cargo.lock | 446 ++++++++++++++------- crates/dogstatsd/Cargo.toml | 1 + crates/dogstatsd/src/dogstatsd.rs | 189 +++++---- crates/dogstatsd/tests/integration_test.rs | 44 +- 6 files changed, 430 insertions(+), 254 deletions(-) diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml index 35c7bd3..9b7103c 100644 --- a/.github/workflows/cargo.yml +++ b/.github/workflows/cargo.yml @@ -84,6 +84,8 @@ jobs: name: Test needs: setup runs-on: ${{ inputs.runner }} + env: + RUST_LOG: debug steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: taiki-e/install-action@9ba3ac3fd006a70c6e186a683577abc1ccf0ff3a # v2.54.0 diff --git a/.gitignore b/.gitignore index df769d2..d53232b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /target -/.idea +/.idea diff --git a/Cargo.lock b/Cargo.lock index 5f965b7..095f92b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,56 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.100" @@ -47,9 +97,9 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.4.1" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ "event-listener", "event-listener-strategy", @@ -74,7 +124,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -91,9 +141,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-fips-sys" -version = "0.13.10" +version = "0.13.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57900537c00a0565a35b63c4c281b372edfc9744b072fd4a3b414350a8f5ed48" +checksum = "df6ea8e07e2df15b9f09f2ac5ee2977369b06d116f0c4eb5fa4ad443b73c7f53" dependencies = [ "bindgen", "cc", @@ -105,9 +155,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.15.2" +version = "1.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288" +checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" dependencies = [ "aws-lc-fips-sys", "aws-lc-sys", @@ -116,9 +166,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.35.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1" +checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a" dependencies = [ "cc", "cmake", @@ -149,7 +199,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -237,9 +287,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.49" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", "jobserver", @@ -288,6 +338,12 @@ dependencies = [ "cc", ] +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -473,7 +529,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", "unicode-xid", ] @@ -495,7 +551,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -506,6 +562,7 @@ dependencies = [ "datadog-protos", "ddsketch-agent", "derive_more", + "env_logger", "fnv", "hashbrown 0.15.5", "mockito", @@ -547,6 +604,29 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -592,9 +672,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixedbitset" @@ -604,9 +684,9 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" dependencies = [ "crc32fast", "miniz_oxide", @@ -698,7 +778,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -749,9 +829,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -782,9 +862,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -941,7 +1021,7 @@ dependencies = [ "similar", "stringmetrics", "tabwriter", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "url", @@ -1005,19 +1085,18 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.4", + "webpki-roots 1.0.5", ] [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -1136,9 +1215,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", @@ -1152,14 +1231,20 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.13.0" @@ -1180,9 +1265,33 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jiff" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] [[package]] name = "jobserver" @@ -1196,9 +1305,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -1212,9 +1321,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libdd-common" @@ -1424,9 +1533,9 @@ dependencies = [ [[package]] name = "mockito" -version = "1.7.1" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e0603425789b4a70fcc4ac4f5a46a566c116ee3e2a6b768dc623f7719c611de" +checksum = "90820618712cab19cfc46b274c6c22546a82affcb3c3bdf0f29e3db8e1bb92c0" dependencies = [ "assert-json-diff", "bytes", @@ -1498,11 +1607,17 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "openssl-probe" -version = "0.1.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "ordered-float" @@ -1542,12 +1657,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "path-tree" version = "0.8.3" @@ -1590,7 +1699,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1611,6 +1720,21 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1636,7 +1760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1665,9 +1789,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -1717,7 +1841,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.111", + "syn 2.0.114", "tempfile", ] @@ -1731,7 +1855,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -1815,7 +1939,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -1836,7 +1960,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -1858,9 +1982,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.42" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -1889,7 +2013,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -1909,7 +2033,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -1918,14 +2042,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -1936,7 +2060,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -1979,9 +2103,9 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.26" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b4c14b2d9afca6a60277086b0cc6a6ae0b568f6f7916c943a8cdc79f8be240f" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", @@ -2013,7 +2137,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.4", + "webpki-roots 1.0.5", ] [[package]] @@ -2024,7 +2148,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -2032,33 +2156,29 @@ dependencies = [ [[package]] name = "rmp" -version = "0.8.14" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" dependencies = [ - "byteorder", "num-traits", - "paste", ] [[package]] name = "rmp-serde" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" dependencies = [ - "byteorder", "rmp", "serde", ] [[package]] name = "rmpv" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58450723cd9ee93273ce44a20b6ec4efe17f8ed2e3631474387bfdecf18bb2a9" +checksum = "7a4e1d4b9b938a26d2996af33229f0ca0956c652c1375067f0b45291c1df8417" dependencies = [ - "num-traits", "rmp", ] @@ -2083,9 +2203,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags", "errno", @@ -2096,9 +2216,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "aws-lc-rs", "once_cell", @@ -2111,9 +2231,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -2132,9 +2252,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -2142,9 +2262,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "aws-lc-rs", "ring", @@ -2172,9 +2292,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "schannel" @@ -2261,20 +2381,20 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -2321,7 +2441,7 @@ checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -2352,10 +2472,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -2373,9 +2494,9 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -2385,9 +2506,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", "windows-sys 0.60.2", @@ -2429,9 +2550,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.111" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -2455,7 +2576,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -2469,14 +2590,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix 1.1.2", + "rustix 1.1.3", "windows-sys 0.61.2", ] @@ -2491,11 +2612,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -2506,18 +2627,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -2556,9 +2677,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", "libc", @@ -2578,7 +2699,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -2593,9 +2714,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -2604,9 +2725,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -2647,14 +2768,14 @@ dependencies = [ "prost-build", "prost-types", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -2715,7 +2836,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -2775,7 +2896,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -2822,9 +2943,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -2856,6 +2977,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "valuable" version = "0.1.1" @@ -2894,18 +3021,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -2916,11 +3043,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -2929,9 +3057,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2939,31 +3067,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -2985,14 +3113,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.4", + "webpki-roots 1.0.5", ] [[package]] name = "webpki-roots" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" dependencies = [ "rustls-pki-types", ] @@ -3182,9 +3310,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" @@ -3211,28 +3339,28 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] [[package]] @@ -3252,7 +3380,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", "synstructure", ] @@ -3292,9 +3420,15 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.114", ] +[[package]] +name = "zmij" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" + [[package]] name = "zstd" version = "0.13.3" diff --git a/crates/dogstatsd/Cargo.toml b/crates/dogstatsd/Cargo.toml index 4ec6e09..b0efdc7 100644 --- a/crates/dogstatsd/Cargo.toml +++ b/crates/dogstatsd/Cargo.toml @@ -28,6 +28,7 @@ datadog-fips = { path = "../datadog-fips", default-features = false } rustls-pemfile = { version = "2.0", default-features = false, features = ["std"] } [dev-dependencies] +env_logger = "0.11" mockito = { version = "1.5.0", default-features = false } proptest = "1.4.0" tracing-test = { version = "0.2.5", default-features = false } diff --git a/crates/dogstatsd/src/dogstatsd.rs b/crates/dogstatsd/src/dogstatsd.rs index 00c52f1..45f67b2 100644 --- a/crates/dogstatsd/src/dogstatsd.rs +++ b/crates/dogstatsd/src/dogstatsd.rs @@ -30,6 +30,12 @@ use { // https://github.com/DataDog/datadog-agent/blob/85939a62b5580b2a15549f6936f257e61c5aa153/pkg/config/config_template.yaml#L2154-L2158 const BUFFER_SIZE: usize = 8192; +// Maximum number of consecutive errors before giving up on named pipe operations. +// Backoff formula: 10ms * 2^error_count, capped at MAX to prevent overflow +// With MAX = 5: backoffs are 20ms, 40ms, 80ms, 160ms, 320ms (total: ~600ms before giving up) +#[cfg(windows)] +const MAX_NAMED_PIPE_ERRORS: u32 = 5; + /// Configuration for the DogStatsD server pub struct DogStatsDConfig { /// Host to bind UDP socket to (e.g., "127.0.0.1") @@ -115,47 +121,37 @@ impl BufferReader { } } -// Maximum number of consecutive errors before giving up on named pipe operations. -// Backoff formula: 10ms * 2^error_count, capped at 2^6 -// With MAX = 5: backoffs are 20ms, 40ms, 80ms, 160ms (total: 300ms before giving up) -#[cfg(windows)] -const MAX_NAMED_PIPE_ERRORS: u32 = 5; - -/// Creates a Windows named pipe and waits for a client to connect. -/// -/// Note: Windows named pipes have transient failures. Retries are centralized in the read function. -/// If most retries are pipe creation errors, we could optimize with additional retry logic here? +/// Helper to handle retry logic with exponential backoff for named pipe errors. +/// Returns true if should continue retrying, false if should break (cancelled). #[cfg(windows)] -async fn create_and_connect_named_pipe( - pipe_name: &str, +async fn handle_pipe_error_with_backoff( + consecutive_errors: &mut u32, + error_type: &str, + error: &std::io::Error, cancel_token: &tokio_util::sync::CancellationToken, -) -> std::io::Result { - let named_pipe = ServerOptions::new() - .access_outbound(false) - .first_pipe_instance(false) - .create(pipe_name) - .map_err(|e| { - error!("Failed to create named pipe: {}", e); - e - })?; - - // Wait for client to connect to named pipe, or for cancellation +) -> std::io::Result { + *consecutive_errors += 1; + + if *consecutive_errors >= MAX_NAMED_PIPE_ERRORS { + return Err(std::io::Error::other(format!( + "Too many consecutive {} errors: {}", + error_type, error + ))); + } + + let backoff_ms = 10u64 * (1 << *consecutive_errors); + + // Sleep with cancellation support for clean, fast shutdown tokio::select! { - result = named_pipe.connect() => { - result?; - debug!("Client connected to DogStatsD named pipe"); - Ok(named_pipe) - } - _ = cancel_token.cancelled() => { - Err(std::io::Error::other("Server Shutdown, do not create a named pipe.")) - } + _ = sleep(Duration::from_millis(backoff_ms)) => Ok(true), + _ = cancel_token.cancelled() => Ok(false), } } /// Reads data from a Windows named pipe with retry logic. /// /// Windows named pipes can experience transient failures (client disconnect, pipe errors). -/// This function implements a retry loop with exponential backoff to handle these failures +/// This function uses a retry loop with exponential backoff to handle these failures /// and has additional logic to allow clean shutdown via cancel_token. #[cfg(windows)] async fn read_from_named_pipe( @@ -164,96 +160,130 @@ async fn read_from_named_pipe( ) -> std::io::Result> { let mut consecutive_errors = 0; let mut current_pipe: Option = None; + let mut needs_connection = true; // Track whether we need to wait for a client // Let named pipes cancel cleanly when the server is shut down - 'reading_named_pipe: while !cancel_token.is_cancelled() { - // Create pipe if needed (initial startup, after client disconnect, or after error) + while !cancel_token.is_cancelled() { + // Create pipe if needed (initial startup or after error) if current_pipe.is_none() { - match create_and_connect_named_pipe(pipe_name, cancel_token).await { + debug!("Creating named pipe: {}", pipe_name); + match ServerOptions::new().create(pipe_name) { Ok(new_pipe) => { - consecutive_errors = 0; + consecutive_errors = 0; // Reset on successful pipe creation current_pipe = Some(new_pipe); + needs_connection = true; } Err(e) => { - // Check for cancellation before retrying - if cancel_token.is_cancelled() { - break 'reading_named_pipe; + error!("Failed to create named pipe: {}", e); + if !handle_pipe_error_with_backoff( + &mut consecutive_errors, + "pipe creation", + &e, + cancel_token, + ) + .await? + { + break; } + continue; + } + } + } - // Handle pipe creation/connection errors with retry logic - consecutive_errors += 1; + // Wait for client connection if needed (after creation or after disconnect) + if needs_connection { + #[allow(clippy::expect_used)] + let pipe = current_pipe.as_ref().expect("pipe must exist"); - if consecutive_errors >= MAX_NAMED_PIPE_ERRORS { - return Err(std::io::Error::other(format!( - "Too many consecutive pipe errors: {}", - e - ))); - } + debug!("Waiting for client connection on named pipe"); + let connect_result = tokio::select! { + result = pipe.connect() => result, + _ = cancel_token.cancelled() => { + break; + } + }; - let backoff_ms = 10u64 * (1 << consecutive_errors.min(6)); - sleep(Duration::from_millis(backoff_ms)).await; + match connect_result { + Ok(()) => { + debug!("Client connected to DogStatsD named pipe"); + consecutive_errors = 0; + needs_connection = false; + } + Err(e) => { + // Connection failed - recreate pipe on next iteration + current_pipe = None; + if !handle_pipe_error_with_backoff( + &mut consecutive_errors, + "connection", + &e, + cancel_token, + ) + .await? + { + break; + } continue; } } } - // Read from the connected pipe. Use select! to allow cancellation during blocking read operation. + // Read from the connected pipe + debug!("Starting read from named pipe"); let mut buf = [0; BUFFER_SIZE]; #[allow(clippy::expect_used)] let pipe = current_pipe .as_mut() - .expect("did not create and connect to a named pipe"); + .expect("pipe must exist and be connected"); - // Allow read operation to be interrupted by cancellation token for clean shutdown let read_result = tokio::select! { result = pipe.read(&mut buf) => result, _ = cancel_token.cancelled() => { - // Server shutdown requested during read operation - return Err(std::io::Error::other("Server shutdown during pipe read")); + return Ok(Vec::new()); } }; match read_result { Ok(0) => { - // Client disconnected gracefully - current_pipe = None; + // Client disconnected - reuse pipe by disconnecting and waiting for new client + debug!("Client disconnected, preparing for reconnection"); + if let Err(e) = pipe.disconnect() { + error!("Failed to disconnect pipe: {}", e); + current_pipe = None; // Recreate on error + } else { + needs_connection = true; // Wait for new client on same pipe + } continue; } Ok(amt) => { - // Successfully read data + // this is the success state: we read some data from the pipe + debug!("Read {} bytes from named pipe", amt); return Ok(buf[..amt].to_vec()); } Err(e) => { - // Read error - retry with backoff error!("Error reading from named pipe: {}", e); - current_pipe = None; - - // Check for cancellation before retrying - if cancel_token.is_cancelled() { - break 'reading_named_pipe; + current_pipe = None; // Recreate pipe on read error + + if !handle_pipe_error_with_backoff( + &mut consecutive_errors, + "read", + &e, + cancel_token, + ) + .await? + { + break; } - - consecutive_errors += 1; - if consecutive_errors >= MAX_NAMED_PIPE_ERRORS { - return Err(std::io::Error::other(format!( - "Too many consecutive read errors: {}", - e - ))); - } - - let backoff_ms = 10u64 * (1 << consecutive_errors.min(6)); - sleep(Duration::from_millis(backoff_ms)).await; continue; } } } - // If we exit due to cancellation, return an empty result. + // If we exit due to cancellation, return an empty result for a clean shutdown. Ok(Vec::new()) } -/// TDogStatsD server to receive, parse, and forward metrics. +/// DogStatsD server to receive, parse, and forward metrics. pub struct DogStatsD { cancel_token: tokio_util::sync::CancellationToken, aggregator_handle: AggregatorHandle, @@ -365,6 +395,13 @@ impl DogStatsD { }) .collect(); if !all_valid_metrics.is_empty() { + debug!( + "Inserting {} metrics into aggregator", + all_valid_metrics.len() + ); + for metric in &all_valid_metrics { + debug!(" - {}: {:?}", metric.name, metric.value); + } // Send metrics through the channel - no lock needed! if let Err(e) = self.aggregator_handle.insert_batch(all_valid_metrics) { error!("Failed to send metrics to aggregator: {}", e); diff --git a/crates/dogstatsd/tests/integration_test.rs b/crates/dogstatsd/tests/integration_test.rs index ca791d9..d84f0b0 100644 --- a/crates/dogstatsd/tests/integration_test.rs +++ b/crates/dogstatsd/tests/integration_test.rs @@ -292,6 +292,8 @@ async fn test_send_with_retry_immediate_failure_after_one_attempt() { #[cfg(windows)] #[tokio::test] async fn test_named_pipe_basic_communication() { + let _ = env_logger::builder().is_test(true).try_init(); + let pipe_name = r"\\.\pipe\test_dogstatsd_basic"; let (service, handle) = AggregatorService::new(SortedTags::parse("test:value").unwrap(), 1_024) .expect("aggregator service creation failed"); @@ -322,12 +324,7 @@ async fn test_named_pipe_basic_communication() { sleep(Duration::from_millis(100)).await; // Connect client and send metric - // Client opens with write-only access to match server's inbound-only configuration - let mut client = ClientOptions::new() - .read(false) - .write(true) - .open(pipe_name) - .expect("client open"); + let mut client = ClientOptions::new().open(pipe_name).expect("client open"); client .write_all(b"test.metric:42|c\n") .await @@ -351,6 +348,8 @@ async fn test_named_pipe_basic_communication() { #[cfg(windows)] #[tokio::test] async fn test_named_pipe_disconnect_reconnect() { + let _ = env_logger::builder().is_test(true).try_init(); + let pipe_name = r"\\.\pipe\test_dogstatsd_reconnect"; let (service, handle) = AggregatorService::new(SortedTags::parse("test:value").unwrap(), 1_024) .expect("aggregator service creation failed"); @@ -382,31 +381,32 @@ async fn test_named_pipe_disconnect_reconnect() { // First client - connect, send, disconnect { - let mut client1 = ClientOptions::new() - .read(false) - .write(true) - .open(pipe_name) - .expect("client1 open"); - client1.write_all(b"metric1:1|c\n").await.expect("write1"); + let mut client1 = ClientOptions::new().open(pipe_name).expect("client1 open"); + client1 + .write_all(b"test.metric:1|c\n") + .await + .expect("write1"); client1.flush().await.expect("flush1"); } // client1 drops here (disconnect) sleep(Duration::from_millis(100)).await; - // Second client - reconnect and send - let mut client2 = ClientOptions::new() - .read(false) - .write(true) - .open(pipe_name) - .expect("client2 open"); - client2.write_all(b"metric2:2|c\n").await.expect("write2"); + // Second client - connect and send (creates new pipe each time) + let mut client2 = ClientOptions::new().open(pipe_name).expect("client2 open"); + client2 + .write_all(b"test.metric:2|c\n") + .await + .expect("write2"); client2.flush().await.expect("flush2"); sleep(Duration::from_millis(100)).await; - // Verify both metrics received + // Verify both metrics received and aggregated let response = handle.flush().await.expect("flush failed"); - assert_eq!(response.series.len(), 2); + assert!( + !response.series.is_empty(), + "Expected at least one series with metrics" + ); // Cleanup cancel_token.cancel(); @@ -419,6 +419,8 @@ async fn test_named_pipe_disconnect_reconnect() { #[cfg(windows)] #[tokio::test] async fn test_named_pipe_cancellation() { + let _ = env_logger::builder().is_test(true).try_init(); + let pipe_name = r"\\.\pipe\test_dogstatsd_cancel"; let (service, handle) = AggregatorService::new(SortedTags::parse("test:value").unwrap(), 1_024) .expect("aggregator service creation failed"); From 41beaba59e1af0034c28f28baab17e89b68d8bdd Mon Sep 17 00:00:00 2001 From: Lewis Date: Mon, 29 Dec 2025 14:30:09 -0500 Subject: [PATCH 10/21] Consistent debugging and cleanup for named pipe dogstatsd code --- .github/workflows/cargo.yml | 2 - Cargo.lock | 137 ------------------ crates/datadog-trace-agent/src/config.rs | 6 - .../src/trace_processor.rs | 2 +- crates/dogstatsd/Cargo.toml | 1 - crates/dogstatsd/src/dogstatsd.rs | 69 ++++----- crates/dogstatsd/tests/integration_test.rs | 72 ++++++++- 7 files changed, 97 insertions(+), 192 deletions(-) diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml index 9b7103c..35c7bd3 100644 --- a/.github/workflows/cargo.yml +++ b/.github/workflows/cargo.yml @@ -84,8 +84,6 @@ jobs: name: Test needs: setup runs-on: ${{ inputs.runner }} - env: - RUST_LOG: debug steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: taiki-e/install-action@9ba3ac3fd006a70c6e186a683577abc1ccf0ff3a # v2.54.0 diff --git a/Cargo.lock b/Cargo.lock index 095f92b..73f82fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,56 +29,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - [[package]] name = "anyhow" version = "1.0.100" @@ -338,12 +288,6 @@ dependencies = [ "cc", ] -[[package]] -name = "colorchoice" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -562,7 +506,6 @@ dependencies = [ "datadog-protos", "ddsketch-agent", "derive_more", - "env_logger", "fnv", "hashbrown 0.15.5", "mockito", @@ -604,29 +547,6 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -[[package]] -name = "env_filter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -1239,12 +1159,6 @@ dependencies = [ "serde", ] -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - [[package]] name = "itertools" version = "0.13.0" @@ -1269,30 +1183,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" -[[package]] -name = "jiff" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", -] - -[[package]] -name = "jiff-static" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "jobserver" version = "0.1.34" @@ -1607,12 +1497,6 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - [[package]] name = "openssl-probe" version = "0.2.1" @@ -1720,21 +1604,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "portable-atomic-util" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" -dependencies = [ - "portable-atomic", -] - [[package]] name = "potential_utf" version = "0.1.4" @@ -2977,12 +2846,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - [[package]] name = "valuable" version = "0.1.1" diff --git a/crates/datadog-trace-agent/src/config.rs b/crates/datadog-trace-agent/src/config.rs index 574e373..cf93485 100644 --- a/crates/datadog-trace-agent/src/config.rs +++ b/crates/datadog-trace-agent/src/config.rs @@ -24,12 +24,6 @@ pub struct Tags { function_tags_string: OnceLock, } -impl Default for Tags { - fn default() -> Self { - Self::new() - } -} - impl Tags { pub fn from_env_string(env_tags: &str) -> Self { let mut tags = HashMap::new(); diff --git a/crates/datadog-trace-agent/src/trace_processor.rs b/crates/datadog-trace-agent/src/trace_processor.rs index 87550fc..fb30809 100644 --- a/crates/datadog-trace-agent/src/trace_processor.rs +++ b/crates/datadog-trace-agent/src/trace_processor.rs @@ -104,7 +104,7 @@ impl TraceProcessor for ServerlessTraceProcessor { // double check content length is < max request content length in case transfer encoding is used if body_size > config.max_request_content_length { return log_and_create_http_response( - "Error processing traces: Payload too large", + &format!("Error processing traces: Payload too large"), StatusCode::PAYLOAD_TOO_LARGE, ); } diff --git a/crates/dogstatsd/Cargo.toml b/crates/dogstatsd/Cargo.toml index b0efdc7..4ec6e09 100644 --- a/crates/dogstatsd/Cargo.toml +++ b/crates/dogstatsd/Cargo.toml @@ -28,7 +28,6 @@ datadog-fips = { path = "../datadog-fips", default-features = false } rustls-pemfile = { version = "2.0", default-features = false, features = ["std"] } [dev-dependencies] -env_logger = "0.11" mockito = { version = "1.5.0", default-features = false } proptest = "1.4.0" tracing-test = { version = "0.2.5", default-features = false } diff --git a/crates/dogstatsd/src/dogstatsd.rs b/crates/dogstatsd/src/dogstatsd.rs index 45f67b2..52d4563 100644 --- a/crates/dogstatsd/src/dogstatsd.rs +++ b/crates/dogstatsd/src/dogstatsd.rs @@ -115,7 +115,7 @@ impl BufferReader { let data = read_from_named_pipe(pipe_name, cancel_token) .await .expect("didn't receive data"); - Ok((data, MessageSource::NamedPipe(Arc::clone(pipe_name)))) + Ok((data, MessageSource::NamedPipe(pipe_name.clone()))) } } } @@ -131,6 +131,10 @@ async fn handle_pipe_error_with_backoff( cancel_token: &tokio_util::sync::CancellationToken, ) -> std::io::Result { *consecutive_errors += 1; + debug!( + "Named pipe error for {} (attempt {}): {}.", + error_type, consecutive_errors, error + ); if *consecutive_errors >= MAX_NAMED_PIPE_ERRORS { return Err(std::io::Error::other(format!( @@ -166,7 +170,6 @@ async fn read_from_named_pipe( while !cancel_token.is_cancelled() { // Create pipe if needed (initial startup or after error) if current_pipe.is_none() { - debug!("Creating named pipe: {}", pipe_name); match ServerOptions::new().create(pipe_name) { Ok(new_pipe) => { consecutive_errors = 0; // Reset on successful pipe creation @@ -174,8 +177,7 @@ async fn read_from_named_pipe( needs_connection = true; } Err(e) => { - error!("Failed to create named pipe: {}", e); - if !handle_pipe_error_with_backoff( + match handle_pipe_error_with_backoff( &mut consecutive_errors, "pipe creation", &e, @@ -183,9 +185,9 @@ async fn read_from_named_pipe( ) .await? { - break; + true => continue, + false => break, } - continue; } } } @@ -195,7 +197,6 @@ async fn read_from_named_pipe( #[allow(clippy::expect_used)] let pipe = current_pipe.as_ref().expect("pipe must exist"); - debug!("Waiting for client connection on named pipe"); let connect_result = tokio::select! { result = pipe.connect() => result, _ = cancel_token.cancelled() => { @@ -205,14 +206,16 @@ async fn read_from_named_pipe( match connect_result { Ok(()) => { - debug!("Client connected to DogStatsD named pipe"); consecutive_errors = 0; needs_connection = false; } Err(e) => { - // Connection failed - recreate pipe on next iteration + // Connection failed - disconnect and recreate pipe on next iteration + if let Some(pipe) = current_pipe.as_ref() { + let _ = pipe.disconnect(); // Ignore disconnect errors, we're recreating anyway + } current_pipe = None; - if !handle_pipe_error_with_backoff( + match handle_pipe_error_with_backoff( &mut consecutive_errors, "connection", &e, @@ -220,15 +223,14 @@ async fn read_from_named_pipe( ) .await? { - break; + true => continue, + false => break, } - continue; } } } // Read from the connected pipe - debug!("Starting read from named pipe"); let mut buf = [0; BUFFER_SIZE]; #[allow(clippy::expect_used)] @@ -246,25 +248,27 @@ async fn read_from_named_pipe( match read_result { Ok(0) => { // Client disconnected - reuse pipe by disconnecting and waiting for new client - debug!("Client disconnected, preparing for reconnection"); - if let Err(e) = pipe.disconnect() { - error!("Failed to disconnect pipe: {}", e); + if let Err(_e) = pipe.disconnect() { current_pipe = None; // Recreate on error } else { needs_connection = true; // Wait for new client on same pipe } + debug!( + "Client disconnected from named pipe. Needs connection: {}", + needs_connection + ); continue; } Ok(amt) => { // this is the success state: we read some data from the pipe - debug!("Read {} bytes from named pipe", amt); return Ok(buf[..amt].to_vec()); } Err(e) => { - error!("Error reading from named pipe: {}", e); - current_pipe = None; // Recreate pipe on read error + // Disconnect before recreating pipe on read error + let _ = pipe.disconnect(); // Ignore disconnect errors, we're recreating anyway + current_pipe = None; - if !handle_pipe_error_with_backoff( + match handle_pipe_error_with_backoff( &mut consecutive_errors, "read", &e, @@ -272,9 +276,9 @@ async fn read_from_named_pipe( ) .await? { - break; + true => continue, + false => break, } - continue; } } } @@ -360,7 +364,12 @@ impl DogStatsD { self.insert_metrics(statsd_metric_strings); } - /// Parses metrics from raw DogStatsD format and forwards them to the aggregator. + fn prepend_namespace(namespace: &str, metric: &mut Metric) { + let new_name = format!("{}.{}", namespace, metric.name); + metric.name = ustr::Ustr::from(&new_name); + metric.id = id(metric.name, &metric.tags, metric.timestamp); + } + fn insert_metrics(&self, msg: Split) { let namespace = self.metric_namespace.as_deref(); let all_valid_metrics: Vec = msg @@ -395,26 +404,12 @@ impl DogStatsD { }) .collect(); if !all_valid_metrics.is_empty() { - debug!( - "Inserting {} metrics into aggregator", - all_valid_metrics.len() - ); - for metric in &all_valid_metrics { - debug!(" - {}: {:?}", metric.name, metric.value); - } // Send metrics through the channel - no lock needed! if let Err(e) = self.aggregator_handle.insert_batch(all_valid_metrics) { error!("Failed to send metrics to aggregator: {}", e); } } } - - /// Prepends a namespace to a metric's name and updates its ID. - fn prepend_namespace(namespace: &str, metric: &mut Metric) { - let new_name = format!("{}.{}", namespace, metric.name); - metric.name = ustr::Ustr::from(&new_name); - metric.id = id(metric.name, &metric.tags, metric.timestamp); - } } #[cfg(test)] diff --git a/crates/dogstatsd/tests/integration_test.rs b/crates/dogstatsd/tests/integration_test.rs index d84f0b0..d805f2f 100644 --- a/crates/dogstatsd/tests/integration_test.rs +++ b/crates/dogstatsd/tests/integration_test.rs @@ -292,8 +292,6 @@ async fn test_send_with_retry_immediate_failure_after_one_attempt() { #[cfg(windows)] #[tokio::test] async fn test_named_pipe_basic_communication() { - let _ = env_logger::builder().is_test(true).try_init(); - let pipe_name = r"\\.\pipe\test_dogstatsd_basic"; let (service, handle) = AggregatorService::new(SortedTags::parse("test:value").unwrap(), 1_024) .expect("aggregator service creation failed"); @@ -348,8 +346,6 @@ async fn test_named_pipe_basic_communication() { #[cfg(windows)] #[tokio::test] async fn test_named_pipe_disconnect_reconnect() { - let _ = env_logger::builder().is_test(true).try_init(); - let pipe_name = r"\\.\pipe\test_dogstatsd_reconnect"; let (service, handle) = AggregatorService::new(SortedTags::parse("test:value").unwrap(), 1_024) .expect("aggregator service creation failed"); @@ -370,7 +366,7 @@ async fn test_named_pipe_disconnect_reconnect() { windows_pipe_name: Some(pipe_name.to_string()), }, handle, - cancel_token_clone.child_token(), + cancel_token_clone, ) .await; dogstatsd.spin().await; @@ -419,8 +415,6 @@ async fn test_named_pipe_disconnect_reconnect() { #[cfg(windows)] #[tokio::test] async fn test_named_pipe_cancellation() { - let _ = env_logger::builder().is_test(true).try_init(); - let pipe_name = r"\\.\pipe\test_dogstatsd_cancel"; let (service, handle) = AggregatorService::new(SortedTags::parse("test:value").unwrap(), 1_024) .expect("aggregator service creation failed"); @@ -441,7 +435,7 @@ async fn test_named_pipe_cancellation() { windows_pipe_name: Some(pipe_name.to_string()), }, handle, - cancel_token_clone.child_token(), + cancel_token_clone, ) .await; dogstatsd.spin().await; @@ -459,3 +453,65 @@ async fn test_named_pipe_cancellation() { handle.shutdown().expect("shutdown failed"); } + +#[cfg(test)] +#[cfg(windows)] +#[tokio::test] +async fn test_named_pipe_max_errors() { + use tokio::net::windows::named_pipe::ServerOptions; + + let pipe_name = r"\\.\pipe\test_dogstatsd_max_errors"; + let (service, handle) = AggregatorService::new(SortedTags::parse("test:value").unwrap(), 1_024) + .expect("aggregator service creation failed"); + tokio::spawn(service.run()); + + let cancel_token = CancellationToken::new(); + + // Create a pipe with first_pipe_instance(true) to block other instances + let _blocking_pipe = ServerOptions::new() + .first_pipe_instance(true) + .create(pipe_name) + .expect("failed to create blocking pipe"); + + // Start DogStatsD server - should fail to create pipe due to existing instance + let dogstatsd_task = { + let handle = handle.clone(); + let cancel_token_clone = cancel_token.clone(); + tokio::spawn(async move { + let dogstatsd = DogStatsD::new( + &DogStatsDConfig { + host: String::new(), + port: 0, + metric_namespace: None, + windows_pipe_name: Some(pipe_name.to_string()), + }, + handle, + cancel_token_clone, + ) + .await; + dogstatsd.spin().await; + }) + }; + + // Task should complete after exceeding MAX_NAMED_PIPE_ERRORS (5) attempts + // With backoff: 20ms + 40ms + 80ms + 160ms + 320ms = ~620ms + overhead + let result = timeout(Duration::from_millis(1500), dogstatsd_task).await; + + // The task should panic because read_from_named_pipe returns an error after max retries + assert!( + result.is_ok(), + "task should complete (panic) after exceeding max errors" + ); + + // Verify the task actually panicked (completed with error) + if let Ok(join_result) = result { + assert!( + join_result.is_err(), + "task should have panicked after exceeding max consecutive errors" + ); + } + + // Cleanup + cancel_token.cancel(); + handle.shutdown().expect("shutdown failed"); +} From 80560ea4052923366f26aa9f918e1756d55cab5a Mon Sep 17 00:00:00 2001 From: Lewis Date: Tue, 30 Dec 2025 10:54:56 -0500 Subject: [PATCH 11/21] Add tests for named pipe error handling --- crates/dogstatsd/src/dogstatsd.rs | 72 +++++++++++++++++++++- crates/dogstatsd/tests/integration_test.rs | 62 ------------------- 2 files changed, 71 insertions(+), 63 deletions(-) diff --git a/crates/dogstatsd/src/dogstatsd.rs b/crates/dogstatsd/src/dogstatsd.rs index 52d4563..1d9fe22 100644 --- a/crates/dogstatsd/src/dogstatsd.rs +++ b/crates/dogstatsd/src/dogstatsd.rs @@ -147,7 +147,7 @@ async fn handle_pipe_error_with_backoff( // Sleep with cancellation support for clean, fast shutdown tokio::select! { - _ = sleep(Duration::from_millis(backoff_ms)) => Ok(true), + _ = tokio::time::sleep(tokio::time::Duration::from_millis(backoff_ms)) => Ok(true), _ = cancel_token.cancelled() => Ok(false), } } @@ -421,6 +421,12 @@ mod tests { use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use tracing_test::traced_test; + #[cfg(windows)] + use { + super::handle_pipe_error_with_backoff, std::time::Duration, + tokio_util::sync::CancellationToken, + }; + #[tokio::test] async fn test_dogstatsd_multi_distribution() { let response = setup_and_consume_dogstatsd( @@ -536,4 +542,68 @@ single_machine_performance.rouster.metrics_max_timestamp_latency:1376.90870216|d response } + + #[cfg(windows)] + #[tokio::test] + async fn test_handle_pipe_error_with_backoff_max_errors() { + let cancel_token = CancellationToken::new(); + let mut consecutive_errors = 0; + let error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "test error"); + + // First 4 errors should return Ok(true) to continue + for i in 1..=4 { + let result = handle_pipe_error_with_backoff( + &mut consecutive_errors, + "test", + &error, + &cancel_token, + ) + .await; + assert!(result.is_ok(), "Error {} should succeed", i); + assert_eq!(result.unwrap(), true, "Error {} should return true", i); + assert_eq!(consecutive_errors, i, "Error count should be {}", i); + } + + // 5th error should return Err because MAX_NAMED_PIPE_ERRORS is 5 + let result = + handle_pipe_error_with_backoff(&mut consecutive_errors, "test", &error, &cancel_token) + .await; + assert!( + result.is_err(), + "5th error should fail with max errors exceeded" + ); + assert_eq!(consecutive_errors, 5, "Error count should be 5"); + + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("Too many consecutive"), + "Error message should mention too many errors: {}", + err_msg + ); + } + + #[cfg(windows)] + #[tokio::test] + async fn test_handle_pipe_error_with_backoff_cancellation() { + let cancel_token = CancellationToken::new(); + let mut consecutive_errors = 0; + let error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "test error"); + + // Spawn task to cancel after 10ms + let cancel_clone = cancel_token.clone(); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_millis(10)).await; + cancel_clone.cancel(); + }); + + // First error increments count and sleeps for 20ms (10 * 2^1) + let result = + handle_pipe_error_with_backoff(&mut consecutive_errors, "test", &error, &cancel_token) + .await; + + // Should return Ok(false) because token was cancelled during backoff sleep + assert!(result.is_ok(), "Should succeed even when cancelled"); + assert_eq!(result.unwrap(), false, "Should return false when cancelled"); + assert_eq!(consecutive_errors, 1, "Error count should be 1"); + } } diff --git a/crates/dogstatsd/tests/integration_test.rs b/crates/dogstatsd/tests/integration_test.rs index d805f2f..8a81cfe 100644 --- a/crates/dogstatsd/tests/integration_test.rs +++ b/crates/dogstatsd/tests/integration_test.rs @@ -453,65 +453,3 @@ async fn test_named_pipe_cancellation() { handle.shutdown().expect("shutdown failed"); } - -#[cfg(test)] -#[cfg(windows)] -#[tokio::test] -async fn test_named_pipe_max_errors() { - use tokio::net::windows::named_pipe::ServerOptions; - - let pipe_name = r"\\.\pipe\test_dogstatsd_max_errors"; - let (service, handle) = AggregatorService::new(SortedTags::parse("test:value").unwrap(), 1_024) - .expect("aggregator service creation failed"); - tokio::spawn(service.run()); - - let cancel_token = CancellationToken::new(); - - // Create a pipe with first_pipe_instance(true) to block other instances - let _blocking_pipe = ServerOptions::new() - .first_pipe_instance(true) - .create(pipe_name) - .expect("failed to create blocking pipe"); - - // Start DogStatsD server - should fail to create pipe due to existing instance - let dogstatsd_task = { - let handle = handle.clone(); - let cancel_token_clone = cancel_token.clone(); - tokio::spawn(async move { - let dogstatsd = DogStatsD::new( - &DogStatsDConfig { - host: String::new(), - port: 0, - metric_namespace: None, - windows_pipe_name: Some(pipe_name.to_string()), - }, - handle, - cancel_token_clone, - ) - .await; - dogstatsd.spin().await; - }) - }; - - // Task should complete after exceeding MAX_NAMED_PIPE_ERRORS (5) attempts - // With backoff: 20ms + 40ms + 80ms + 160ms + 320ms = ~620ms + overhead - let result = timeout(Duration::from_millis(1500), dogstatsd_task).await; - - // The task should panic because read_from_named_pipe returns an error after max retries - assert!( - result.is_ok(), - "task should complete (panic) after exceeding max errors" - ); - - // Verify the task actually panicked (completed with error) - if let Ok(join_result) = result { - assert!( - join_result.is_err(), - "task should have panicked after exceeding max consecutive errors" - ); - } - - // Cleanup - cancel_token.cancel(); - handle.shutdown().expect("shutdown failed"); -} From 3c6221faa2183277b734015ae417eea58e71fae3 Mon Sep 17 00:00:00 2001 From: Lewis Date: Tue, 30 Dec 2025 11:25:06 -0500 Subject: [PATCH 12/21] Handle pipe names consistently with dogstatsd client --- crates/datadog-serverless-compat/src/main.rs | 4 + crates/dogstatsd/src/dogstatsd.rs | 91 ++++++++++++++++++-- 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/crates/datadog-serverless-compat/src/main.rs b/crates/datadog-serverless-compat/src/main.rs index 93cd6d3..1b52419 100644 --- a/crates/datadog-serverless-compat/src/main.rs +++ b/crates/datadog-serverless-compat/src/main.rs @@ -65,6 +65,10 @@ pub async fn main() { }; let dd_api_key: Option = env::var("DD_API_KEY").ok(); + + // Windows named pipe name for DogStatsD. + // Can be either a simple name (e.g., "dd_dogstatsd") or a full path (e.g., "\\.\pipe\dd_dogstatsd"). + // The DogStatsD server will normalize a simple name to the full path format. let dd_dogstatsd_windows_pipe_name: Option = { #[cfg(windows)] { diff --git a/crates/dogstatsd/src/dogstatsd.rs b/crates/dogstatsd/src/dogstatsd.rs index 1d9fe22..a4533b0 100644 --- a/crates/dogstatsd/src/dogstatsd.rs +++ b/crates/dogstatsd/src/dogstatsd.rs @@ -17,12 +17,7 @@ use tracing::{debug, error, trace}; // Windows-specific imports #[cfg(windows)] -use { - std::sync::Arc, - tokio::io::AsyncReadExt, - tokio::net::windows::named_pipe::ServerOptions, - tokio::time::{sleep, Duration}, -}; +use {std::sync::Arc, tokio::io::AsyncReadExt, tokio::net::windows::named_pipe::ServerOptions}; // DogStatsD buffer size for receiving metrics // TODO(astuyve) buf should be dynamic @@ -36,6 +31,10 @@ const BUFFER_SIZE: usize = 8192; #[cfg(windows)] const MAX_NAMED_PIPE_ERRORS: u32 = 5; +// Windows named pipe prefix. All named pipes on Windows must start with this prefix. +#[cfg(windows)] +const NAMED_PIPE_PREFIX: &str = "\\\\.\\pipe\\"; + /// Configuration for the DogStatsD server pub struct DogStatsDConfig { /// Host to bind UDP socket to (e.g., "127.0.0.1") @@ -44,7 +43,8 @@ pub struct DogStatsDConfig { pub port: u16, /// Optional namespace to prepend to all metric names (e.g., "myapp") pub metric_namespace: Option, - /// Optional Windows named pipe path (e.g., "\\\\.\\pipe\\my_pipe") + /// Optional Windows named pipe name. Can be either a simple name (e.g., "my_pipe") + /// or a full path (e.g., "\\\\.\\pipe\\my_pipe"). The prefix will be added automatically if missing. pub windows_pipe_name: Option, } @@ -152,6 +152,18 @@ async fn handle_pipe_error_with_backoff( } } +/// Normalizes a Windows named pipe name by adding the required prefix if missing. +/// +/// Windows named pipes must have the format `\\.\pipe\{name}`. +#[cfg(windows)] +fn normalize_pipe_name(pipe_name: &str) -> String { + if pipe_name.starts_with(NAMED_PIPE_PREFIX) { + pipe_name.to_string() + } else { + format!("{}{}", NAMED_PIPE_PREFIX, pipe_name) + } +} + /// Reads data from a Windows named pipe with retry logic. /// /// Windows named pipes can experience transient failures (client disconnect, pipe errors). @@ -310,8 +322,10 @@ impl DogStatsD { let buffer_reader = if let Some(ref pipe_name) = config.windows_pipe_name { #[cfg(windows)] { + // Normalize pipe name to ensure it has the \\.\pipe\ prefix + let normalized_pipe_name = normalize_pipe_name(pipe_name); BufferReader::NamedPipe { - pipe_name: Arc::new(pipe_name.clone()), + pipe_name: Arc::new(normalized_pipe_name), cancel_token: cancel_token.clone(), } } @@ -546,6 +560,7 @@ single_machine_performance.rouster.metrics_max_timestamp_latency:1376.90870216|d #[cfg(windows)] #[tokio::test] async fn test_handle_pipe_error_with_backoff_max_errors() { + use super::handle_pipe_error_with_backoff; let cancel_token = CancellationToken::new(); let mut consecutive_errors = 0; let error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "test error"); @@ -585,6 +600,7 @@ single_machine_performance.rouster.metrics_max_timestamp_latency:1376.90870216|d #[cfg(windows)] #[tokio::test] async fn test_handle_pipe_error_with_backoff_cancellation() { + use super::handle_pipe_error_with_backoff; let cancel_token = CancellationToken::new(); let mut consecutive_errors = 0; let error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "test error"); @@ -592,7 +608,7 @@ single_machine_performance.rouster.metrics_max_timestamp_latency:1376.90870216|d // Spawn task to cancel after 10ms let cancel_clone = cancel_token.clone(); tokio::spawn(async move { - tokio::time::sleep(Duration::from_millis(10)).await; + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; cancel_clone.cancel(); }); @@ -606,4 +622,61 @@ single_machine_performance.rouster.metrics_max_timestamp_latency:1376.90870216|d assert_eq!(result.unwrap(), false, "Should return false when cancelled"); assert_eq!(consecutive_errors, 1, "Error count should be 1"); } + + #[cfg(windows)] + #[test] + fn test_normalize_pipe_name() { + use super::normalize_pipe_name; + + // Test cases: (input, expected_output, description) + let test_cases = vec![ + // Names without prefix should get it added + ( + "my_pipe", + "\\\\.\\pipe\\my_pipe", + "simple name without prefix", + ), + ( + "dd_dogstatsd", + "\\\\.\\pipe\\dd_dogstatsd", + "dd_dogstatsd without prefix", + ), + ( + "datadog_statsd", + "\\\\.\\pipe\\datadog_statsd", + "datadog_statsd without prefix", + ), + ( + "test-pipe_123", + "\\\\.\\pipe\\test-pipe_123", + "name with hyphens and underscores", + ), + ("", "\\\\.\\pipe\\", "empty string"), + // Names with prefix should remain unchanged + ( + "\\\\.\\pipe\\my_pipe", + "\\\\.\\pipe\\my_pipe", + "already has prefix", + ), + ( + "\\\\.\\pipe\\dd_dogstatsd", + "\\\\.\\pipe\\dd_dogstatsd", + "dd_dogstatsd with prefix", + ), + ( + "\\\\.\\pipe\\test", + "\\\\.\\pipe\\test", + "short name with prefix", + ), + ]; + + for (input, expected, description) in test_cases { + assert_eq!( + normalize_pipe_name(input), + expected, + "Failed for test case: {}", + description + ); + } + } } From b06f1f1ee757aa02165a6f0751e8981531d3514a Mon Sep 17 00:00:00 2001 From: Lewis Date: Tue, 30 Dec 2025 14:39:52 -0500 Subject: [PATCH 13/21] Add pipe sharing check --- crates/dogstatsd/src/dogstatsd.rs | 40 +++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/crates/dogstatsd/src/dogstatsd.rs b/crates/dogstatsd/src/dogstatsd.rs index a4533b0..d78403b 100644 --- a/crates/dogstatsd/src/dogstatsd.rs +++ b/crates/dogstatsd/src/dogstatsd.rs @@ -17,7 +17,11 @@ use tracing::{debug, error, trace}; // Windows-specific imports #[cfg(windows)] -use {std::sync::Arc, tokio::io::AsyncReadExt, tokio::net::windows::named_pipe::ServerOptions}; +use { + std::sync::Arc, + tokio::io::AsyncReadExt, + tokio::net::windows::named_pipe::{ClientOptions, ServerOptions}, +}; // DogStatsD buffer size for receiving metrics // TODO(astuyve) buf should be dynamic @@ -164,6 +168,31 @@ fn normalize_pipe_name(pipe_name: &str) -> String { } } +/// Checks if a named pipe already exists by attempting to open it as a client. +#[cfg(windows)] +async fn pipe_exists(pipe_name: &str) -> std::io::Result { + match ClientOptions::new().open(pipe_name) { + Ok(_client) => Ok(true), + Err(e) => match e.raw_os_error() { + Some(2) => { + // ERROR_FILE_NOT_FOUND - pipe doesn't exist + debug!("Named pipe '{}' does not exist (error 2)", pipe_name); + Ok(false) + } + _ => { + // Other errors (access denied, invalid handle, etc.) + debug!( + "Unable to check if pipe '{}' exists: {} (error code: {:?})", + pipe_name, + e, + e.raw_os_error() + ); + Err(e) + } + }, + } +} + /// Reads data from a Windows named pipe with retry logic. /// /// Windows named pipes can experience transient failures (client disconnect, pipe errors). @@ -180,8 +209,15 @@ async fn read_from_named_pipe( // Let named pipes cancel cleanly when the server is shut down while !cancel_token.is_cancelled() { - // Create pipe if needed (initial startup or after error) + // Create server if needed (initial startup or after error) if current_pipe.is_none() { + // First, check if pipe already exists (e.g. multiple dogstatsd instances) + if let Ok(true) = pipe_exists(pipe_name).await { + debug!("DogStatsD server with named pipe '{}' exists, pipe can be shared.", pipe_name); + return Ok(()); + } + + // Attempt to create the pipe server match ServerOptions::new().create(pipe_name) { Ok(new_pipe) => { consecutive_errors = 0; // Reset on successful pipe creation From 357659bf6bc0c3cb08415df29ce35ffd40fc3dd6 Mon Sep 17 00:00:00 2001 From: Lewis Date: Tue, 30 Dec 2025 15:11:14 -0500 Subject: [PATCH 14/21] Try different named pipe server / read separation --- crates/datadog-serverless-compat/src/main.rs | 14 +- crates/dogstatsd/src/dogstatsd.rs | 467 +++++-------------- crates/dogstatsd/tests/integration_test.rs | 5 +- 3 files changed, 118 insertions(+), 368 deletions(-) diff --git a/crates/datadog-serverless-compat/src/main.rs b/crates/datadog-serverless-compat/src/main.rs index 1b52419..5d7a6db 100644 --- a/crates/datadog-serverless-compat/src/main.rs +++ b/crates/datadog-serverless-compat/src/main.rs @@ -67,12 +67,20 @@ pub async fn main() { let dd_api_key: Option = env::var("DD_API_KEY").ok(); // Windows named pipe name for DogStatsD. - // Can be either a simple name (e.g., "dd_dogstatsd") or a full path (e.g., "\\.\pipe\dd_dogstatsd"). - // The DogStatsD server will normalize a simple name to the full path format. + // Normalize by adding \\.\pipe\ prefix if not present let dd_dogstatsd_windows_pipe_name: Option = { #[cfg(windows)] { - env::var("DD_DOGSTATSD_WINDOWS_PIPE_NAME").ok() + env::var("DD_DOGSTATSD_WINDOWS_PIPE_NAME") + .ok() + .map(|pipe_name| { + if pipe_name.starts_with("\\\\.\\pipe\\") || pipe_name.starts_with(r"\\.\pipe\") + { + pipe_name + } else { + format!(r"\\.\pipe\{}", pipe_name) + } + }) } #[cfg(not(windows))] { diff --git a/crates/dogstatsd/src/dogstatsd.rs b/crates/dogstatsd/src/dogstatsd.rs index d78403b..431b5ae 100644 --- a/crates/dogstatsd/src/dogstatsd.rs +++ b/crates/dogstatsd/src/dogstatsd.rs @@ -29,16 +29,6 @@ use { // https://github.com/DataDog/datadog-agent/blob/85939a62b5580b2a15549f6936f257e61c5aa153/pkg/config/config_template.yaml#L2154-L2158 const BUFFER_SIZE: usize = 8192; -// Maximum number of consecutive errors before giving up on named pipe operations. -// Backoff formula: 10ms * 2^error_count, capped at MAX to prevent overflow -// With MAX = 5: backoffs are 20ms, 40ms, 80ms, 160ms, 320ms (total: ~600ms before giving up) -#[cfg(windows)] -const MAX_NAMED_PIPE_ERRORS: u32 = 5; - -// Windows named pipe prefix. All named pipes on Windows must start with this prefix. -#[cfg(windows)] -const NAMED_PIPE_PREFIX: &str = "\\\\.\\pipe\\"; - /// Configuration for the DogStatsD server pub struct DogStatsDConfig { /// Host to bind UDP socket to (e.g., "127.0.0.1") @@ -47,8 +37,7 @@ pub struct DogStatsDConfig { pub port: u16, /// Optional namespace to prepend to all metric names (e.g., "myapp") pub metric_namespace: Option, - /// Optional Windows named pipe name. Can be either a simple name (e.g., "my_pipe") - /// or a full path (e.g., "\\\\.\\pipe\\my_pipe"). The prefix will be added automatically if missing. + /// Optional Windows named pipe name. (e.g., "\\\\.\\pipe\\my_pipe"). pub windows_pipe_name: Option, } @@ -85,7 +74,7 @@ enum BufferReader { #[cfg(windows)] NamedPipe { pipe_name: Arc, - cancel_token: tokio_util::sync::CancellationToken, + receiver: Arc>>>, }, } @@ -112,227 +101,19 @@ impl BufferReader { #[cfg(windows)] BufferReader::NamedPipe { pipe_name, - cancel_token, + receiver, } => { - // Named Pipe Reader: retries with backoff due to expected transient errors - #[allow(clippy::expect_used)] - let data = read_from_named_pipe(pipe_name, cancel_token) - .await - .expect("didn't receive data"); - Ok((data, MessageSource::NamedPipe(pipe_name.clone()))) - } - } - } -} - -/// Helper to handle retry logic with exponential backoff for named pipe errors. -/// Returns true if should continue retrying, false if should break (cancelled). -#[cfg(windows)] -async fn handle_pipe_error_with_backoff( - consecutive_errors: &mut u32, - error_type: &str, - error: &std::io::Error, - cancel_token: &tokio_util::sync::CancellationToken, -) -> std::io::Result { - *consecutive_errors += 1; - debug!( - "Named pipe error for {} (attempt {}): {}.", - error_type, consecutive_errors, error - ); - - if *consecutive_errors >= MAX_NAMED_PIPE_ERRORS { - return Err(std::io::Error::other(format!( - "Too many consecutive {} errors: {}", - error_type, error - ))); - } - - let backoff_ms = 10u64 * (1 << *consecutive_errors); - - // Sleep with cancellation support for clean, fast shutdown - tokio::select! { - _ = tokio::time::sleep(tokio::time::Duration::from_millis(backoff_ms)) => Ok(true), - _ = cancel_token.cancelled() => Ok(false), - } -} - -/// Normalizes a Windows named pipe name by adding the required prefix if missing. -/// -/// Windows named pipes must have the format `\\.\pipe\{name}`. -#[cfg(windows)] -fn normalize_pipe_name(pipe_name: &str) -> String { - if pipe_name.starts_with(NAMED_PIPE_PREFIX) { - pipe_name.to_string() - } else { - format!("{}{}", NAMED_PIPE_PREFIX, pipe_name) - } -} - -/// Checks if a named pipe already exists by attempting to open it as a client. -#[cfg(windows)] -async fn pipe_exists(pipe_name: &str) -> std::io::Result { - match ClientOptions::new().open(pipe_name) { - Ok(_client) => Ok(true), - Err(e) => match e.raw_os_error() { - Some(2) => { - // ERROR_FILE_NOT_FOUND - pipe doesn't exist - debug!("Named pipe '{}' does not exist (error 2)", pipe_name); - Ok(false) - } - _ => { - // Other errors (access denied, invalid handle, etc.) - debug!( - "Unable to check if pipe '{}' exists: {} (error code: {:?})", - pipe_name, - e, - e.raw_os_error() - ); - Err(e) - } - }, - } -} - -/// Reads data from a Windows named pipe with retry logic. -/// -/// Windows named pipes can experience transient failures (client disconnect, pipe errors). -/// This function uses a retry loop with exponential backoff to handle these failures -/// and has additional logic to allow clean shutdown via cancel_token. -#[cfg(windows)] -async fn read_from_named_pipe( - pipe_name: &str, - cancel_token: &tokio_util::sync::CancellationToken, -) -> std::io::Result> { - let mut consecutive_errors = 0; - let mut current_pipe: Option = None; - let mut needs_connection = true; // Track whether we need to wait for a client - - // Let named pipes cancel cleanly when the server is shut down - while !cancel_token.is_cancelled() { - // Create server if needed (initial startup or after error) - if current_pipe.is_none() { - // First, check if pipe already exists (e.g. multiple dogstatsd instances) - if let Ok(true) = pipe_exists(pipe_name).await { - debug!("DogStatsD server with named pipe '{}' exists, pipe can be shared.", pipe_name); - return Ok(()); - } - - // Attempt to create the pipe server - match ServerOptions::new().create(pipe_name) { - Ok(new_pipe) => { - consecutive_errors = 0; // Reset on successful pipe creation - current_pipe = Some(new_pipe); - needs_connection = true; - } - Err(e) => { - match handle_pipe_error_with_backoff( - &mut consecutive_errors, - "pipe creation", - &e, - cancel_token, - ) - .await? - { - true => continue, - false => break, - } - } - } - } - - // Wait for client connection if needed (after creation or after disconnect) - if needs_connection { - #[allow(clippy::expect_used)] - let pipe = current_pipe.as_ref().expect("pipe must exist"); - - let connect_result = tokio::select! { - result = pipe.connect() => result, - _ = cancel_token.cancelled() => { - break; - } - }; - - match connect_result { - Ok(()) => { - consecutive_errors = 0; - needs_connection = false; - } - Err(e) => { - // Connection failed - disconnect and recreate pipe on next iteration - if let Some(pipe) = current_pipe.as_ref() { - let _ = pipe.disconnect(); // Ignore disconnect errors, we're recreating anyway - } - current_pipe = None; - match handle_pipe_error_with_backoff( - &mut consecutive_errors, - "connection", - &e, - cancel_token, - ) - .await? - { - true => continue, - false => break, + // Named Pipe Reader: receives data from client handler tasks + match receiver.lock().await.recv().await { + Some(data) => Ok((data, MessageSource::NamedPipe(pipe_name.clone()))), + None => { + // Channel closed - server exited, already triggered cancellation + Ok((Vec::new(), MessageSource::NamedPipe(pipe_name.clone()))) } } } } - - // Read from the connected pipe - let mut buf = [0; BUFFER_SIZE]; - - #[allow(clippy::expect_used)] - let pipe = current_pipe - .as_mut() - .expect("pipe must exist and be connected"); - - let read_result = tokio::select! { - result = pipe.read(&mut buf) => result, - _ = cancel_token.cancelled() => { - return Ok(Vec::new()); - } - }; - - match read_result { - Ok(0) => { - // Client disconnected - reuse pipe by disconnecting and waiting for new client - if let Err(_e) = pipe.disconnect() { - current_pipe = None; // Recreate on error - } else { - needs_connection = true; // Wait for new client on same pipe - } - debug!( - "Client disconnected from named pipe. Needs connection: {}", - needs_connection - ); - continue; - } - Ok(amt) => { - // this is the success state: we read some data from the pipe - return Ok(buf[..amt].to_vec()); - } - Err(e) => { - // Disconnect before recreating pipe on read error - let _ = pipe.disconnect(); // Ignore disconnect errors, we're recreating anyway - current_pipe = None; - - match handle_pipe_error_with_backoff( - &mut consecutive_errors, - "read", - &e, - cancel_token, - ) - .await? - { - true => continue, - false => break, - } - } - } } - - // If we exit due to cancellation, return an empty result for a clean shutdown. - Ok(Vec::new()) } /// DogStatsD server to receive, parse, and forward metrics. @@ -358,11 +139,22 @@ impl DogStatsD { let buffer_reader = if let Some(ref pipe_name) = config.windows_pipe_name { #[cfg(windows)] { - // Normalize pipe name to ensure it has the \\.\pipe\ prefix - let normalized_pipe_name = normalize_pipe_name(pipe_name); + let pipe_name = Arc::new(pipe_name.clone()); + + // Create channel for receiving data from client handlers + let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); + let receiver = Arc::new(tokio::sync::Mutex::new(receiver)); + + // Spawn server accept loop + let server_pipe_name = pipe_name.clone(); + let server_cancel = cancel_token.clone(); + tokio::spawn(async move { + run_named_pipe_server(server_pipe_name, sender, server_cancel).await; + }); + BufferReader::NamedPipe { - pipe_name: Arc::new(normalized_pipe_name), - cancel_token: cancel_token.clone(), + pipe_name, + receiver, } } #[cfg(not(windows))] @@ -462,6 +254,88 @@ impl DogStatsD { } } +/// Named Pipe server - accepts client connections and forwards metrics. +/// +/// Uses a multi-instance approach (like winio in the main agent): +/// - Creates new server instance for each client +/// - Spawns task to handle each client +#[cfg(windows)] +async fn run_named_pipe_server( + pipe_name: Arc, + sender: tokio::sync::mpsc::UnboundedSender>, + cancel_token: tokio_util::sync::CancellationToken, +) { + loop { + if cancel_token.is_cancelled() { + break; + } + + // Create new server instance + let server = match ServerOptions::new().create(&*pipe_name) { + Ok(s) => { + debug!("Created pipe server instance '{}'", pipe_name); + s + } + Err(e) => { + error!("Failed to create pipe '{}': {}", pipe_name, e); + tokio::select! { + _ = tokio::time::sleep(tokio::time::Duration::from_secs(1)) => continue, + _ = cancel_token.cancelled() => break, + } + } + }; + + // Wait for client connection (cancellable) + let connect_result = tokio::select! { + result = server.connect() => result, + _ = cancel_token.cancelled() => break, + }; + + if let Err(e) = connect_result { + error!("Connection failed on '{}': {}", pipe_name, e); + continue; + } + + debug!("Client connected to '{}'", pipe_name); + + // Spawn task to handle this client + let sender_clone = sender.clone(); + let pipe_name_clone = pipe_name.clone(); + let cancel_clone = cancel_token.clone(); + tokio::spawn(async move { + let mut buf = [0u8; BUFFER_SIZE]; + let mut server = server; + + loop { + // Read with cancellation support + let read_result = tokio::select! { + result = server.read(&mut buf) => result, + _ = cancel_clone.cancelled() => break, + }; + + let n = match read_result { + Ok(0) => { + debug!("Client disconnected from '{}'", pipe_name_clone); + break; // Client disconnected + } + Ok(n) => n, + Err(e) => { + error!("Read error on '{}': {}", pipe_name_clone, e); + break; + } + }; + + // Send data to the main read function / processing loop + if sender_clone.send(buf[..n].to_vec()).is_err() { + error!("Failed to send data from '{}'", pipe_name_clone); + break; + } + } + // Server instance is dropped here, automatically cleaned up + }); + } +} + #[cfg(test)] #[allow(clippy::unwrap_used)] mod tests { @@ -471,12 +345,6 @@ mod tests { use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use tracing_test::traced_test; - #[cfg(windows)] - use { - super::handle_pipe_error_with_backoff, std::time::Duration, - tokio_util::sync::CancellationToken, - }; - #[tokio::test] async fn test_dogstatsd_multi_distribution() { let response = setup_and_consume_dogstatsd( @@ -592,127 +460,4 @@ single_machine_performance.rouster.metrics_max_timestamp_latency:1376.90870216|d response } - - #[cfg(windows)] - #[tokio::test] - async fn test_handle_pipe_error_with_backoff_max_errors() { - use super::handle_pipe_error_with_backoff; - let cancel_token = CancellationToken::new(); - let mut consecutive_errors = 0; - let error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "test error"); - - // First 4 errors should return Ok(true) to continue - for i in 1..=4 { - let result = handle_pipe_error_with_backoff( - &mut consecutive_errors, - "test", - &error, - &cancel_token, - ) - .await; - assert!(result.is_ok(), "Error {} should succeed", i); - assert_eq!(result.unwrap(), true, "Error {} should return true", i); - assert_eq!(consecutive_errors, i, "Error count should be {}", i); - } - - // 5th error should return Err because MAX_NAMED_PIPE_ERRORS is 5 - let result = - handle_pipe_error_with_backoff(&mut consecutive_errors, "test", &error, &cancel_token) - .await; - assert!( - result.is_err(), - "5th error should fail with max errors exceeded" - ); - assert_eq!(consecutive_errors, 5, "Error count should be 5"); - - let err_msg = result.unwrap_err().to_string(); - assert!( - err_msg.contains("Too many consecutive"), - "Error message should mention too many errors: {}", - err_msg - ); - } - - #[cfg(windows)] - #[tokio::test] - async fn test_handle_pipe_error_with_backoff_cancellation() { - use super::handle_pipe_error_with_backoff; - let cancel_token = CancellationToken::new(); - let mut consecutive_errors = 0; - let error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "test error"); - - // Spawn task to cancel after 10ms - let cancel_clone = cancel_token.clone(); - tokio::spawn(async move { - tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; - cancel_clone.cancel(); - }); - - // First error increments count and sleeps for 20ms (10 * 2^1) - let result = - handle_pipe_error_with_backoff(&mut consecutive_errors, "test", &error, &cancel_token) - .await; - - // Should return Ok(false) because token was cancelled during backoff sleep - assert!(result.is_ok(), "Should succeed even when cancelled"); - assert_eq!(result.unwrap(), false, "Should return false when cancelled"); - assert_eq!(consecutive_errors, 1, "Error count should be 1"); - } - - #[cfg(windows)] - #[test] - fn test_normalize_pipe_name() { - use super::normalize_pipe_name; - - // Test cases: (input, expected_output, description) - let test_cases = vec![ - // Names without prefix should get it added - ( - "my_pipe", - "\\\\.\\pipe\\my_pipe", - "simple name without prefix", - ), - ( - "dd_dogstatsd", - "\\\\.\\pipe\\dd_dogstatsd", - "dd_dogstatsd without prefix", - ), - ( - "datadog_statsd", - "\\\\.\\pipe\\datadog_statsd", - "datadog_statsd without prefix", - ), - ( - "test-pipe_123", - "\\\\.\\pipe\\test-pipe_123", - "name with hyphens and underscores", - ), - ("", "\\\\.\\pipe\\", "empty string"), - // Names with prefix should remain unchanged - ( - "\\\\.\\pipe\\my_pipe", - "\\\\.\\pipe\\my_pipe", - "already has prefix", - ), - ( - "\\\\.\\pipe\\dd_dogstatsd", - "\\\\.\\pipe\\dd_dogstatsd", - "dd_dogstatsd with prefix", - ), - ( - "\\\\.\\pipe\\test", - "\\\\.\\pipe\\test", - "short name with prefix", - ), - ]; - - for (input, expected, description) in test_cases { - assert_eq!( - normalize_pipe_name(input), - expected, - "Failed for test case: {}", - description - ); - } - } } diff --git a/crates/dogstatsd/tests/integration_test.rs b/crates/dogstatsd/tests/integration_test.rs index 8a81cfe..bd9d141 100644 --- a/crates/dogstatsd/tests/integration_test.rs +++ b/crates/dogstatsd/tests/integration_test.rs @@ -46,7 +46,7 @@ async fn dogstatsd_server_ships_series() { // Start the service in a background task tokio::spawn(service.run()); - let cancel_token = start_dogstatsd(handle.clone()).await; + let _ = start_dogstatsd(handle.clone()).await; let api_key_factory = ApiKeyFactory::new("mock-api-key"); @@ -93,9 +93,6 @@ async fn dogstatsd_server_ships_series() { Ok(_) => mock.assert(), Err(_) => panic!("timed out before server received metric flush"), } - - // Cleanup - cancel_token.cancel(); } async fn start_dogstatsd(aggregator_handle: AggregatorHandle) -> CancellationToken { From 845ea9f33c2ddcbb433d1ee02d53303cbfd48d2b Mon Sep 17 00:00:00 2001 From: Lewis Date: Wed, 31 Dec 2025 11:36:24 -0500 Subject: [PATCH 15/21] Add buffer for metrics --- crates/dogstatsd/src/dogstatsd.rs | 66 +++++++++++++++++----- crates/dogstatsd/tests/integration_test.rs | 2 +- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/crates/dogstatsd/src/dogstatsd.rs b/crates/dogstatsd/src/dogstatsd.rs index 431b5ae..aa591b5 100644 --- a/crates/dogstatsd/src/dogstatsd.rs +++ b/crates/dogstatsd/src/dogstatsd.rs @@ -199,6 +199,12 @@ impl DogStatsD { .await .expect("didn't receive data"); + // Skip empty buffers (e.g., from channel close) + if buf.is_empty() { + debug!("Received empty buffer from {}, skipping", src); + return; + } + #[allow(clippy::expect_used)] let msgs = std::str::from_utf8(&buf).expect("couldn't parse as string"); trace!("Received message: {} from {}", msgs, src); @@ -270,10 +276,10 @@ async fn run_named_pipe_server( break; } - // Create new server instance + // Create new named pipe server let server = match ServerOptions::new().create(&*pipe_name) { Ok(s) => { - debug!("Created pipe server instance '{}'", pipe_name); + debug!("Created pipe server instance '{}' in byte mode", pipe_name); s } Err(e) => { @@ -300,35 +306,65 @@ async fn run_named_pipe_server( // Spawn task to handle this client let sender_clone = sender.clone(); - let pipe_name_clone = pipe_name.clone(); let cancel_clone = cancel_token.clone(); tokio::spawn(async move { let mut buf = [0u8; BUFFER_SIZE]; let mut server = server; + // Byte mode requires manual buffering to handle messages split across reads + // so let's track where we're writing each read in the buffer + let mut start_write_index = 0; loop { - // Read with cancellation support let read_result = tokio::select! { - result = server.read(&mut buf) => result, + result = server.read(&mut buf[start_write_index..]) => result, _ = cancel_clone.cancelled() => break, }; - let n = match read_result { - Ok(0) => { - debug!("Client disconnected from '{}'", pipe_name_clone); - break; // Client disconnected - } - Ok(n) => n, + let bytes_read = match read_result { Err(e) => { error!("Read error on '{}': {}", pipe_name_clone, e); break; } + Ok(0) => { + debug!("Client disconnected from '{}'", pipe_name_clone); + break; + } + Ok(n) => n, }; - // Send data to the main read function / processing loop - if sender_clone.send(buf[..n].to_vec()).is_err() { - error!("Failed to send data from '{}'", pipe_name_clone); - break; + let end_index = start_write_index + bytes_read; + + // From the start of the buffer to the end of the last complete message + let complete_message_size = buf[..end_index] + .iter() + .rposition(|&b| b == b'\n') + .map(|pos| pos + 1) // \n is part of that last message, so +1 + .unwrap_or(0); + + if complete_message_size > 0 { + // Send complete messages + match sender_clone.send(buf[..message_size].to_vec()) { + Err(e) => { + error!("Failed to send data from '{}': {}", pipe_name_clone, e); + break; + } + Ok(msg_str) => { + std::str::from_utf8(&buf[..message_size]) { + debug!("Sent {} bytes from '{}'", message_size, pipe_name_clone); + } + } + } + } + + // Complete message has been sent, so we can write over it. + start_write_index = end_index - complete_message_size; + + // If the message is bigger than the buffer size, drop it and go on. + if start_write_index >= BUFFER_SIZE { + start_write_index = 0; + } else if start_write_index > 0 { + // Keep incomplete data in the buffer + buf.copy_within(message_size..end_index, 0); } } // Server instance is dropped here, automatically cleaned up diff --git a/crates/dogstatsd/tests/integration_test.rs b/crates/dogstatsd/tests/integration_test.rs index bd9d141..c60e2ae 100644 --- a/crates/dogstatsd/tests/integration_test.rs +++ b/crates/dogstatsd/tests/integration_test.rs @@ -449,4 +449,4 @@ async fn test_named_pipe_cancellation() { assert!(result.is_ok(), "task should complete after cancellation"); handle.shutdown().expect("shutdown failed"); -} +} \ No newline at end of file From 3e7da73bb552e9abc3152bb0c7c6da739684042d Mon Sep 17 00:00:00 2001 From: Lewis Date: Wed, 31 Dec 2025 14:54:34 -0500 Subject: [PATCH 16/21] Add test for metrics buffer --- crates/dogstatsd/src/dogstatsd.rs | 16 +++-- crates/dogstatsd/tests/integration_test.rs | 73 +++++++++++++++++++++- 2 files changed, 82 insertions(+), 7 deletions(-) diff --git a/crates/dogstatsd/src/dogstatsd.rs b/crates/dogstatsd/src/dogstatsd.rs index aa591b5..a7faa2c 100644 --- a/crates/dogstatsd/src/dogstatsd.rs +++ b/crates/dogstatsd/src/dogstatsd.rs @@ -307,6 +307,7 @@ async fn run_named_pipe_server( // Spawn task to handle this client let sender_clone = sender.clone(); let cancel_clone = cancel_token.clone(); + let pipe_name_clone = pipe_name.clone(); tokio::spawn(async move { let mut buf = [0u8; BUFFER_SIZE]; let mut server = server; @@ -338,19 +339,22 @@ async fn run_named_pipe_server( let complete_message_size = buf[..end_index] .iter() .rposition(|&b| b == b'\n') - .map(|pos| pos + 1) // \n is part of that last message, so +1 + .map(|pos| pos + 1) // \n is part of that last message, so +1 .unwrap_or(0); if complete_message_size > 0 { // Send complete messages - match sender_clone.send(buf[..message_size].to_vec()) { + match sender_clone.send(buf[..complete_message_size].to_vec()) { Err(e) => { error!("Failed to send data from '{}': {}", pipe_name_clone, e); break; } - Ok(msg_str) => { - std::str::from_utf8(&buf[..message_size]) { - debug!("Sent {} bytes from '{}'", message_size, pipe_name_clone); + Ok(_) => { + if let Ok(_) = std::str::from_utf8(&buf[..complete_message_size]) { + debug!( + "Sent {} bytes from '{}'", + complete_message_size, pipe_name_clone + ); } } } @@ -364,7 +368,7 @@ async fn run_named_pipe_server( start_write_index = 0; } else if start_write_index > 0 { // Keep incomplete data in the buffer - buf.copy_within(message_size..end_index, 0); + buf.copy_within(complete_message_size..end_index, 0); } } // Server instance is dropped here, automatically cleaned up diff --git a/crates/dogstatsd/tests/integration_test.rs b/crates/dogstatsd/tests/integration_test.rs index c60e2ae..443c72e 100644 --- a/crates/dogstatsd/tests/integration_test.rs +++ b/crates/dogstatsd/tests/integration_test.rs @@ -449,4 +449,75 @@ async fn test_named_pipe_cancellation() { assert!(result.is_ok(), "task should complete after cancellation"); handle.shutdown().expect("shutdown failed"); -} \ No newline at end of file +} + +#[cfg(test)] +#[cfg(windows)] +#[tokio::test] +async fn test_buffer_split_message() { + let pipe_name = r"\\.\pipe\test_dogstatsd_buffer_split"; + let (service, handle) = AggregatorService::new(SortedTags::parse("test:value").unwrap(), 1_024) + .expect("aggregator service creation failed"); + tokio::spawn(service.run()); + + let cancel_token = CancellationToken::new(); + + // Start DogStatsD server + let dogstatsd_task = { + let handle = handle.clone(); + let cancel_token_clone = cancel_token.clone(); + tokio::spawn(async move { + let dogstatsd = DogStatsD::new( + &DogStatsDConfig { + host: String::new(), + port: 0, + metric_namespace: None, + windows_pipe_name: Some(pipe_name.to_string()), + }, + handle, + cancel_token_clone, + ) + .await; + dogstatsd.spin().await; + }) + }; + + sleep(Duration::from_millis(100)).await; + + // Connect client and send partial message (no newline) + let mut client = ClientOptions::new().open(pipe_name).expect("client open"); + client + .write_all(b"test.split:1|") + .await + .expect("write partial"); + client.flush().await.expect("flush partial"); + + // Wait briefly to simulate message arriving in separate reads + sleep(Duration::from_millis(50)).await; + + // Verify no metrics yet - buffer should be holding incomplete message + let response = handle.flush().await.expect("flush failed"); + assert!( + response.series.is_empty(), + "Expected no series from incomplete message without newline" + ); + + // Send the completion of the message + client.write_all(b"c\n").await.expect("write completion"); + client.flush().await.expect("flush completion"); + + sleep(Duration::from_millis(100)).await; + + // Verify metric was received and aggregated + let response = handle.flush().await.expect("flush failed"); + assert!( + !response.series.is_empty(), + "Expected at least one series with metrics from buffered split message" + ); + + // Cleanup + cancel_token.cancel(); + let result = timeout(Duration::from_millis(500), dogstatsd_task).await; + assert!(result.is_ok(), "tasks should complete after cancellation"); + handle.shutdown().expect("shutdown failed"); +} From db42f1e3a16e1c38666c27b0968f455c3759bf5f Mon Sep 17 00:00:00 2001 From: Lewis Date: Fri, 9 Jan 2026 09:47:22 -0500 Subject: [PATCH 17/21] Include named pipe in /info --- crates/datadog-serverless-compat/src/main.rs | 42 ++-------- crates/datadog-trace-agent/src/config.rs | 81 ++++++++++++++++--- crates/datadog-trace-agent/src/mini_agent.rs | 8 +- .../src/trace_processor.rs | 1 + .../tests/integration_test.rs | 1 + 5 files changed, 88 insertions(+), 45 deletions(-) diff --git a/crates/datadog-serverless-compat/src/main.rs b/crates/datadog-serverless-compat/src/main.rs index 5d7a6db..c583ca1 100644 --- a/crates/datadog-serverless-compat/src/main.rs +++ b/crates/datadog-serverless-compat/src/main.rs @@ -40,7 +40,6 @@ use tokio_util::sync::CancellationToken; const DOGSTATSD_FLUSH_INTERVAL: u64 = 10; const DOGSTATSD_TIMEOUT_DURATION: Duration = Duration::from_secs(5); -const DEFAULT_DOGSTATSD_PORT: u16 = 8125; const AGENT_HOST: &str = "0.0.0.0"; #[tokio::main] @@ -65,36 +64,6 @@ pub async fn main() { }; let dd_api_key: Option = env::var("DD_API_KEY").ok(); - - // Windows named pipe name for DogStatsD. - // Normalize by adding \\.\pipe\ prefix if not present - let dd_dogstatsd_windows_pipe_name: Option = { - #[cfg(windows)] - { - env::var("DD_DOGSTATSD_WINDOWS_PIPE_NAME") - .ok() - .map(|pipe_name| { - if pipe_name.starts_with("\\\\.\\pipe\\") || pipe_name.starts_with(r"\\.\pipe\") - { - pipe_name - } else { - format!(r"\\.\pipe\{}", pipe_name) - } - }) - } - #[cfg(not(windows))] - { - None - } - }; - let dd_dogstatsd_port: u16 = if dd_dogstatsd_windows_pipe_name.is_some() { - 0 // Override to 0 when using Windows named pipe - } else { - env::var("DD_DOGSTATSD_PORT") - .ok() - .and_then(|port| port.parse::().ok()) - .unwrap_or(DEFAULT_DOGSTATSD_PORT) - }; let dd_site = env::var("DD_SITE").unwrap_or_else(|_| "datadoghq.com".to_string()); let dd_use_dogstatsd = env::var("DD_USE_DOGSTATSD") .map(|val| val.to_lowercase() != "false") @@ -172,19 +141,22 @@ pub async fn main() { let (mut metrics_flusher, _aggregator_handle) = if dd_use_dogstatsd { debug!("Starting dogstatsd"); let (_, metrics_flusher, aggregator_handle) = start_dogstatsd( - dd_dogstatsd_port, + config.dd_dogstatsd_port, dd_api_key, dd_site, https_proxy, dogstatsd_tags, dd_statsd_metric_namespace, - dd_dogstatsd_windows_pipe_name.clone(), + config.dd_dogstatsd_windows_pipe_name.clone(), ) .await; - if let Some(ref windows_pipe_name) = dd_dogstatsd_windows_pipe_name { + if let Some(ref windows_pipe_name) = config.dd_dogstatsd_windows_pipe_name { info!("dogstatsd-pipe: starting to listen on pipe {windows_pipe_name}"); } else { - info!("dogstatsd-udp: starting to listen on port {dd_dogstatsd_port}"); + info!( + "dogstatsd-udp: starting to listen on port {}", + config.dd_dogstatsd_port + ); } (metrics_flusher, Some(aggregator_handle)) } else { diff --git a/crates/datadog-trace-agent/src/config.rs b/crates/datadog-trace-agent/src/config.rs index cf93485..6a02ef2 100644 --- a/crates/datadog-trace-agent/src/config.rs +++ b/crates/datadog-trace-agent/src/config.rs @@ -77,6 +77,7 @@ pub struct Config { pub dd_apm_receiver_port: u16, pub dd_apm_windows_pipe_name: Option, pub dd_dogstatsd_port: u16, + pub dd_dogstatsd_windows_pipe_name: Option, pub env_type: trace_utils::EnvironmentType, pub app_name: Option, pub max_request_content_length: usize, @@ -112,11 +113,25 @@ impl Config { anyhow::anyhow!("Unable to identify environment. Shutting down Mini Agent.") })?; - let dd_apm_windows_pipe_name: Option = - env::var("DD_APM_WINDOWS_PIPE_NAME").ok().map(|pipe_name| { - // Prepend \\.\pipe\ prefix to match datadog-agent behavior - format!(r"\\.\pipe\{}", pipe_name) - }); + // Windows named pipe name for APM receiver. + // Normalize by adding \\.\pipe\ prefix if not present + let dd_apm_windows_pipe_name: Option = { + #[cfg(any(windows, test))] + { + env::var("DD_APM_WINDOWS_PIPE_NAME").ok().map(|pipe_name| { + if pipe_name.starts_with("\\\\.\\pipe\\") || pipe_name.starts_with(r"\\.\pipe\") + { + pipe_name + } else { + format!(r"\\.\pipe\{}", pipe_name) + } + }) + } + #[cfg(not(any(windows, test)))] + { + None + } + }; let dd_apm_receiver_port: u16 = if dd_apm_windows_pipe_name.is_some() { 0 // Override to 0 when using Windows named pipe } else { @@ -126,10 +141,36 @@ impl Config { .unwrap_or(DEFAULT_APM_RECEIVER_PORT) }; - let dd_dogstatsd_port: u16 = env::var("DD_DOGSTATSD_PORT") - .ok() - .and_then(|port| port.parse::().ok()) - .unwrap_or(DEFAULT_DOGSTATSD_PORT); + // Windows named pipe name for DogStatsD. + // Normalize by adding \\.\pipe\ prefix if not present + let dd_dogstatsd_windows_pipe_name: Option = { + #[cfg(any(windows, test))] + { + env::var("DD_DOGSTATSD_WINDOWS_PIPE_NAME") + .ok() + .map(|pipe_name| { + if pipe_name.starts_with("\\\\.\\pipe\\") + || pipe_name.starts_with(r"\\.\pipe\") + { + pipe_name + } else { + format!(r"\\.\pipe\{}", pipe_name) + } + }) + } + #[cfg(not(any(windows, test)))] + { + None + } + }; + let dd_dogstatsd_port: u16 = if dd_dogstatsd_windows_pipe_name.is_some() { + 0 // Override to 0 when using Windows named pipe + } else { + env::var("DD_DOGSTATSD_PORT") + .ok() + .and_then(|port| port.parse::().ok()) + .unwrap_or(DEFAULT_DOGSTATSD_PORT) + }; let dd_site = env::var("DD_SITE").unwrap_or_else(|_| "datadoghq.com".to_string()); // construct the trace & trace stats intake urls based on DD_SITE env var (to flush traces & @@ -179,6 +220,7 @@ impl Config { dd_apm_receiver_port, dd_apm_windows_pipe_name, dd_dogstatsd_port, + dd_dogstatsd_windows_pipe_name, dd_site, trace_intake: Endpoint { url: hyper::Uri::from_str(&trace_intake_url).unwrap(), @@ -371,6 +413,7 @@ mod tests { config.dd_apm_windows_pipe_name, Some(r"\\.\pipe\test_pipe".to_string()) ); + // Port should be overridden to 0 when pipe is set assert_eq!(config.dd_apm_receiver_port, 0); env::remove_var("DD_API_KEY"); @@ -378,6 +421,26 @@ mod tests { env::remove_var("DD_APM_WINDOWS_PIPE_NAME"); } + #[test] + #[serial] + fn test_dogstatsd_windows_pipe_name() { + env::set_var("DD_API_KEY", "_not_a_real_key_"); + env::set_var("ASCSVCRT_SPRING__APPLICATION__NAME", "test-spring-app"); + env::set_var("DD_DOGSTATSD_WINDOWS_PIPE_NAME", r"test_pipe"); + let config_res = config::Config::new(); + assert!(config_res.is_ok()); + let config = config_res.unwrap(); + assert_eq!( + config.dd_dogstatsd_windows_pipe_name, + Some(r"\\.\pipe\test_pipe".to_string()) + ); + // Port should be overridden to 0 when pipe is set + assert_eq!(config.dd_dogstatsd_port, 0); + env::remove_var("DD_API_KEY"); + env::remove_var("ASCSVCRT_SPRING__APPLICATION__NAME"); + env::remove_var("DD_DOGSTATSD_WINDOWS_PIPE_NAME"); + } + #[test] #[serial] fn test_default_apm_receiver_port() { diff --git a/crates/datadog-trace-agent/src/mini_agent.rs b/crates/datadog-trace-agent/src/mini_agent.rs index 7e7cef1..330921e 100644 --- a/crates/datadog-trace-agent/src/mini_agent.rs +++ b/crates/datadog-trace-agent/src/mini_agent.rs @@ -388,6 +388,7 @@ impl MiniAgent { config.dd_apm_receiver_port, config.dd_apm_windows_pipe_name.as_deref(), config.dd_dogstatsd_port, + config.dd_dogstatsd_windows_pipe_name.as_deref(), ) { Ok(res) => Ok(res), Err(err) => log_and_create_http_response( @@ -460,16 +461,21 @@ impl MiniAgent { dd_apm_receiver_port: u16, dd_apm_windows_pipe_name: Option<&str>, dd_dogstatsd_port: u16, + dd_dogstatsd_windows_pipe_name: Option<&str>, ) -> http::Result { // pipe_name already includes \\.\pipe\ prefix from config let receiver_socket = dd_apm_windows_pipe_name.unwrap_or(""); - let config_json = serde_json::json!({ + let mut config_json = serde_json::json!({ "receiver_port": dd_apm_receiver_port, "statsd_port": dd_dogstatsd_port, "receiver_socket": receiver_socket }); + if let Some(pipe_name) = dd_dogstatsd_windows_pipe_name { + config_json["statsd_windows_pipe_name"] = serde_json::json!(pipe_name); + } + let response_json = json!( { "endpoints": [ diff --git a/crates/datadog-trace-agent/src/trace_processor.rs b/crates/datadog-trace-agent/src/trace_processor.rs index fb30809..7d30ef0 100644 --- a/crates/datadog-trace-agent/src/trace_processor.rs +++ b/crates/datadog-trace-agent/src/trace_processor.rs @@ -206,6 +206,7 @@ mod tests { dd_apm_receiver_port: 8126, dd_apm_windows_pipe_name: None, dd_dogstatsd_port: 8125, + dd_dogstatsd_windows_pipe_name: None, env_type: trace_utils::EnvironmentType::CloudFunction, os: "linux".to_string(), obfuscation_config: ObfuscationConfig::new().unwrap(), diff --git a/crates/datadog-trace-agent/tests/integration_test.rs b/crates/datadog-trace-agent/tests/integration_test.rs index eacb64d..62d632e 100644 --- a/crates/datadog-trace-agent/tests/integration_test.rs +++ b/crates/datadog-trace-agent/tests/integration_test.rs @@ -26,6 +26,7 @@ pub fn create_tcp_test_config() -> Config { dd_apm_receiver_port: 8126, dd_apm_windows_pipe_name: None, dd_dogstatsd_port: 8125, + dd_dogstatsd_windows_pipe_name: None, env_type: trace_utils::EnvironmentType::AzureFunction, app_name: Some("test-app".to_string()), max_request_content_length: 10_000_000, From 5c65840997ae8f28122f9f202f4789dc97b5728b Mon Sep 17 00:00:00 2001 From: Lewis Date: Mon, 2 Feb 2026 12:29:33 -0500 Subject: [PATCH 18/21] Keep dogstatsd+tracer separate --- .gitignore | 2 +- crates/datadog-serverless-compat/src/main.rs | 42 ++++++++++--- crates/datadog-trace-agent/src/config.rs | 62 +++++++++++--------- 3 files changed, 70 insertions(+), 36 deletions(-) diff --git a/.gitignore b/.gitignore index d53232b..df769d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /target - /.idea + diff --git a/crates/datadog-serverless-compat/src/main.rs b/crates/datadog-serverless-compat/src/main.rs index c583ca1..5d7a6db 100644 --- a/crates/datadog-serverless-compat/src/main.rs +++ b/crates/datadog-serverless-compat/src/main.rs @@ -40,6 +40,7 @@ use tokio_util::sync::CancellationToken; const DOGSTATSD_FLUSH_INTERVAL: u64 = 10; const DOGSTATSD_TIMEOUT_DURATION: Duration = Duration::from_secs(5); +const DEFAULT_DOGSTATSD_PORT: u16 = 8125; const AGENT_HOST: &str = "0.0.0.0"; #[tokio::main] @@ -64,6 +65,36 @@ pub async fn main() { }; let dd_api_key: Option = env::var("DD_API_KEY").ok(); + + // Windows named pipe name for DogStatsD. + // Normalize by adding \\.\pipe\ prefix if not present + let dd_dogstatsd_windows_pipe_name: Option = { + #[cfg(windows)] + { + env::var("DD_DOGSTATSD_WINDOWS_PIPE_NAME") + .ok() + .map(|pipe_name| { + if pipe_name.starts_with("\\\\.\\pipe\\") || pipe_name.starts_with(r"\\.\pipe\") + { + pipe_name + } else { + format!(r"\\.\pipe\{}", pipe_name) + } + }) + } + #[cfg(not(windows))] + { + None + } + }; + let dd_dogstatsd_port: u16 = if dd_dogstatsd_windows_pipe_name.is_some() { + 0 // Override to 0 when using Windows named pipe + } else { + env::var("DD_DOGSTATSD_PORT") + .ok() + .and_then(|port| port.parse::().ok()) + .unwrap_or(DEFAULT_DOGSTATSD_PORT) + }; let dd_site = env::var("DD_SITE").unwrap_or_else(|_| "datadoghq.com".to_string()); let dd_use_dogstatsd = env::var("DD_USE_DOGSTATSD") .map(|val| val.to_lowercase() != "false") @@ -141,22 +172,19 @@ pub async fn main() { let (mut metrics_flusher, _aggregator_handle) = if dd_use_dogstatsd { debug!("Starting dogstatsd"); let (_, metrics_flusher, aggregator_handle) = start_dogstatsd( - config.dd_dogstatsd_port, + dd_dogstatsd_port, dd_api_key, dd_site, https_proxy, dogstatsd_tags, dd_statsd_metric_namespace, - config.dd_dogstatsd_windows_pipe_name.clone(), + dd_dogstatsd_windows_pipe_name.clone(), ) .await; - if let Some(ref windows_pipe_name) = config.dd_dogstatsd_windows_pipe_name { + if let Some(ref windows_pipe_name) = dd_dogstatsd_windows_pipe_name { info!("dogstatsd-pipe: starting to listen on pipe {windows_pipe_name}"); } else { - info!( - "dogstatsd-udp: starting to listen on port {}", - config.dd_dogstatsd_port - ); + info!("dogstatsd-udp: starting to listen on port {dd_dogstatsd_port}"); } (metrics_flusher, Some(aggregator_handle)) } else { diff --git a/crates/datadog-trace-agent/src/config.rs b/crates/datadog-trace-agent/src/config.rs index 6a02ef2..db40522 100644 --- a/crates/datadog-trace-agent/src/config.rs +++ b/crates/datadog-trace-agent/src/config.rs @@ -24,6 +24,12 @@ pub struct Tags { function_tags_string: OnceLock, } +impl Default for Tags { + fn default() -> Self { + Self::new() + } +} + impl Tags { pub fn from_env_string(env_tags: &str) -> Self { let mut tags = HashMap::new(); @@ -141,8 +147,6 @@ impl Config { .unwrap_or(DEFAULT_APM_RECEIVER_PORT) }; - // Windows named pipe name for DogStatsD. - // Normalize by adding \\.\pipe\ prefix if not present let dd_dogstatsd_windows_pipe_name: Option = { #[cfg(any(windows, test))] { @@ -171,6 +175,7 @@ impl Config { .and_then(|port| port.parse::().ok()) .unwrap_or(DEFAULT_DOGSTATSD_PORT) }; + let dd_site = env::var("DD_SITE").unwrap_or_else(|_| "datadoghq.com".to_string()); // construct the trace & trace stats intake urls based on DD_SITE env var (to flush traces & @@ -373,72 +378,73 @@ mod tests { #[test] #[serial] - fn test_default_dogstatsd_port() { + fn test_apm_windows_pipe_name() { env::set_var("DD_API_KEY", "_not_a_real_key_"); env::set_var("ASCSVCRT_SPRING__APPLICATION__NAME", "test-spring-app"); + env::set_var("DD_APM_WINDOWS_PIPE_NAME", r"test_pipe"); let config_res = config::Config::new(); assert!(config_res.is_ok()); let config = config_res.unwrap(); - assert_eq!(config.dd_dogstatsd_port, 8125); + assert_eq!( + config.dd_apm_windows_pipe_name, + Some(r"\\.\pipe\test_pipe".to_string()) + ); + + // Port should be overridden to 0 when pipe is set + assert_eq!(config.dd_apm_receiver_port, 0); env::remove_var("DD_API_KEY"); env::remove_var("ASCSVCRT_SPRING__APPLICATION__NAME"); + env::remove_var("DD_APM_WINDOWS_PIPE_NAME"); } #[test] #[serial] - fn test_custom_dogstatsd_port() { + fn test_dogstatsd_windows_pipe_name() { env::set_var("DD_API_KEY", "_not_a_real_key_"); env::set_var("ASCSVCRT_SPRING__APPLICATION__NAME", "test-spring-app"); - env::set_var("DD_DOGSTATSD_PORT", "18125"); + env::set_var("DD_DOGSTATSD_WINDOWS_PIPE_NAME", r"test_pipe"); let config_res = config::Config::new(); - println!("{:?}", config_res); assert!(config_res.is_ok()); let config = config_res.unwrap(); - assert_eq!(config.dd_dogstatsd_port, 18125); + assert_eq!( + config.dd_dogstatsd_windows_pipe_name, + Some(r"\\.\pipe\test_pipe".to_string()) + ); + + // Port should be overridden to 0 when pipe is set + assert_eq!(config.dd_dogstatsd_port, 0); env::remove_var("DD_API_KEY"); env::remove_var("ASCSVCRT_SPRING__APPLICATION__NAME"); - env::remove_var("DD_DOGSTATSD_PORT"); + env::remove_var("DD_DOGSTATSD_WINDOWS_PIPE_NAME"); } #[test] #[serial] - fn test_apm_windows_pipe_name() { + fn test_default_dogstatsd_port() { env::set_var("DD_API_KEY", "_not_a_real_key_"); env::set_var("ASCSVCRT_SPRING__APPLICATION__NAME", "test-spring-app"); - env::set_var("DD_APM_WINDOWS_PIPE_NAME", r"test_pipe"); let config_res = config::Config::new(); assert!(config_res.is_ok()); let config = config_res.unwrap(); - assert_eq!( - config.dd_apm_windows_pipe_name, - Some(r"\\.\pipe\test_pipe".to_string()) - ); - - // Port should be overridden to 0 when pipe is set - assert_eq!(config.dd_apm_receiver_port, 0); + assert_eq!(config.dd_dogstatsd_port, 8125); env::remove_var("DD_API_KEY"); env::remove_var("ASCSVCRT_SPRING__APPLICATION__NAME"); - env::remove_var("DD_APM_WINDOWS_PIPE_NAME"); } #[test] #[serial] - fn test_dogstatsd_windows_pipe_name() { + fn test_custom_dogstatsd_port() { env::set_var("DD_API_KEY", "_not_a_real_key_"); env::set_var("ASCSVCRT_SPRING__APPLICATION__NAME", "test-spring-app"); - env::set_var("DD_DOGSTATSD_WINDOWS_PIPE_NAME", r"test_pipe"); + env::set_var("DD_DOGSTATSD_PORT", "18125"); let config_res = config::Config::new(); + println!("{:?}", config_res); assert!(config_res.is_ok()); let config = config_res.unwrap(); - assert_eq!( - config.dd_dogstatsd_windows_pipe_name, - Some(r"\\.\pipe\test_pipe".to_string()) - ); - // Port should be overridden to 0 when pipe is set - assert_eq!(config.dd_dogstatsd_port, 0); + assert_eq!(config.dd_dogstatsd_port, 18125); env::remove_var("DD_API_KEY"); env::remove_var("ASCSVCRT_SPRING__APPLICATION__NAME"); - env::remove_var("DD_DOGSTATSD_WINDOWS_PIPE_NAME"); + env::remove_var("DD_DOGSTATSD_PORT"); } #[test] From 21040d5ea9c3c96fefc8a1306f801026aebcc967 Mon Sep 17 00:00:00 2001 From: Lewis Date: Tue, 3 Feb 2026 09:52:17 -0500 Subject: [PATCH 19/21] Update license file --- LICENSE-3rdparty.csv | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index f535588..6b22109 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -120,12 +120,11 @@ nix,https://github.com/nix-rust/nix,MIT,The nix-rust Project Developers nu-ansi-term,https://github.com/nushell/nu-ansi-term,MIT,"ogham@bsago.me, Ryan Scheel (Havvy) , Josh Triplett , The Nushell Project Developers" num-traits,https://github.com/rust-num/num-traits,MIT OR Apache-2.0,The Rust Project Developers once_cell,https://github.com/matklad/once_cell,MIT OR Apache-2.0,Aleksey Kladov -openssl-probe,https://github.com/alexcrichton/openssl-probe,MIT OR Apache-2.0,Alex Crichton +openssl-probe,https://github.com/rustls/openssl-probe,MIT OR Apache-2.0,Alex Crichton ordered-float,https://github.com/reem/rust-ordered-float,MIT,"Jonathan Reem , Matt Brubeck " parking,https://github.com/smol-rs/parking,Apache-2.0 OR MIT,"Stjepan Glavina , The Rust Project Developers" parking_lot,https://github.com/Amanieu/parking_lot,MIT OR Apache-2.0,Amanieu d'Antras parking_lot_core,https://github.com/Amanieu/parking_lot,MIT OR Apache-2.0,Amanieu d'Antras -paste,https://github.com/dtolnay/paste,MIT OR Apache-2.0,David Tolnay path-tree,https://github.com/viz-rs/path-tree,MIT OR Apache-2.0,Fangdun Tsai percent-encoding,https://github.com/servo/rust-url,MIT OR Apache-2.0,The rust-url developers petgraph,https://github.com/petgraph/petgraph,MIT OR Apache-2.0,"bluss, mitchmindtree" @@ -162,7 +161,7 @@ regex-automata,https://github.com/rust-lang/regex,MIT OR Apache-2.0,"The Rust Pr regex-syntax,https://github.com/rust-lang/regex,MIT OR Apache-2.0,"The Rust Project Developers, Andrew Gallant " reqwest,https://github.com/seanmonstar/reqwest,MIT OR Apache-2.0,Sean McArthur ring,https://github.com/briansmith/ring,Apache-2.0 AND ISC,The ring Authors -rmp,https://github.com/3Hren/msgpack-rust,MIT,Evgeny Safronov +rmp,https://github.com/3Hren/msgpack-rust,MIT,"Evgeny Safronov , Kornel " rmp-serde,https://github.com/3Hren/msgpack-rust,MIT,Evgeny Safronov rmpv,https://github.com/3Hren/msgpack-rust,MIT,Evgeny Safronov rustc-hash,https://github.com/rust-lang/rustc-hash,Apache-2.0 OR MIT,The Rust Project Developers @@ -285,6 +284,7 @@ zeroize,https://github.com/RustCrypto/utils,Apache-2.0 OR MIT,The RustCrypto Pro zerotrie,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers zerovec,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers zerovec-derive,https://github.com/unicode-org/icu4x,Unicode-3.0,Manish Goregaokar +zmij,https://github.com/dtolnay/zmij,MIT,David Tolnay zstd,https://github.com/gyscos/zstd-rs,MIT,Alexandre Bury zstd-safe,https://github.com/gyscos/zstd-rs,MIT OR Apache-2.0,Alexandre Bury zstd-sys,https://github.com/gyscos/zstd-rs,MIT OR Apache-2.0,Alexandre Bury From 1ae2a1980938eaf7d8cde12e0787daa4c641e83d Mon Sep 17 00:00:00 2001 From: Lewis Date: Wed, 4 Feb 2026 11:29:30 -0500 Subject: [PATCH 20/21] Fix rebase debug message formatting --- crates/datadog-trace-agent/src/trace_processor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/datadog-trace-agent/src/trace_processor.rs b/crates/datadog-trace-agent/src/trace_processor.rs index 7d30ef0..f9d796c 100644 --- a/crates/datadog-trace-agent/src/trace_processor.rs +++ b/crates/datadog-trace-agent/src/trace_processor.rs @@ -104,7 +104,7 @@ impl TraceProcessor for ServerlessTraceProcessor { // double check content length is < max request content length in case transfer encoding is used if body_size > config.max_request_content_length { return log_and_create_http_response( - &format!("Error processing traces: Payload too large"), + "Error processing traces: Payload too large", StatusCode::PAYLOAD_TOO_LARGE, ); } From 12ea043f96e5fd541ab53a05eaeadc9eaafad704 Mon Sep 17 00:00:00 2001 From: Lewis Date: Wed, 4 Feb 2026 12:41:11 -0500 Subject: [PATCH 21/21] Remove statsd metrics named pipe from /info --- crates/datadog-trace-agent/src/mini_agent.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/datadog-trace-agent/src/mini_agent.rs b/crates/datadog-trace-agent/src/mini_agent.rs index 330921e..7e7cef1 100644 --- a/crates/datadog-trace-agent/src/mini_agent.rs +++ b/crates/datadog-trace-agent/src/mini_agent.rs @@ -388,7 +388,6 @@ impl MiniAgent { config.dd_apm_receiver_port, config.dd_apm_windows_pipe_name.as_deref(), config.dd_dogstatsd_port, - config.dd_dogstatsd_windows_pipe_name.as_deref(), ) { Ok(res) => Ok(res), Err(err) => log_and_create_http_response( @@ -461,21 +460,16 @@ impl MiniAgent { dd_apm_receiver_port: u16, dd_apm_windows_pipe_name: Option<&str>, dd_dogstatsd_port: u16, - dd_dogstatsd_windows_pipe_name: Option<&str>, ) -> http::Result { // pipe_name already includes \\.\pipe\ prefix from config let receiver_socket = dd_apm_windows_pipe_name.unwrap_or(""); - let mut config_json = serde_json::json!({ + let config_json = serde_json::json!({ "receiver_port": dd_apm_receiver_port, "statsd_port": dd_dogstatsd_port, "receiver_socket": receiver_socket }); - if let Some(pipe_name) = dd_dogstatsd_windows_pipe_name { - config_json["statsd_windows_pipe_name"] = serde_json::json!(pipe_name); - } - let response_json = json!( { "endpoints": [