From a8b1345733731e11e996cab2bd50289c0457339f Mon Sep 17 00:00:00 2001 From: Jules Wiriath Date: Mon, 13 Apr 2026 19:17:02 +0200 Subject: [PATCH 01/14] feat(capablities): sleep & spawn capabilities fix: ZST spawner fix: docs and fixes fix(shared_runtime): implicit debug impl chore: format feat: change the Output to Result feat: mod native cfg gated for better readability chore: revert to main's version of thing --- Cargo.lock | 1 + datadog-sidecar/src/service/agent_info.rs | 4 +- datadog-sidecar/src/service/stats_flusher.rs | 2 +- .../src/service/tracing/trace_flusher.rs | 9 +- libdd-capabilities-impl/Cargo.toml | 1 + libdd-capabilities-impl/README.md | 6 +- libdd-capabilities-impl/src/http.rs | 12 +- libdd-capabilities-impl/src/lib.rs | 112 ++-- libdd-capabilities-impl/src/sleep.rs | 19 + libdd-capabilities-impl/src/spawn.rs | 45 ++ libdd-capabilities/README.md | 8 +- libdd-capabilities/src/http.rs | 2 +- libdd-capabilities/src/lib.rs | 6 +- libdd-capabilities/src/sleep.rs | 15 + libdd-capabilities/src/spawn.rs | 52 ++ libdd-data-pipeline/benches/trace_buffer.rs | 4 +- libdd-data-pipeline/src/agent_info/fetcher.rs | 42 +- libdd-data-pipeline/src/otlp/exporter.rs | 8 +- libdd-data-pipeline/src/telemetry/mod.rs | 4 +- libdd-data-pipeline/src/trace_buffer/mod.rs | 44 +- .../src/trace_exporter/builder.rs | 386 ++++++------- libdd-data-pipeline/src/trace_exporter/mod.rs | 50 +- .../src/trace_exporter/stats.rs | 47 +- libdd-shared-runtime/src/lib.rs | 13 + .../src/shared_runtime/mod.rs | 528 +++++++++++------- .../src/shared_runtime/pausable_worker.rs | 113 ++-- libdd-trace-stats/src/stats_exporter.rs | 46 +- libdd-trace-utils/src/send_data/mod.rs | 49 +- libdd-trace-utils/src/send_with_retry/mod.rs | 47 +- .../src/send_with_retry/retry_strategy.rs | 88 ++- libdd-trace-utils/src/stats_utils.rs | 4 +- .../src/test_utils/datadog_test_agent.rs | 2 +- libdd-trace-utils/tests/test_send_data.rs | 2 +- 33 files changed, 1073 insertions(+), 698 deletions(-) create mode 100644 libdd-capabilities-impl/src/sleep.rs create mode 100644 libdd-capabilities-impl/src/spawn.rs create mode 100644 libdd-capabilities/src/sleep.rs create mode 100644 libdd-capabilities/src/spawn.rs diff --git a/Cargo.lock b/Cargo.lock index 61d77dea88..838632edd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2841,6 +2841,7 @@ dependencies = [ "http-body-util", "libdd-capabilities", "libdd-common", + "tokio", ] [[package]] diff --git a/datadog-sidecar/src/service/agent_info.rs b/datadog-sidecar/src/service/agent_info.rs index 82c94769a7..9854b138a9 100644 --- a/datadog-sidecar/src/service/agent_info.rs +++ b/datadog-sidecar/src/service/agent_info.rs @@ -16,7 +16,7 @@ use datadog_ipc::platform::NamedShmHandle; use futures::future::Shared; use futures::FutureExt; use http::uri::PathAndQuery; -use libdd_capabilities_impl::DefaultHttpClient; +use libdd_capabilities_impl::NativeCapabilities; use libdd_common::{Endpoint, MutexExt}; use libdd_data_pipeline::agent_info::schema::AgentInfoStruct; use libdd_data_pipeline::agent_info::{fetch_info_with_state, FetchInfoStatus}; @@ -103,7 +103,7 @@ impl AgentInfoFetcher { fetch_endpoint.url = http::Uri::from_parts(parts).unwrap(); loop { let fetched = - fetch_info_with_state::(&fetch_endpoint, state.as_deref()) + fetch_info_with_state::(&fetch_endpoint, state.as_deref()) .await; let mut complete_fut = None; { diff --git a/datadog-sidecar/src/service/stats_flusher.rs b/datadog-sidecar/src/service/stats_flusher.rs index a507477342..9865d52cef 100644 --- a/datadog-sidecar/src/service/stats_flusher.rs +++ b/datadog-sidecar/src/service/stats_flusher.rs @@ -15,7 +15,7 @@ use datadog_ipc::shm_stats::{ ShmSpanConcentrator, DEFAULT_SLOT_COUNT, DEFAULT_STRING_POOL_BYTES, RELOAD_FILL_RATIO, }; use http::uri::PathAndQuery; -use libdd_capabilities_impl::{HttpClientTrait, NativeCapabilities}; +use libdd_capabilities_impl::{HttpClientCapability, NativeCapabilities}; use libdd_common::{Endpoint, MutexExt}; use libdd_trace_stats::stats_exporter::{StatsExporter, StatsMetadata}; use std::collections::HashMap; diff --git a/datadog-sidecar/src/service/tracing/trace_flusher.rs b/datadog-sidecar/src/service/tracing/trace_flusher.rs index 53d09c5823..75897762ab 100644 --- a/datadog-sidecar/src/service/tracing/trace_flusher.rs +++ b/datadog-sidecar/src/service/tracing/trace_flusher.rs @@ -5,8 +5,7 @@ use super::TraceSendData; use crate::agent_remote_config::AgentRemoteConfigWriter; use datadog_ipc::platform::NamedShmHandle; use futures::future::join_all; -use libdd_capabilities::HttpClientTrait; -use libdd_capabilities_impl::DefaultHttpClient; +use libdd_capabilities_impl::{HttpClientCapability, NativeCapabilities}; use libdd_common::{Endpoint, MutexExt}; use libdd_trace_utils::trace_utils; use libdd_trace_utils::trace_utils::SendData; @@ -96,7 +95,7 @@ pub(crate) struct TraceFlusher { pub(crate) min_force_drop_size_bytes: AtomicU32, // put a limit on memory usage remote_config: Mutex, pub metrics: Mutex, - client: DefaultHttpClient, + capabilities: NativeCapabilities, } impl Default for TraceFlusher { fn default() -> Self { @@ -107,7 +106,7 @@ impl Default for TraceFlusher { min_force_drop_size_bytes: AtomicU32::new(trace_utils::MAX_PAYLOAD_SIZE as u32), remote_config: Mutex::new(Default::default()), metrics: Mutex::new(Default::default()), - client: DefaultHttpClient::new_client(), + capabilities: NativeCapabilities::new_client(), } } } @@ -249,7 +248,7 @@ impl TraceFlusher { async fn send_and_handle_trace(&self, send_data: SendData) { let endpoint = send_data.get_target().clone(); - let response = send_data.send(&self.client).await; + let response = send_data.send(&self.capabilities).await; self.metrics.lock_or_panic().update(&response); match response.last_result { Ok(response) => { diff --git a/libdd-capabilities-impl/Cargo.toml b/libdd-capabilities-impl/Cargo.toml index 4238cbdab6..67f17808bf 100644 --- a/libdd-capabilities-impl/Cargo.toml +++ b/libdd-capabilities-impl/Cargo.toml @@ -20,6 +20,7 @@ bytes = "1" http = "1" libdd-capabilities = { path = "../libdd-capabilities", version = "1.0.0" } libdd-common = { path = "../libdd-common", version = "4.0.0", default-features = false } +tokio = { version = "1", features = ["time", "rt"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] http-body-util = "0.1" diff --git a/libdd-capabilities-impl/README.md b/libdd-capabilities-impl/README.md index 01b45384ec..47dcdacf7a 100644 --- a/libdd-capabilities-impl/README.md +++ b/libdd-capabilities-impl/README.md @@ -8,11 +8,13 @@ Native implementations of `libdd-capabilities` traits. ## Capabilities -- **`DefaultHttpClient`**: HTTP client backed by hyper and the `libdd-common` connector infrastructure (supports Unix sockets, HTTPS with rustls, Windows named pipes). +- **`NativeHttpClient`**: HTTP client backed by hyper and the `libdd-common` connector infrastructure (supports Unix sockets, HTTPS with rustls, Windows named pipes). +- **`NativeSleepCapability`**: Sleep backed by `tokio::time::sleep`. +- **`NativeSpawnCapability`**: Task spawning backed by `tokio::runtime::Handle::spawn`. ## Types -- **`NativeCapabilities`**: Bundle type alias that implements all capability traits using native backends. Currently delegates to `DefaultHttpClient`; as more capability traits are added (spawn, sleep, etc.), this type will implement all of them. +- **`NativeCapabilities`**: Bundle struct that implements all capability traits using native backends. Delegates to `NativeHttpClient`, `NativeSleepCapability`, and `NativeSpawnCapability`. ## Usage diff --git a/libdd-capabilities-impl/src/http.rs b/libdd-capabilities-impl/src/http.rs index b0ae9b3c75..a08b5a878e 100644 --- a/libdd-capabilities-impl/src/http.rs +++ b/libdd-capabilities-impl/src/http.rs @@ -4,7 +4,7 @@ //! Native HTTP client implementation backed by hyper. mod native { - use libdd_capabilities::http::{HttpClientTrait, HttpError}; + use libdd_capabilities::http::{HttpClientCapability, HttpError}; use libdd_capabilities::maybe_send::MaybeSend; use libdd_common::connector::Connector; use libdd_common::http_common::{new_default_client, Body, GenericHttpClient}; @@ -12,17 +12,17 @@ mod native { use http_body_util::BodyExt; #[derive(Clone)] - pub struct DefaultHttpClient { + pub struct NativeHttpClient { client: GenericHttpClient, } - impl std::fmt::Debug for DefaultHttpClient { + impl std::fmt::Debug for NativeHttpClient { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("DefaultHttpClient").finish() + f.debug_struct("NativeHttpClient").finish() } } - impl HttpClientTrait for DefaultHttpClient { + impl HttpClientCapability for NativeHttpClient { fn new_client() -> Self { Self { client: new_default_client(), @@ -57,4 +57,4 @@ mod native { } } -pub use native::DefaultHttpClient; +pub use native::NativeHttpClient; diff --git a/libdd-capabilities-impl/src/lib.rs b/libdd-capabilities-impl/src/lib.rs index 6b3f6cd959..4031fc0c2e 100644 --- a/libdd-capabilities-impl/src/lib.rs +++ b/libdd-capabilities-impl/src/lib.rs @@ -8,54 +8,82 @@ //! etc.). Leaf crates (FFI, benchmarks) pin this type as the generic parameter. mod http; +pub mod sleep; +pub mod spawn; -pub use libdd_capabilities::HttpClientTrait; - -#[cfg(not(target_arch = "wasm32"))] -pub use http::DefaultHttpClient; - -#[cfg(not(target_arch = "wasm32"))] -mod native { - use core::future::Future; - - use libdd_capabilities::http::HttpError; - use libdd_capabilities::MaybeSend; - - use super::DefaultHttpClient; - use super::HttpClientTrait; - - /// Bundle struct for native platform capabilities. - /// - /// Delegates to [`DefaultHttpClient`] for HTTP. As more capability traits are - /// added (spawn, sleep, etc.), additional fields and impls are added here - /// without changing the type identity — consumers see the same - /// `NativeCapabilities` throughout. - /// - /// Individual capability traits keep minimal per-function bounds (e.g. - /// functions that only need HTTP require just `H: HttpClientTrait`, not the - /// full bundle) so that native callers like the sidecar can use - /// `DefaultHttpClient` directly without pulling in this bundle. - #[derive(Clone, Debug)] - pub struct NativeCapabilities { - http: DefaultHttpClient, +use core::future::Future; +use std::time::Duration; + +pub use http::NativeHttpClient; +use libdd_capabilities::{http::HttpError, MaybeSend}; +pub use libdd_capabilities::{HttpClientCapability, SleepCapability, SpawnCapability}; +pub use sleep::NativeSleepCapability; +pub use spawn::{NativeJoinHandle, NativeSpawnCapability}; + +/// Bundle struct for native platform capabilities. +/// +/// Delegates to [`NativeHttpClient`] for HTTP, [`NativeSleepCapability`] for +/// sleep, and [`NativeSpawnCapability`] for task spawning. +/// +/// Individual capability traits keep minimal per-function bounds (e.g. +/// functions that only need HTTP require just `H: HttpClientCapability`, not the +/// full bundle) so that native callers like the sidecar can use +/// `NativeHttpClient` directly without pulling in this bundle. +#[derive(Clone, Debug)] +pub struct NativeCapabilities { + http: NativeHttpClient, + sleep: NativeSleepCapability, + spawn: NativeSpawnCapability, +} + +impl Default for NativeCapabilities { + fn default() -> Self { + Self::new() } +} - impl HttpClientTrait for NativeCapabilities { - fn new_client() -> Self { - Self { - http: DefaultHttpClient::new_client(), - } +impl NativeCapabilities { + pub fn new() -> Self { + Self { + http: NativeHttpClient::new_client(), + sleep: NativeSleepCapability, + spawn: NativeSpawnCapability, } + } +} - fn request( - &self, - req: ::http::Request, - ) -> impl Future, HttpError>> + MaybeSend - { - self.http.request(req) +impl HttpClientCapability for NativeCapabilities { + fn new_client() -> Self { + Self { + http: NativeHttpClient::new_client(), + sleep: NativeSleepCapability, + spawn: NativeSpawnCapability, } } + + fn request( + &self, + req: ::http::Request, + ) -> impl Future, HttpError>> + MaybeSend { + self.http.request(req) + } +} + +impl SleepCapability for NativeCapabilities { + fn sleep(&self, duration: Duration) -> impl Future + MaybeSend { + self.sleep.sleep(duration) + } } -#[cfg(not(target_arch = "wasm32"))] -pub use native::NativeCapabilities; +impl SpawnCapability for NativeCapabilities { + type RuntimeContext = tokio::runtime::Handle; + type JoinHandle = NativeJoinHandle; + + fn spawn(&self, future: F, ctx: &tokio::runtime::Handle) -> NativeJoinHandle + where + F: Future + MaybeSend + 'static, + T: MaybeSend + 'static, + { + self.spawn.spawn(future, ctx) + } +} diff --git a/libdd-capabilities-impl/src/sleep.rs b/libdd-capabilities-impl/src/sleep.rs new file mode 100644 index 0000000000..2510e335ba --- /dev/null +++ b/libdd-capabilities-impl/src/sleep.rs @@ -0,0 +1,19 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Native sleep implementation backed by `tokio::time::sleep`. + +use core::future::Future; +use std::time::Duration; + +use libdd_capabilities::maybe_send::MaybeSend; +use libdd_capabilities::sleep::SleepCapability; + +#[derive(Clone, Debug)] +pub struct NativeSleepCapability; + +impl SleepCapability for NativeSleepCapability { + fn sleep(&self, duration: Duration) -> impl Future + MaybeSend { + tokio::time::sleep(duration) + } +} diff --git a/libdd-capabilities-impl/src/spawn.rs b/libdd-capabilities-impl/src/spawn.rs new file mode 100644 index 0000000000..be71879e49 --- /dev/null +++ b/libdd-capabilities-impl/src/spawn.rs @@ -0,0 +1,45 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Native spawn implementation backed by `tokio::runtime::Handle::spawn`. + +use core::future::Future; +use core::pin::Pin; +use core::task::{Context, Poll}; + +use libdd_capabilities::maybe_send::MaybeSend; +use libdd_capabilities::spawn::{SpawnCapability, SpawnError}; +use tokio::task::JoinHandle; + +#[derive(Clone, Debug)] +pub struct NativeSpawnCapability; + +impl SpawnCapability for NativeSpawnCapability { + type RuntimeContext = tokio::runtime::Handle; + type JoinHandle = NativeJoinHandle; + + fn spawn(&self, future: F, ctx: &tokio::runtime::Handle) -> NativeJoinHandle + where + F: Future + MaybeSend + 'static, + T: MaybeSend + 'static, + { + NativeJoinHandle(ctx.spawn(future)) + } +} + +/// Newtype wrapping `tokio::task::JoinHandle` that surfaces +/// `Result`, mapping tokio's `JoinError` (panic / abort) +/// into the executor-agnostic [`SpawnError`] from `libdd-capabilities`. +pub struct NativeJoinHandle(JoinHandle); + +impl Future for NativeJoinHandle { + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match Pin::new(&mut self.get_mut().0).poll(cx) { + Poll::Ready(Ok(val)) => Poll::Ready(Ok(val)), + Poll::Ready(Err(e)) => Poll::Ready(Err(SpawnError::new(e.to_string()))), + Poll::Pending => Poll::Pending, + } + } +} diff --git a/libdd-capabilities/README.md b/libdd-capabilities/README.md index aff709d27a..c93fa7c19c 100644 --- a/libdd-capabilities/README.md +++ b/libdd-capabilities/README.md @@ -10,7 +10,7 @@ This crate has **zero platform dependencies**: it compiles on any target includi ## Traits -- **`HttpClientTrait`**: Async HTTP request/response using `http::Request` / `http::Response`. +- **`HttpClientCapability`**: Async HTTP request/response using `http::Request` / `http::Response`. - **`MaybeSend`**: Conditional `Send` bound: equivalent to `Send` on native, auto-implemented for all types on wasm. This bridges the gap between tokio's multi-threaded runtime (requires `Send` futures) and wasm's single-threaded model (where JS interop types are `!Send`). ## Architecture @@ -18,15 +18,15 @@ This crate has **zero platform dependencies**: it compiles on any target includi Three-layer design: 1. **Trait definitions** (this crate): Pure traits, no platform deps. -2. **Core crates** (`libdd-trace-utils`, `libdd-data-pipeline`): Generic over `C: HttpClientTrait`. Depend only on this crate for trait bounds. +2. **Core crates** (`libdd-trace-utils`, `libdd-data-pipeline`): Generic over `C: HttpClientCapability`. Depend only on this crate for trait bounds. 3. **Leaf crates** (FFI, wasm bindings): Pin a concrete type, `NativeCapabilities` from `libdd-capabilities-impl` on native, `WasmCapabilities` from the Node.js binding crate on wasm. ## Usage ```rust -use libdd_capabilities::{HttpClientTrait, MaybeSend}; +use libdd_capabilities::{HttpClientCapability, MaybeSend}; -async fn fetch(client: &C, req: http::Request) { +async fn fetch(client: &C, req: http::Request) { let response = client.request(req).await.unwrap(); println!("status: {}", response.status()); } diff --git a/libdd-capabilities/src/http.rs b/libdd-capabilities/src/http.rs index 98dab26abf..f4451acc1a 100644 --- a/libdd-capabilities/src/http.rs +++ b/libdd-capabilities/src/http.rs @@ -24,7 +24,7 @@ pub enum HttpError { Other(anyhow::Error), } -pub trait HttpClientTrait: Clone + std::fmt::Debug { +pub trait HttpClientCapability: Clone + std::fmt::Debug { fn new_client() -> Self; fn request( diff --git a/libdd-capabilities/src/lib.rs b/libdd-capabilities/src/lib.rs index 5e10d9fc88..c6e21f5dce 100644 --- a/libdd-capabilities/src/lib.rs +++ b/libdd-capabilities/src/lib.rs @@ -5,8 +5,12 @@ pub mod http; pub mod maybe_send; +pub mod sleep; +pub mod spawn; -pub use self::http::{HttpClientTrait, HttpError}; +pub use self::http::{HttpClientCapability, HttpError}; +pub use self::sleep::SleepCapability; +pub use self::spawn::{SpawnCapability, SpawnError}; pub use ::http::{Request, Response}; pub use bytes::Bytes; pub use maybe_send::MaybeSend; diff --git a/libdd-capabilities/src/sleep.rs b/libdd-capabilities/src/sleep.rs new file mode 100644 index 0000000000..9486193f12 --- /dev/null +++ b/libdd-capabilities/src/sleep.rs @@ -0,0 +1,15 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Sleep capability trait. +//! +//! Abstracts async sleep so that native code can use `tokio::time::sleep` +//! while wasm delegates to `setTimeout` via `JsFuture`. + +use crate::maybe_send::MaybeSend; +use core::future::Future; +use std::time::Duration; + +pub trait SleepCapability: Clone + std::fmt::Debug { + fn sleep(&self, duration: Duration) -> impl Future + MaybeSend; +} diff --git a/libdd-capabilities/src/spawn.rs b/libdd-capabilities/src/spawn.rs new file mode 100644 index 0000000000..8076306a90 --- /dev/null +++ b/libdd-capabilities/src/spawn.rs @@ -0,0 +1,52 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Spawn capability trait. +//! +//! Abstracts task spawning so that native code can use `tokio::spawn` +//! while wasm delegates to `wasm_bindgen_futures::spawn_local` with a +//! `RemoteHandle` for join/cancel semantics. + +use crate::maybe_send::MaybeSend; +use core::fmt; +use core::future::Future; + +/// Executor-agnostic error returned when a spawned task is aborted or panics. +#[derive(Debug)] +pub struct SpawnError { + msg: String, +} + +impl SpawnError { + pub fn new(msg: impl Into) -> Self { + Self { msg: msg.into() } + } +} + +impl fmt::Display for SpawnError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "spawned task failed: {}", self.msg) + } +} + +impl core::error::Error for SpawnError {} + +pub trait SpawnCapability: Clone + std::fmt::Debug { + /// Platform-specific context passed to [`spawn`](Self::spawn). + /// + /// On native this is typically `tokio::runtime::Handle` — the spawner uses + /// it to schedule the future on the correct runtime. On wasm this is `()` + /// because `spawn_local` does not need an external handle. + type RuntimeContext; + + /// Handle to a spawned task. + /// + /// Awaiting the handle yields `Ok(T)` on success, or `Err(SpawnError)` if + /// the task panicked or was aborted. + type JoinHandle: Future> + MaybeSend; + + fn spawn(&self, future: F, ctx: &Self::RuntimeContext) -> Self::JoinHandle + where + F: Future + MaybeSend + 'static, + T: MaybeSend + 'static; +} diff --git a/libdd-data-pipeline/benches/trace_buffer.rs b/libdd-data-pipeline/benches/trace_buffer.rs index 5f180a28f1..634542ee34 100644 --- a/libdd-data-pipeline/benches/trace_buffer.rs +++ b/libdd-data-pipeline/benches/trace_buffer.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use std::time::Duration; use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use libdd_capabilities_impl::NativeCapabilities; use libdd_data_pipeline::trace_buffer::{Export, TraceBuffer, TraceBufferConfig, TraceChunk}; use libdd_data_pipeline::trace_exporter::{ agent_response::AgentResponse, error::TraceExporterError, @@ -44,7 +45,8 @@ fn setup_buffer() -> (Arc, Arc>) { .span_flush_threshold(500) .max_flush_interval(Duration::from_secs(2)); let (buf, worker) = TraceBuffer::new(cfg, Box::new(|_| {}), Box::new(SleepExport)); - rt.spawn_worker(worker, true).expect("spawn_worker"); + rt.spawn_worker(worker, true, &NativeCapabilities::new()) + .expect("spawn_worker"); (rt, Arc::new(buf)) } diff --git a/libdd-data-pipeline/src/agent_info/fetcher.rs b/libdd-data-pipeline/src/agent_info/fetcher.rs index 41a80364bd..87fbc8b4be 100644 --- a/libdd-data-pipeline/src/agent_info/fetcher.rs +++ b/libdd-data-pipeline/src/agent_info/fetcher.rs @@ -10,7 +10,7 @@ use super::{ use anyhow::{anyhow, Result}; use async_trait::async_trait; use bytes::Bytes; -use libdd_capabilities::{HttpClientTrait, MaybeSend}; +use libdd_capabilities::{HttpClientCapability, MaybeSend}; use libdd_common::Endpoint; use libdd_shared_runtime::Worker; use sha2::{Digest, Sha256}; @@ -35,7 +35,7 @@ pub enum FetchInfoStatus { /// If either the agent state hash or container tags hash is different from the current one: /// - Return a `FetchInfoStatus::NewState` of the info struct /// - Else return `FetchInfoStatus::SameState` -async fn fetch_info_with_state_and_container_tags( +async fn fetch_info_with_state_and_container_tags( info_endpoint: &Endpoint, current_state_hash: Option<&str>, current_container_tags_hash: Option<&str>, @@ -65,7 +65,7 @@ async fn fetch_info_with_state_and_container_tags( /// If the state hash is different from the current one: /// - Return a `FetchInfoStatus::NewState` of the info struct /// - Else return `FetchInfoStatus::SameState` -pub async fn fetch_info_with_state( +pub async fn fetch_info_with_state( info_endpoint: &Endpoint, current_state_hash: Option<&str>, ) -> Result { @@ -93,7 +93,9 @@ pub async fn fetch_info_with_state( /// # Ok(()) /// # } /// ``` -pub async fn fetch_info(info_endpoint: &Endpoint) -> Result> { +pub async fn fetch_info( + info_endpoint: &Endpoint, +) -> Result> { match fetch_info_with_state::(info_endpoint, None).await? { FetchInfoStatus::NewState(info) => Ok(info), // Should never be reached since there is no previous state. @@ -105,7 +107,7 @@ pub async fn fetch_info(info_endpoint: &Endpoint) -> Result< /// /// Returns a tuple of (state_hash, response_body_bytes, container_tags_hash). /// The hash is calculated using SHA256 to match the agent's calculation method. -async fn fetch_and_hash_response( +async fn fetch_and_hash_response( info_endpoint: &Endpoint, ) -> Result<(String, bytes::Bytes, Option)> { let req = info_endpoint @@ -149,13 +151,14 @@ async fn fetch_and_hash_response( /// # Example /// ```no_run /// # use anyhow::Result; -/// # use libdd_capabilities_impl::NativeCapabilities; +/// # use libdd_capabilities_impl::{HttpClientCapability, NativeCapabilities}; /// # use libdd_shared_runtime::Worker; /// # #[tokio::main] /// # async fn main() -> Result<()> { /// // Define the endpoint /// use libdd_data_pipeline::agent_info; /// let endpoint = libdd_common::Endpoint::from_url("http://localhost:8126/info".parse().unwrap()); +/// let capabilities = NativeCapabilities::new_client(); /// // Create the fetcher /// let (mut fetcher, _response_observer) = libdd_data_pipeline::agent_info::AgentInfoFetcher::< /// NativeCapabilities, @@ -164,7 +167,7 @@ async fn fetch_and_hash_response( /// ); /// // Start the fetcher on a shared runtime /// let runtime = libdd_shared_runtime::SharedRuntime::new()?; -/// runtime.spawn_worker(fetcher, true)?; +/// runtime.spawn_worker(fetcher, true, &capabilities)?; /// /// // Get the Arc to access the info /// let agent_info_arc = agent_info::get_agent_info(); @@ -179,10 +182,10 @@ async fn fetch_and_hash_response( /// # Ok(()) /// # } /// ``` -/// `H` is the HTTP client implementation, see [`HttpClientTrait`]. Leaf crates +/// `H` is the HTTP client implementation, see [`HttpClientCapability`]. Leaf crates /// pin it to a concrete type. #[derive(Debug)] -pub struct AgentInfoFetcher { +pub struct AgentInfoFetcher { info_endpoint: Endpoint, refresh_interval: Duration, trigger_rx: Option>, @@ -192,7 +195,7 @@ pub struct AgentInfoFetcher { _phantom: PhantomData, } -impl AgentInfoFetcher { +impl AgentInfoFetcher { /// Return a new `AgentInfoFetcher` fetching the `info_endpoint` on each `refresh_interval` /// and updating the stored info. /// @@ -227,7 +230,7 @@ impl AgentInfoFetcher { #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl Worker for AgentInfoFetcher { +impl Worker for AgentInfoFetcher { async fn initial_trigger(&mut self) { // Skip initial wait if cache is not populated if AGENT_INFO_CACHE.load().is_none() { @@ -274,7 +277,7 @@ impl Worker for AgentInfoFetche } } -impl AgentInfoFetcher { +impl AgentInfoFetcher { /// Fetch agent info and update cache if needed async fn fetch_and_update(&self) { let current_info = AGENT_INFO_CACHE.load(); @@ -577,7 +580,10 @@ mod single_threaded_tests { ); assert!(agent_info::get_agent_info().is_none()); let shared_runtime = SharedRuntime::new().unwrap(); - shared_runtime.spawn_worker(fetcher, true).unwrap(); + let spawner = NativeCapabilities::new(); + shared_runtime + .spawn_worker(fetcher, true, &spawner) + .unwrap(); // Wait until the info is fetched let start = std::time::Instant::now(); @@ -660,7 +666,10 @@ mod single_threaded_tests { AgentInfoFetcher::::new(endpoint, Duration::from_secs(3600)); let shared_runtime = SharedRuntime::new().unwrap(); - shared_runtime.spawn_worker(fetcher, true).unwrap(); + let spawner = NativeCapabilities::new(); + shared_runtime + .spawn_worker(fetcher, true, &spawner) + .unwrap(); // Create a mock HTTP response with the new agent state let response = http::Response::builder() @@ -741,7 +750,10 @@ mod single_threaded_tests { AgentInfoFetcher::::new(endpoint, Duration::from_secs(3600)); // Very long interval let shared_runtime = SharedRuntime::new().unwrap(); - shared_runtime.spawn_worker(fetcher, true).unwrap(); + let spawner = NativeCapabilities::new(); + shared_runtime + .spawn_worker(fetcher, true, &spawner) + .unwrap(); // Create a mock HTTP response with the same agent state let response = http::Response::builder() diff --git a/libdd-data-pipeline/src/otlp/exporter.rs b/libdd-data-pipeline/src/otlp/exporter.rs index 8cb13a8cae..08c849392e 100644 --- a/libdd-data-pipeline/src/otlp/exporter.rs +++ b/libdd-data-pipeline/src/otlp/exporter.rs @@ -5,7 +5,7 @@ use super::config::OtlpTraceConfig; use crate::trace_exporter::error::{InternalErrorKind, RequestError, TraceExporterError}; -use libdd_capabilities::HttpClientTrait; +use libdd_capabilities::{HttpClientCapability, SleepCapability}; use libdd_common::Endpoint; use libdd_trace_utils::send_with_retry::{ send_with_retry, RetryBackoffType, RetryStrategy, SendWithRetryError, @@ -22,8 +22,8 @@ const OTLP_RETRY_DELAY_MS: u64 = 100; /// /// `test_token` is forwarded as `X-Datadog-Test-Session-Token` when set, enabling snapshot tests /// against the Datadog test agent's OTLP endpoint. -pub async fn send_otlp_traces_http( - client: &H, +pub async fn send_otlp_traces_http( + capabilities: &C, config: &OtlpTraceConfig, test_token: Option<&str>, json_body: Vec, @@ -62,7 +62,7 @@ pub async fn send_otlp_traces_http( None, ); - match send_with_retry(client, &target, json_body, &headers, &retry_strategy).await { + match send_with_retry(capabilities, &target, json_body, &headers, &retry_strategy).await { Ok(_) => Ok(()), Err(e) => Err(map_send_error(e).await), } diff --git a/libdd-data-pipeline/src/telemetry/mod.rs b/libdd-data-pipeline/src/telemetry/mod.rs index 1676665614..0cfe5c6b2a 100644 --- a/libdd-data-pipeline/src/telemetry/mod.rs +++ b/libdd-data-pipeline/src/telemetry/mod.rs @@ -328,6 +328,7 @@ mod tests { use httpmock::Method::POST; use httpmock::MockServer; use libdd_capabilities::HttpError; + use libdd_capabilities_impl::NativeCapabilities; use libdd_shared_runtime::{SharedRuntime, WorkerHandle}; use libdd_trace_utils::test_utils::poll_for_mock_hits; // Use `regex::Regex` directly here because `httpmock`'s `body_matches` @@ -349,8 +350,9 @@ mod tests { .set_heartbeat(100) .set_debug_enabled(true) .build(); + let spawner = NativeCapabilities::new(); let handle = runtime - .spawn_worker(worker, true) + .spawn_worker(worker, true, &spawner) .expect("Failed to spawn worker"); (client, handle) } diff --git a/libdd-data-pipeline/src/trace_buffer/mod.rs b/libdd-data-pipeline/src/trace_buffer/mod.rs index 95e7ee52e8..d6b169476a 100644 --- a/libdd-data-pipeline/src/trace_buffer/mod.rs +++ b/libdd-data-pipeline/src/trace_buffer/mod.rs @@ -13,7 +13,8 @@ use std::{ time::{Duration, Instant}, }; -use libdd_capabilities::{HttpClientTrait, MaybeSend}; +use libdd_capabilities::{HttpClientCapability, MaybeSend, SleepCapability, SpawnCapability}; +use libdd_shared_runtime::SpawnRuntimeContext; use libdd_shared_runtime::Worker; use crate::trace_exporter::{ @@ -568,18 +569,39 @@ pub trait Export: Send + Debug { } #[derive(Debug)] -pub struct DefaultExport { - trace_exporter: TraceExporter, -} - -impl DefaultExport { - pub fn new(trace_exporter: TraceExporter) -> Self { +pub struct DefaultExport< + C: HttpClientCapability + + SleepCapability + + SpawnCapability + + MaybeSend + + Sync + + 'static, +> { + trace_exporter: TraceExporter, +} + +impl< + C: HttpClientCapability + + SleepCapability + + SpawnCapability + + MaybeSend + + Sync + + 'static, + > DefaultExport +{ + pub fn new(trace_exporter: TraceExporter) -> Self { Self { trace_exporter } } } -impl - Export for DefaultExport +impl< + C: HttpClientCapability + + SleepCapability + + SpawnCapability + + MaybeSend + + Sync + + 'static, + > Export for DefaultExport { fn export_trace_chunks( &mut self, @@ -701,6 +723,7 @@ mod tests { use std::sync::Arc; use std::time::Duration; + use libdd_capabilities_impl::NativeCapabilities; use libdd_shared_runtime::SharedRuntime; use crate::trace_buffer::{Export, TraceBuffer, TraceBufferConfig}; @@ -754,7 +777,8 @@ mod tests { ), Box::new(AssertExporter(assert_export, sem.clone())), ); - rt.spawn_worker(worker, true).unwrap(); + rt.spawn_worker(worker, true, &NativeCapabilities::new()) + .unwrap(); (rt, sem, sender) } diff --git a/libdd-data-pipeline/src/trace_exporter/builder.rs b/libdd-data-pipeline/src/trace_exporter/builder.rs index fbff2016b1..eba1418b53 100644 --- a/libdd-data-pipeline/src/trace_exporter/builder.rs +++ b/libdd-data-pipeline/src/trace_exporter/builder.rs @@ -4,11 +4,11 @@ use crate::agent_info::AgentInfoFetcher; use crate::otlp::config::{OtlpProtocol, DEFAULT_OTLP_TIMEOUT}; use crate::otlp::OtlpTraceConfig; -#[cfg(feature = "telemetry")] +#[cfg(all(not(target_arch = "wasm32"), feature = "telemetry"))] use crate::telemetry::TelemetryClientBuilder; use crate::trace_exporter::agent_response::AgentResponsePayloadVersion; use crate::trace_exporter::error::BuilderErrorKind; -#[cfg(feature = "telemetry")] +#[cfg(all(not(target_arch = "wasm32"), feature = "telemetry"))] use crate::trace_exporter::TelemetryConfig; #[cfg(not(target_arch = "wasm32"))] use crate::trace_exporter::TraceExporterWorkers; @@ -18,10 +18,10 @@ use crate::trace_exporter::{ TracerMetadata, INFO_ENDPOINT, }; use arc_swap::ArcSwap; -use libdd_capabilities::{HttpClientTrait, MaybeSend}; +use libdd_capabilities::{HttpClientCapability, MaybeSend, SleepCapability, SpawnCapability}; use libdd_common::{parse_uri, tag, Endpoint}; use libdd_dogstatsd_client::new; -use libdd_shared_runtime::SharedRuntime; +use libdd_shared_runtime::{SharedRuntime, SpawnRuntimeContext}; use std::sync::Arc; use std::time::Duration; @@ -274,9 +274,16 @@ impl TraceExporterBuilder { } #[allow(missing_docs)] - pub fn build( + pub fn build< + C: HttpClientCapability + + SleepCapability + + SpawnCapability + + MaybeSend + + Sync + + 'static, + >( self, - ) -> Result, TraceExporterError> { + ) -> Result, TraceExporterError> { if !Self::is_inputs_outputs_formats_compatible(self.input_format, self.output_format) { return Err(TraceExporterError::Builder( BuilderErrorKind::InvalidConfiguration( @@ -305,230 +312,177 @@ impl TraceExporterBuilder { })?; let libdatadog_version = tag!("libdatadog_version", env!("CARGO_PKG_VERSION")); + + // On native, `C::new_client()` may capture `tokio::runtime::Handle::current()` + // internally (e.g. `NativeCapabilities`). Enter the SharedRuntime's tokio context + // so that handle is available. On wasm this is a no-op — the JS event loop is + // always the implicit executor. + #[cfg(not(target_arch = "wasm32"))] + let _guard = shared_runtime + .runtime_handle() + .map_err(|e| { + TraceExporterError::Builder(BuilderErrorKind::InvalidConfiguration(e.to_string())) + })? + .enter(); + let capabilities = C::new_client(); + + // --- Platform-specific worker setup --- + // The blocks below spawn background workers on native and create + // lightweight stubs on wasm. The `#[cfg]` interleaving is inherent to + // the platform split; each block is kept small to stay readable. + + let info_endpoint = Endpoint::from_url(add_path(&agent_url, INFO_ENDPOINT)); + #[cfg(not(target_arch = "wasm32"))] + let (info_fetcher_handle, info_response_observer) = { + let (info_fetcher, observer) = + AgentInfoFetcher::::new(info_endpoint.clone(), Duration::from_secs(5 * 60)); + let handle = shared_runtime + .spawn_worker(info_fetcher, false, &capabilities) + .map_err(|e| { + TraceExporterError::Builder(BuilderErrorKind::InvalidConfiguration( + e.to_string(), + )) + })?; + (handle, observer) + }; + // On wasm the AgentInfoFetcher is not spawned yet (it requires spawn_local), + // but we still need the ResponseObserver for header checks. + #[cfg(target_arch = "wasm32")] + let (_info_fetcher, info_response_observer) = + AgentInfoFetcher::::new(info_endpoint, Duration::from_secs(5 * 60)); + #[allow(unused_mut)] let mut stats = StatsComputationStatus::Disabled; - #[cfg(not(target_arch = "wasm32"))] - { - let info_endpoint = Endpoint::from_url(add_path(&agent_url, INFO_ENDPOINT)); - let (info_fetcher, info_response_observer) = - AgentInfoFetcher::::new(info_endpoint.clone(), Duration::from_secs(5 * 60)); - let info_fetcher_handle = - shared_runtime - .spawn_worker(info_fetcher, false) - .map_err(|e| { + if let Some(bucket_size) = self.stats_bucket_size { + stats = StatsComputationStatus::DisabledByAgent { bucket_size }; + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "telemetry"))] + let (telemetry_client, telemetry_handle) = { + let sessions = self.telemetry_instrumentation_sessions; + let telemetry = self.telemetry.map(|telemetry_config| { + let mut builder = TelemetryClientBuilder::default() + .set_language(&self.language) + .set_language_version(&self.language_version) + .set_service_name(&self.service) + .set_service_version(&self.app_version) + .set_env(&self.env) + .set_tracer_version(&self.tracer_version) + .set_heartbeat(telemetry_config.heartbeat) + .set_url(base_url) + .set_debug_enabled(telemetry_config.debug_enabled); + if let Some(id) = telemetry_config.runtime_id { + builder = builder.set_runtime_id(&id); + } + if let Some(ref id) = sessions.session_id { + builder = builder.set_session_id(id); + } + if let Some(ref id) = sessions.root_session_id { + builder = builder.set_root_session_id(id); + } + if let Some(ref id) = sessions.parent_session_id { + builder = builder.set_parent_session_id(id); + } + Ok(builder.build()) + }); + match telemetry { + Some(Ok((client_tel, worker))) => { + let handle = shared_runtime + .spawn_worker(worker, false, &capabilities) + .map_err(|e| { + TraceExporterError::Builder(BuilderErrorKind::InvalidConfiguration( + e.to_string(), + )) + })?; + shared_runtime.block_on(client_tel.start()).map_err(|e| { TraceExporterError::Builder(BuilderErrorKind::InvalidConfiguration( e.to_string(), )) })?; - - if let Some(bucket_size) = self.stats_bucket_size { - stats = StatsComputationStatus::DisabledByAgent { bucket_size }; + (Some(client_tel), Some(handle)) + } + Some(Err(e)) => return Err(e), + None => (None, None), } + }; - #[cfg(feature = "telemetry")] - let (telemetry_client, telemetry_handle) = { - let sessions = self.telemetry_instrumentation_sessions; - let telemetry = self.telemetry.map(|telemetry_config| { - let mut builder = TelemetryClientBuilder::default() - .set_language(&self.language) - .set_language_version(&self.language_version) - .set_service_name(&self.service) - .set_service_version(&self.app_version) - .set_env(&self.env) - .set_tracer_version(&self.tracer_version) - .set_heartbeat(telemetry_config.heartbeat) - .set_url(base_url) - .set_debug_enabled(telemetry_config.debug_enabled); - if let Some(id) = telemetry_config.runtime_id { - builder = builder.set_runtime_id(&id); + let otlp_config = self.otlp_endpoint.map(|url| { + let mut headers = http::HeaderMap::new(); + for (key, value) in self.otlp_headers { + match ( + http::HeaderName::from_bytes(key.as_bytes()), + http::HeaderValue::from_str(&value), + ) { + (Ok(name), Ok(val)) => { + headers.insert(name, val); } - if let Some(ref id) = sessions.session_id { - builder = builder.set_session_id(id); + _ => { + tracing::warn!("Skipping invalid OTLP header: {:?}={:?}", key, value); } - if let Some(ref id) = sessions.root_session_id { - builder = builder.set_root_session_id(id); - } - if let Some(ref id) = sessions.parent_session_id { - builder = builder.set_parent_session_id(id); - } - Ok(builder.build()) - }); - match telemetry { - Some(Ok((client, worker))) => { - let handle = shared_runtime.spawn_worker(worker, false).map_err(|e| { - TraceExporterError::Builder(BuilderErrorKind::InvalidConfiguration( - e.to_string(), - )) - })?; - shared_runtime.block_on(client.start()).map_err(|e| { - TraceExporterError::Builder(BuilderErrorKind::InvalidConfiguration( - e.to_string(), - )) - })?; - (Some(client), Some(handle)) - } - Some(Err(e)) => return Err(e), - None => (None, None), } - }; - - Ok(TraceExporter { - endpoint: Endpoint { - url: agent_url, - test_token: self.test_session_token.map(|token| token.into()), - timeout_ms: self - .connection_timeout - .unwrap_or(Endpoint::default().timeout_ms), - ..Default::default() - }, - metadata: TracerMetadata { - tracer_version: self.tracer_version, - language_version: self.language_version, - language_interpreter: self.language_interpreter, - language_interpreter_vendor: self.language_interpreter_vendor, - language: self.language, - git_commit_sha: self.git_commit_sha, - process_tags: self.process_tags, - client_computed_stats: self.client_computed_stats, - client_computed_top_level: self.client_computed_top_level, - hostname: self.hostname, - env: self.env, - app_version: self.app_version, - runtime_id: uuid::Uuid::new_v4().to_string(), - service: self.service, - }, - input_format: self.input_format, - output_format: self.output_format, - serializer: TraceSerializer::new(self.output_format), - client_computed_top_level: self.client_computed_top_level, - shared_runtime, - dogstatsd, - common_stats_tags: vec![libdatadog_version], - client_side_stats: ArcSwap::new(stats.into()), - previous_info_state: arc_swap::ArcSwapOption::new(None), - info_response_observer, - #[cfg(feature = "telemetry")] - telemetry: telemetry_client, - health_metrics_enabled: self.health_metrics_enabled, - client: H::new_client(), - workers: TraceExporterWorkers { - info_fetcher: info_fetcher_handle, - #[cfg(feature = "telemetry")] - telemetry: telemetry_handle, - }, - agent_payload_response_version: self - .agent_rates_payload_version_enabled - .then(AgentResponsePayloadVersion::new), - otlp_config: self.otlp_endpoint.map(|url| { - let mut headers = http::HeaderMap::new(); - for (key, value) in self.otlp_headers { - match ( - http::HeaderName::from_bytes(key.as_bytes()), - http::HeaderValue::from_str(&value), - ) { - (Ok(name), Ok(val)) => { - headers.insert(name, val); - } - _ => { - tracing::warn!( - "Skipping invalid OTLP header: {:?}={:?}", - key, - value - ); - } - } - } - OtlpTraceConfig { - endpoint_url: url, - headers, - timeout: self - .connection_timeout - .map(Duration::from_millis) - .unwrap_or(DEFAULT_OTLP_TIMEOUT), - protocol: OtlpProtocol::HttpJson, - } - }), - }) - } + } + OtlpTraceConfig { + endpoint_url: url, + headers, + timeout: self + .connection_timeout + .map(Duration::from_millis) + .unwrap_or(DEFAULT_OTLP_TIMEOUT), + protocol: OtlpProtocol::HttpJson, + } + }); - #[cfg(target_arch = "wasm32")] - { - let info_endpoint = Endpoint::from_url(add_path(&agent_url, INFO_ENDPOINT)); - let (_info_fetcher, info_response_observer) = - AgentInfoFetcher::::new(info_endpoint, Duration::from_secs(5 * 60)); - - Ok(TraceExporter { - endpoint: Endpoint { - url: agent_url, - test_token: self.test_session_token.map(|token| token.into()), - timeout_ms: self - .connection_timeout - .unwrap_or(Endpoint::default().timeout_ms), - ..Default::default() - }, - metadata: TracerMetadata { - tracer_version: self.tracer_version, - language_version: self.language_version, - language_interpreter: self.language_interpreter, - language_interpreter_vendor: self.language_interpreter_vendor, - language: self.language, - git_commit_sha: self.git_commit_sha, - process_tags: self.process_tags, - client_computed_stats: self.client_computed_stats, - client_computed_top_level: self.client_computed_top_level, - hostname: self.hostname, - env: self.env, - app_version: self.app_version, - runtime_id: uuid::Uuid::new_v4().to_string(), - service: self.service, - }, - input_format: self.input_format, - output_format: self.output_format, - serializer: TraceSerializer::new(self.output_format), + Ok(TraceExporter { + endpoint: Endpoint { + url: agent_url, + test_token: self.test_session_token.map(|token| token.into()), + timeout_ms: self + .connection_timeout + .unwrap_or(Endpoint::default().timeout_ms), + ..Default::default() + }, + metadata: TracerMetadata { + tracer_version: self.tracer_version, + language_version: self.language_version, + language_interpreter: self.language_interpreter, + language_interpreter_vendor: self.language_interpreter_vendor, + language: self.language, + git_commit_sha: self.git_commit_sha, + process_tags: self.process_tags, + client_computed_stats: self.client_computed_stats, client_computed_top_level: self.client_computed_top_level, - shared_runtime, - dogstatsd, - common_stats_tags: vec![libdatadog_version], - client_side_stats: ArcSwap::new(stats.into()), - previous_info_state: arc_swap::ArcSwapOption::new(None), - info_response_observer, - health_metrics_enabled: self.health_metrics_enabled, - client: H::new_client(), - agent_payload_response_version: self - .agent_rates_payload_version_enabled - .then(AgentResponsePayloadVersion::new), - otlp_config: self.otlp_endpoint.map(|url| { - let mut headers = http::HeaderMap::new(); - for (key, value) in self.otlp_headers { - match ( - http::HeaderName::from_bytes(key.as_bytes()), - http::HeaderValue::from_str(&value), - ) { - (Ok(name), Ok(val)) => { - headers.insert(name, val); - } - _ => { - tracing::warn!( - "Skipping invalid OTLP header: {:?}={:?}", - key, - value - ); - } - } - } - OtlpTraceConfig { - endpoint_url: url, - headers, - timeout: self - .connection_timeout - .map(Duration::from_millis) - .unwrap_or(DEFAULT_OTLP_TIMEOUT), - protocol: OtlpProtocol::HttpJson, - } - }), - }) - } + hostname: self.hostname, + env: self.env, + app_version: self.app_version, + runtime_id: uuid::Uuid::new_v4().to_string(), + service: self.service, + }, + input_format: self.input_format, + output_format: self.output_format, + serializer: TraceSerializer::new(self.output_format), + client_computed_top_level: self.client_computed_top_level, + shared_runtime, + dogstatsd, + common_stats_tags: vec![libdatadog_version], + client_side_stats: ArcSwap::new(stats.into()), + previous_info_state: arc_swap::ArcSwapOption::new(None), + info_response_observer, + #[cfg(all(not(target_arch = "wasm32"), feature = "telemetry"))] + telemetry: telemetry_client, + health_metrics_enabled: self.health_metrics_enabled, + capabilities, + #[cfg(not(target_arch = "wasm32"))] + workers: TraceExporterWorkers { + info_fetcher: info_fetcher_handle, + #[cfg(feature = "telemetry")] + telemetry: telemetry_handle, + }, + agent_payload_response_version: self + .agent_rates_payload_version_enabled + .then(AgentResponsePayloadVersion::new), + otlp_config, + }) } fn is_inputs_outputs_formats_compatible( diff --git a/libdd-data-pipeline/src/trace_exporter/mod.rs b/libdd-data-pipeline/src/trace_exporter/mod.rs index 71edd11f0a..07b594ce78 100644 --- a/libdd-data-pipeline/src/trace_exporter/mod.rs +++ b/libdd-data-pipeline/src/trace_exporter/mod.rs @@ -34,11 +34,11 @@ use bytes::Bytes; use http::header::HeaderMap; use http::uri::PathAndQuery; use http::Uri; -use libdd_capabilities::{HttpClientTrait, MaybeSend}; +use libdd_capabilities::{HttpClientCapability, MaybeSend, SleepCapability, SpawnCapability}; use libdd_common::tag::Tag; use libdd_common::Endpoint; use libdd_dogstatsd_client::Client; -use libdd_shared_runtime::{SharedRuntime, WorkerHandle}; +use libdd_shared_runtime::{SharedRuntime, SpawnRuntimeContext, WorkerHandle}; use libdd_trace_utils::msgpack_decoder; use libdd_trace_utils::send_with_retry::{ send_with_retry, RetryStrategy, SendWithRetryError, SendWithRetryResult, @@ -207,10 +207,17 @@ impl From for DeserInputFormat { } } -/// `H` is the HTTP client implementation, see [`HttpClientTrait`]. Leaf crates -/// pin it to a concrete type. +/// `C` is the capabilities bundle (HTTP, sleep, spawn). Leaf crates +/// pin it to a concrete type (`NativeCapabilities` or `WasmCapabilities`). #[derive(Debug)] -pub struct TraceExporter { +pub struct TraceExporter< + C: HttpClientCapability + + SleepCapability + + SpawnCapability + + MaybeSend + + Sync + + 'static, +> { endpoint: Endpoint, metadata: TracerMetadata, input_format: TraceExporterInputFormat, @@ -225,10 +232,10 @@ pub struct TraceExporter { #[cfg_attr(target_arch = "wasm32", allow(dead_code))] previous_info_state: ArcSwapOption, info_response_observer: ResponseObserver, - #[cfg(feature = "telemetry")] + #[cfg(all(not(target_arch = "wasm32"), feature = "telemetry"))] telemetry: Option, health_metrics_enabled: bool, - client: H, + capabilities: C, #[cfg(not(target_arch = "wasm32"))] workers: TraceExporterWorkers, agent_payload_response_version: Option, @@ -236,7 +243,15 @@ pub struct TraceExporter { otlp_config: Option, } -impl TraceExporter { +impl< + C: HttpClientCapability + + SleepCapability + + SpawnCapability + + MaybeSend + + Sync + + 'static, + > TraceExporter +{ #[allow(missing_docs)] pub fn builder() -> TraceExporterBuilder { TraceExporterBuilder::default() @@ -250,6 +265,7 @@ impl TraceExporter { /// # Errors /// Returns [`SharedRuntimeError::ShutdownTimedOut`] if a timeout was given and elapsed before /// all workers finished. + #[cfg(not(target_arch = "wasm32"))] pub fn shutdown(self, timeout: Option) -> Result<(), TraceExporterError> { let runtime = self.shared_runtime.clone(); if let Some(timeout) = timeout { @@ -313,6 +329,7 @@ impl TraceExporter { /// # Returns /// * Ok(AgentResponse): The response from the agent /// * Err(TraceExporterError): An error detailing what went wrong in the process + #[cfg(not(target_arch = "wasm32"))] pub fn send(&self, data: &[u8]) -> Result { self.check_agent_info(); @@ -390,7 +407,7 @@ impl TraceExporter { &ctx, &agent_info, &self.client_side_stats, - self.client.clone(), + self.capabilities.clone(), ); } StatsComputationStatus::Enabled { @@ -474,6 +491,7 @@ impl TraceExporter { /// # Returns /// * Ok(AgentResponse): The response from the agent (or Unchanged for OTLP) /// * Err(TraceExporterError): An error detailing what went wrong in the process + #[cfg(not(target_arch = "wasm32"))] pub fn send_trace_chunks( &self, trace_chunks: Vec>>, @@ -521,7 +539,7 @@ impl TraceExporter { TraceExporterError::Internal(InternalErrorKind::InvalidWorkerState(e.to_string())) })?; send_otlp_traces_http( - &self.client, + &self.capabilities, config, self.endpoint.test_token.as_deref(), json_body, @@ -531,6 +549,7 @@ impl TraceExporter { } /// Deserializes, processes and sends trace chunks to the agent + #[cfg(not(target_arch = "wasm32"))] fn send_deser( &self, data: &[u8], @@ -574,9 +593,16 @@ impl TraceExporter { let payload_len = mp_payload.len(); // Send traces to the agent - let result = send_with_retry(&self.client, endpoint, mp_payload, &headers, &strategy).await; + let result = send_with_retry( + &self.capabilities, + endpoint, + mp_payload, + &headers, + &strategy, + ) + .await; - #[cfg(feature = "telemetry")] + #[cfg(all(not(target_arch = "wasm32"), feature = "telemetry"))] if let Some(telemetry) = &self.telemetry { if let Err(e) = telemetry.send(&SendPayloadTelemetry::from_retry_result( &result, diff --git a/libdd-data-pipeline/src/trace_exporter/stats.rs b/libdd-data-pipeline/src/trace_exporter/stats.rs index 1fbaf82d0d..96584a6558 100644 --- a/libdd-data-pipeline/src/trace_exporter/stats.rs +++ b/libdd-data-pipeline/src/trace_exporter/stats.rs @@ -10,11 +10,11 @@ #[cfg(not(target_arch = "wasm32"))] use crate::agent_info::schema::AgentInfo; use arc_swap::ArcSwap; -use libdd_capabilities::{HttpClientTrait, MaybeSend}; +use libdd_capabilities::{HttpClientCapability, MaybeSend, SleepCapability, SpawnCapability}; #[cfg(not(target_arch = "wasm32"))] use libdd_common::Endpoint; use libdd_common::MutexExt; -use libdd_shared_runtime::{SharedRuntime, WorkerHandle}; +use libdd_shared_runtime::{SharedRuntime, SpawnRuntimeContext, WorkerHandle}; use libdd_trace_stats::span_concentrator::SpanConcentrator; #[cfg(not(target_arch = "wasm32"))] use libdd_trace_stats::stats_exporter::{StatsExporter, StatsMetadata}; @@ -71,12 +71,19 @@ fn get_span_kinds_for_stats(agent_info: &Arc) -> Vec { /// Start the stats exporter and enable stats computation /// /// Should only be used if the agent enabled stats computation -pub(crate) fn start_stats_computation( +pub(crate) fn start_stats_computation< + C: HttpClientCapability + + SleepCapability + + SpawnCapability + + MaybeSend + + Sync + + 'static, +>( ctx: &StatsContext, client_side_stats: &ArcSwap, span_kinds: Vec, peer_tags: Vec, - client: H, + capabilities: C, ) -> anyhow::Result<()> { if let StatsComputationStatus::DisabledByAgent { bucket_size } = **client_side_stats.load() { let stats_concentrator = Arc::new(Mutex::new(SpanConcentrator::new( @@ -90,7 +97,7 @@ pub(crate) fn start_stats_computation( +fn create_and_start_stats_worker< + C: HttpClientCapability + + SleepCapability + + SpawnCapability + + MaybeSend + + Sync + + 'static, +>( ctx: &StatsContext, bucket_size: Duration, stats_concentrator: &Arc>, client_side_stats: &ArcSwap, - client: H, + capabilities: C, ) -> anyhow::Result<()> { - let stats_exporter = StatsExporter::::new( + let stats_exporter = StatsExporter::::new( bucket_size, stats_concentrator.clone(), StatsMetadata::from(ctx.metadata.clone()), Endpoint::from_url(add_path(ctx.endpoint_url, STATS_ENDPOINT)), - client, + capabilities.clone(), ); let worker_handle = ctx .shared_runtime - .spawn_worker(stats_exporter, false) + .spawn_worker(stats_exporter, false, &capabilities) .map_err(|e| anyhow::anyhow!(e))?; // Update the stats computation state with the new worker components. @@ -153,11 +167,18 @@ pub(crate) fn stop_stats_computation( #[cfg(not(target_arch = "wasm32"))] /// Handle stats computation when agent changes from disabled to enabled -pub(crate) fn handle_stats_disabled_by_agent( +pub(crate) fn handle_stats_disabled_by_agent< + C: HttpClientCapability + + SleepCapability + + SpawnCapability + + MaybeSend + + Sync + + 'static, +>( ctx: &StatsContext, agent_info: &Arc, client_side_stats: &ArcSwap, - client: H, + capabilities: C, ) { if agent_info.info.client_drop_p0s.is_some_and(|v| v) { let status = start_stats_computation( @@ -165,7 +186,7 @@ pub(crate) fn handle_stats_disabled_by_agent debug!("Client-side stats enabled"), diff --git a/libdd-shared-runtime/src/lib.rs b/libdd-shared-runtime/src/lib.rs index e9dc0ee642..7cb7b83225 100644 --- a/libdd-shared-runtime/src/lib.rs +++ b/libdd-shared-runtime/src/lib.rs @@ -22,3 +22,16 @@ pub mod worker; // Top-level re-exports for convenience pub use shared_runtime::{SharedRuntime, SharedRuntimeError, WorkerHandle, WorkerHandleError}; pub use worker::Worker; + +/// The concrete [`SpawnCapability::RuntimeContext`] type for the current platform. +/// +/// On native this is `tokio::runtime::Handle`; on wasm it is `()`. +/// Generic code that calls [`SharedRuntime::spawn_worker`] should bound +/// `C: SpawnCapability` so the constraint +/// is satisfied on every target without `#[cfg]` on individual where clauses. +#[cfg(not(target_arch = "wasm32"))] +pub type SpawnRuntimeContext = tokio::runtime::Handle; + +/// See the non-wasm variant for documentation. +#[cfg(target_arch = "wasm32")] +pub type SpawnRuntimeContext = (); diff --git a/libdd-shared-runtime/src/shared_runtime/mod.rs b/libdd-shared-runtime/src/shared_runtime/mod.rs index 8fda5245b9..8bab3c37cd 100644 --- a/libdd-shared-runtime/src/shared_runtime/mod.rs +++ b/libdd-shared-runtime/src/shared_runtime/mod.rs @@ -14,12 +14,292 @@ use crate::worker::Worker; use futures::stream::{FuturesUnordered, StreamExt}; use libdd_common::MutexExt; use pausable_worker::{PausableWorker, PausableWorkerError}; -use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::atomic::AtomicU64; use std::sync::{Arc, Mutex}; use std::{fmt, io}; -use tokio::runtime::{Builder, Runtime}; use tracing::{debug, error}; +/// Native-only runtime management, fork safety, and tokio integration. +/// +/// Gated once here so individual items inside don't need `#[cfg]`. +#[cfg(not(target_arch = "wasm32"))] +mod native { + use super::*; + use core::pin::Pin; + use core::task::{Context, Poll}; + use libdd_capabilities::spawn::{SpawnCapability, SpawnError}; + use libdd_capabilities::MaybeSend; + use std::future::Future; + use std::sync::atomic::Ordering; + use tokio::runtime::{Builder, Runtime}; + + fn build_runtime() -> Result { + Builder::new_multi_thread() + .worker_threads(1) + .enable_all() + .build() + } + + /// Internal spawner for fork recovery paths. + /// + /// The runtime handle is passed at spawn time via [`SpawnCapability::RuntimeContext`], + /// not stored, so the spawner is always using the current (potentially rebuilt) runtime. + #[derive(Clone, Debug)] + pub(crate) struct RuntimeSpawner; + + pub(crate) struct RuntimeJoinHandle(tokio::task::JoinHandle); + + impl Future for RuntimeJoinHandle { + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match Pin::new(&mut self.get_mut().0).poll(cx) { + Poll::Ready(Ok(val)) => Poll::Ready(Ok(val)), + Poll::Ready(Err(e)) => Poll::Ready(Err(SpawnError::new(e.to_string()))), + Poll::Pending => Poll::Pending, + } + } + } + + impl SpawnCapability for RuntimeSpawner { + type RuntimeContext = tokio::runtime::Handle; + type JoinHandle = RuntimeJoinHandle; + + fn spawn(&self, future: F, ctx: &tokio::runtime::Handle) -> RuntimeJoinHandle + where + F: Future + MaybeSend + 'static, + T: MaybeSend + 'static, + { + RuntimeJoinHandle(ctx.spawn(future)) + } + } + + impl SharedRuntime { + pub(in super::super) fn new_native() -> Result { + Ok(Self { + runtime: Arc::new(Mutex::new(Some(Arc::new(build_runtime()?)))), + workers: Arc::new(Mutex::new(Vec::new())), + next_worker_id: AtomicU64::new(1), + }) + } + + /// Returns a clone of the tokio runtime handle managed by this SharedRuntime. + /// + /// # Errors + /// Returns [`SharedRuntimeError::RuntimeUnavailable`] if the runtime has been shut down. + pub fn runtime_handle(&self) -> Result { + Ok(self + .runtime + .lock_or_panic() + .as_ref() + .ok_or(SharedRuntimeError::RuntimeUnavailable)? + .handle() + .clone()) + } + + /// Spawn a PausableWorker using the provided spawn capability. + /// + /// The worker will be tracked by this SharedRuntime and will be paused/resumed + /// during fork operations (native only). + /// If `restart_on_fork` is true, the worker will be reset and restarted when calling + /// `after_fork_child` else the worker is dropped *without* calling `Worker::shutdown`. + /// + /// # Errors + /// Returns an error if the worker cannot be started. + pub fn spawn_worker< + T: Worker + Sync + 'static, + S: SpawnCapability + 'static, + >( + &self, + worker: T, + restart_on_fork: bool, + spawner: &S, + ) -> Result { + let boxed_worker: BoxedWorker = Box::new(worker); + debug!(?boxed_worker, "Spawning worker on SharedRuntime"); + let mut pausable_worker = PausableWorker::new(boxed_worker); + + // Lock runtime first to synchronize with before_fork (which does + // runtime.take() then workers.lock()), following the documented mutex + // lock order. If the runtime has been taken (fork window), skip starting + // the worker, after_fork_parent/child will start it on the new runtime. + let runtime_handle = self + .runtime + .lock_or_panic() + .as_ref() + .map(|rt| rt.handle().clone()); + let mut workers_guard = self.workers.lock_or_panic(); + + if let Some(ref handle) = runtime_handle { + if let Err(e) = pausable_worker.start(spawner, handle) { + return Err(e.into()); + } + } + + let worker_id = self.next_worker_id.fetch_add(1, Ordering::Relaxed); + + workers_guard.push(WorkerEntry { + id: worker_id, + restart_on_fork, + worker: pausable_worker, + }); + + Ok(WorkerHandle { + worker_id, + workers: self.workers.clone(), + }) + } + + /// Hook to be called before forking. + /// + /// This method pauses all workers and prepares the runtime for forking. + /// It ensures that no background tasks are running when the fork occurs, + /// preventing potential deadlocks in the child process. + /// + /// Worker errors are logged but do not cause the function to fail. + /// If the worker fails to pause it is dropped without calling shutdown. + pub fn before_fork(&self) { + debug!("before_fork: pausing all workers"); + if let Some(runtime) = self.runtime.lock_or_panic().take() { + let mut workers_lock = self.workers.lock_or_panic(); + runtime.block_on(async { + let futures: FuturesUnordered<_> = workers_lock + .iter_mut() + .map(|worker_entry| async { + if let Err(e) = worker_entry.worker.pause().await { + error!("Worker failed to pause before fork: {:?}", e); + } + }) + .collect(); + + futures.collect::<()>().await; + }); + } + } + + fn restart_runtime(&self) -> Result<(), SharedRuntimeError> { + let mut runtime_lock = self.runtime.lock_or_panic(); + if runtime_lock.is_none() { + *runtime_lock = Some(Arc::new(build_runtime()?)); + } + Ok(()) + } + + /// Hook to be called in the parent process after forking. + /// + /// This method restarts workers and resumes normal operation in the parent process. + /// The runtime may need to be recreated if it was shut down in before_fork. + /// + /// # Errors + /// Returns an error if workers cannot be restarted or the runtime cannot be recreated. + pub fn after_fork_parent(&self) -> Result<(), SharedRuntimeError> { + debug!("after_fork_parent: restarting runtime and workers"); + self.restart_runtime()?; + + let runtime_lock = self.runtime.lock_or_panic(); + let handle = runtime_lock + .as_ref() + .ok_or(SharedRuntimeError::RuntimeUnavailable)? + .handle() + .clone(); + drop(runtime_lock); + + let spawner = RuntimeSpawner; + let mut workers_lock = self.workers.lock_or_panic(); + + for worker_entry in workers_lock.iter_mut() { + worker_entry.worker.start(&spawner, &handle)?; + } + + Ok(()) + } + + /// Hook to be called in the child process after forking. + /// + /// This method reinitializes the runtime and workers in the child process. + /// A new runtime must be created since tokio runtimes cannot be safely forked. + /// Workers are reset and restarted to resume operations in the child. + /// + /// # Errors + /// Returns an error if the runtime cannot be reinitialized or workers cannot be started. + pub fn after_fork_child(&self) -> Result<(), SharedRuntimeError> { + debug!("after_fork_child: reinitializing runtime and workers"); + self.restart_runtime()?; + + let runtime_lock = self.runtime.lock_or_panic(); + let handle = runtime_lock + .as_ref() + .ok_or(SharedRuntimeError::RuntimeUnavailable)? + .handle() + .clone(); + drop(runtime_lock); + + let spawner = RuntimeSpawner; + let mut workers_lock = self.workers.lock_or_panic(); + + workers_lock.retain(|entry| entry.restart_on_fork); + + for worker_entry in workers_lock.iter_mut() { + worker_entry.worker.reset(); + worker_entry.worker.start(&spawner, &handle)?; + } + + Ok(()) + } + + /// Run a future to completion on the shared runtime, blocking the current thread. + /// + /// If the runtime is not available (e.g. after calling before_fork), a temporary + /// single-threaded runtime is used. + /// + /// Not available on wasm32 -- use async paths instead. + /// + /// # Errors + /// Returns an error if it fails to create a fallback runtime. + pub fn block_on(&self, f: F) -> Result { + let runtime = match self.runtime.lock_or_panic().as_ref() { + None => Arc::new(Builder::new_current_thread().enable_all().build()?), + Some(runtime) => runtime.clone(), + }; + Ok(runtime.block_on(f)) + } + + /// Shutdown the runtime and all workers synchronously with optional timeout. + /// + /// Not available on wasm32 -- use [`shutdown_async`](Self::shutdown_async) instead. + /// + /// Worker errors are logged but do not cause the function to fail. + /// + /// # Errors + /// Returns an error only if shutdown times out. + pub fn shutdown( + &self, + timeout: Option, + ) -> Result<(), SharedRuntimeError> { + debug!(?timeout, "Shutting down SharedRuntime"); + match self.runtime.lock_or_panic().take() { + Some(runtime) => { + if let Some(timeout) = timeout { + match runtime.block_on(async { + tokio::time::timeout(timeout, self.shutdown_async()).await + }) { + Ok(()) => Ok(()), + Err(_) => Err(SharedRuntimeError::ShutdownTimedOut(timeout)), + } + } else { + runtime.block_on(self.shutdown_async()); + Ok(()) + } + } + None => Ok(()), + } + } + } +} + +#[cfg(all(not(target_arch = "wasm32"), test))] +pub(crate) use native::RuntimeSpawner; + type BoxedWorker = Box; #[derive(Debug)] @@ -140,84 +420,71 @@ impl From for SharedRuntimeError { /// A shared runtime that manages PausableWorkers and provides fork safety hooks. /// -/// The SharedRuntime owns a tokio runtime and tracks PausableWorkers spawned on it. -/// It provides methods to safely pause workers before forking and restart them -/// after fork in both parent and child processes. +/// The SharedRuntime owns a tokio runtime (on native) and tracks PausableWorkers +/// spawned on it. It provides methods to safely pause workers before forking and +/// restart them after fork in both parent and child processes. +/// +/// On wasm32, no tokio runtime is created. Workers are spawned via the caller-provided +/// [`SpawnCapability`] which delegates to `spawn_local` on the JS event loop. /// /// # Mutex lock order /// When locking both [Self::runtime] and [Self::workers], the mutex must be locked in the order of /// the fields in the struct. When possible avoid holding both locks simultaneously. #[derive(Debug)] pub struct SharedRuntime { - runtime: Arc>>>, + #[cfg(not(target_arch = "wasm32"))] + runtime: Arc>>>, workers: Arc>>, next_worker_id: AtomicU64, } -/// Build a tokio runtime appropriate for the current platform. -/// -/// On wasm32, a single-threaded current-thread runtime is used since multi-threading -/// is not available. On all other platforms a multi-threaded runtime is used. -fn build_runtime() -> Result { - #[cfg(not(target_arch = "wasm32"))] - { - Builder::new_multi_thread() - .worker_threads(1) - .enable_all() - .build() - } - #[cfg(target_arch = "wasm32")] - { - Builder::new_current_thread().enable_all().build() - } -} - impl SharedRuntime { - /// Create a new SharedRuntime with a default tokio runtime. + /// Create a new SharedRuntime. + /// + /// On native, this creates a tokio multi-thread runtime. On wasm32, no runtime + /// is created (workers are spawned on the JS event loop via [`SpawnCapability`]). /// /// # Errors - /// Returns an error if the tokio runtime cannot be created. + /// Returns an error if the tokio runtime cannot be created (native only). pub fn new() -> Result { debug!("Creating new SharedRuntime"); - let runtime = build_runtime()?; - Ok(Self { - runtime: Arc::new(Mutex::new(Some(Arc::new(runtime)))), - workers: Arc::new(Mutex::new(Vec::new())), - next_worker_id: AtomicU64::new(1), - }) + #[cfg(not(target_arch = "wasm32"))] + { + Self::new_native() + } + #[cfg(target_arch = "wasm32")] + { + Ok(Self { + workers: Arc::new(Mutex::new(Vec::new())), + next_worker_id: AtomicU64::new(1), + }) + } } - /// Spawn a PausableWorker on this runtime. + /// Spawn a PausableWorker using the provided spawn capability (wasm variant). /// - /// The worker will be tracked by this SharedRuntime and will be paused/resumed - /// during fork operations. - /// If `restart_on_fork` is true, the worker will be reset and restarted when calling - /// `after_fork_child` else the worker is dropped *without* calling `Worker::shutdown`. - /// - /// # Errors - /// Returns an error if the runtime is not available or the worker cannot be started. - pub fn spawn_worker( + /// On wasm the runtime context is `()` — workers are spawned on the JS event loop. + #[cfg(target_arch = "wasm32")] + pub fn spawn_worker< + T: Worker + Sync + 'static, + S: libdd_capabilities::spawn::SpawnCapability + 'static, + >( &self, worker: T, restart_on_fork: bool, + spawner: &S, ) -> Result { + use std::sync::atomic::Ordering; + let boxed_worker: BoxedWorker = Box::new(worker); debug!(?boxed_worker, "Spawning worker on SharedRuntime"); let mut pausable_worker = PausableWorker::new(boxed_worker); - // Hold the workers lock while starting the worker to avoid a race with - // before_fork: without this, before_fork could run after the worker is started but - // before it's added to the list, not pausing the worker before the runtime is dropped. - let runtime = self.runtime.lock_or_panic().clone(); let mut workers_guard = self.workers.lock_or_panic(); - // If the runtime is not available, the worker will be started - // when the runtime is recreated (after_fork_parent/child). - if let Some(runtime) = runtime { - if let Err(e) = pausable_worker.start(&runtime) { - return Err(e.into()); - } + if let Err(e) = pausable_worker.start(spawner, &()) { + return Err(e.into()); } let worker_id = self.next_worker_id.fetch_add(1, Ordering::Relaxed); @@ -234,145 +501,6 @@ impl SharedRuntime { }) } - /// Hook to be called before forking. - /// - /// This method pauses all workers and prepares the runtime for forking. - /// It ensures that no background tasks are running when the fork occurs, - /// preventing potential deadlocks in the child process. - /// - /// Worker errors are logged but do not cause the function to fail. - /// If the worker fails to pause it is dropped without calling shutdown. - #[cfg(not(target_arch = "wasm32"))] - pub fn before_fork(&self) { - debug!("before_fork: pausing all workers"); - if let Some(runtime) = self.runtime.lock_or_panic().take() { - let mut workers_lock = self.workers.lock_or_panic(); - runtime.block_on(async { - let futures: FuturesUnordered<_> = workers_lock - .iter_mut() - .map(|worker_entry| async { - if let Err(e) = worker_entry.worker.pause().await { - error!("Worker failed to pause before fork: {:?}", e); - } - }) - .collect(); - - futures.collect::<()>().await; - }); - } - } - - fn restart_runtime(&self) -> Result<(), SharedRuntimeError> { - let mut runtime_lock = self.runtime.lock_or_panic(); - if runtime_lock.is_none() { - *runtime_lock = Some(Arc::new(build_runtime()?)); - } - Ok(()) - } - - /// Hook to be called in the parent process after forking. - /// - /// This method restarts workers and resumes normal operation in the parent process. - /// The runtime may need to be recreated if it was shut down in before_fork. - /// - /// # Errors - /// Returns an error if workers cannot be restarted or the runtime cannot be recreated. - #[cfg(not(target_arch = "wasm32"))] - pub fn after_fork_parent(&self) -> Result<(), SharedRuntimeError> { - debug!("after_fork_parent: restarting runtime and workers"); - self.restart_runtime()?; - - let runtime_lock = self.runtime.lock_or_panic(); - let runtime = runtime_lock - .as_ref() - .ok_or(SharedRuntimeError::RuntimeUnavailable)? - .clone(); - drop(runtime_lock); - - let mut workers_lock = self.workers.lock_or_panic(); - - // Restart all workers - for worker_entry in workers_lock.iter_mut() { - worker_entry.worker.start(&runtime)?; - } - - Ok(()) - } - - /// Hook to be called in the child process after forking. - /// - /// This method reinitializes the runtime and workers in the child process. - /// A new runtime must be created since tokio runtimes cannot be safely forked. - /// Workers are reset and restarted to resume operations in the child. - /// - /// # Errors - /// Returns an error if the runtime cannot be reinitialized or workers cannot be started. - #[cfg(not(target_arch = "wasm32"))] - pub fn after_fork_child(&self) -> Result<(), SharedRuntimeError> { - debug!("after_fork_child: reinitializing runtime and workers"); - self.restart_runtime()?; - - let runtime_lock = self.runtime.lock_or_panic(); - let runtime = runtime_lock - .as_ref() - .ok_or(SharedRuntimeError::RuntimeUnavailable)? - .clone(); - drop(runtime_lock); - - let mut workers_lock = self.workers.lock_or_panic(); - - // Drop workers not marked as restart on fork - workers_lock.retain(|entry| entry.restart_on_fork); - - for worker_entry in workers_lock.iter_mut() { - worker_entry.worker.reset(); - worker_entry.worker.start(&runtime)?; - } - - Ok(()) - } - - /// Run a future to completion on the shared runtime, blocking the current thread. - /// - /// If the runtime is not available (e.g. after calling before_fork), a temporary - /// single-threaded runtime is used. - /// - /// # Errors - /// Returns an error if it fails to create a fallback runtime. - pub fn block_on(&self, f: F) -> Result { - let runtime = match self.runtime.lock_or_panic().as_ref() { - None => Arc::new(Builder::new_current_thread().enable_all().build()?), - Some(runtime) => runtime.clone(), - }; - Ok(runtime.block_on(f)) - } - - /// Shutdown the runtime and all workers synchronously with optional timeout. - /// - /// Worker errors are logged but do not cause the function to fail. - /// - /// # Errors - /// Returns an error only if shutdown times out. - pub fn shutdown(&self, timeout: Option) -> Result<(), SharedRuntimeError> { - debug!(?timeout, "Shutting down SharedRuntime"); - match self.runtime.lock_or_panic().take() { - Some(runtime) => { - if let Some(timeout) = timeout { - match runtime.block_on(async { - tokio::time::timeout(timeout, self.shutdown_async()).await - }) { - Ok(()) => Ok(()), - Err(_) => Err(SharedRuntimeError::ShutdownTimedOut(timeout)), - } - } else { - runtime.block_on(self.shutdown_async()); - Ok(()) - } - } - None => Ok(()), // The runtime is not running so there's nothing to shutdown - } - } - /// Shutdown all workers asynchronously. /// /// This should be called during application shutdown to cleanly stop all @@ -450,9 +578,10 @@ mod tests { #[test] fn test_spawn_worker() { let shared_runtime = SharedRuntime::new().unwrap(); + let spawner = RuntimeSpawner; let (worker, receiver) = make_test_worker(); - let result = shared_runtime.spawn_worker(worker, true); + let result = shared_runtime.spawn_worker(worker, true, &spawner); assert!(result.is_ok()); assert_eq!(shared_runtime.workers.lock_or_panic().len(), 1); @@ -469,9 +598,10 @@ mod tests { fn test_worker_handle_stop() { let rt = tokio::runtime::Runtime::new().unwrap(); let shared_runtime = SharedRuntime::new().unwrap(); + let spawner = RuntimeSpawner; let (worker, receiver) = make_test_worker(); - let handle = shared_runtime.spawn_worker(worker, true).unwrap(); + let handle = shared_runtime.spawn_worker(worker, true, &spawner).unwrap(); assert_eq!(shared_runtime.workers.lock_or_panic().len(), 1); // Wait for at least one run before stopping @@ -498,9 +628,10 @@ mod tests { #[test] fn test_before_and_after_fork_parent() { let shared_runtime = SharedRuntime::new().unwrap(); + let spawner = RuntimeSpawner; let (worker, receiver) = make_test_worker(); - shared_runtime.spawn_worker(worker, true).unwrap(); + shared_runtime.spawn_worker(worker, true, &spawner).unwrap(); // Let the worker run until state > 0 so that preservation is observable let mut state_before_fork = 0; @@ -529,9 +660,10 @@ mod tests { #[test] fn test_after_fork_child() { let shared_runtime = SharedRuntime::new().unwrap(); + let spawner = RuntimeSpawner; let (worker, receiver) = make_test_worker(); - shared_runtime.spawn_worker(worker, true).unwrap(); + shared_runtime.spawn_worker(worker, true, &spawner).unwrap(); // Let the worker run until state > 0 so that the reset is observable let mut state_before_fork = 0; @@ -560,9 +692,10 @@ mod tests { #[test] fn test_shutdown() { let shared_runtime = SharedRuntime::new().unwrap(); + let spawner = RuntimeSpawner; let (worker, receiver) = make_test_worker(); - shared_runtime.spawn_worker(worker, true).unwrap(); + shared_runtime.spawn_worker(worker, true, &spawner).unwrap(); // Wait for at least one run before shutting down receiver @@ -584,9 +717,12 @@ mod tests { #[test] fn test_after_fork_child_drops_worker_not_restart_on_fork() { let shared_runtime = SharedRuntime::new().unwrap(); + let spawner = RuntimeSpawner; let (worker, receiver) = make_test_worker(); - shared_runtime.spawn_worker(worker, false).unwrap(); + shared_runtime + .spawn_worker(worker, false, &spawner) + .unwrap(); // Wait for the worker to run at least once receiver diff --git a/libdd-shared-runtime/src/shared_runtime/pausable_worker.rs b/libdd-shared-runtime/src/shared_runtime/pausable_worker.rs index e3dcec8701..b5ae7e1ff1 100644 --- a/libdd-shared-runtime/src/shared_runtime/pausable_worker.rs +++ b/libdd-shared-runtime/src/shared_runtime/pausable_worker.rs @@ -4,21 +4,29 @@ //! Defines a pausable worker to be able to stop background processes before forks use crate::worker::Worker; +use core::pin::Pin; +use libdd_capabilities::spawn::{SpawnCapability, SpawnError}; use libdd_capabilities::MaybeSend; use std::fmt::Display; -use tokio::{runtime::Runtime, select, task::JoinHandle}; +use std::future::Future; +use tokio::select; use tokio_util::sync::CancellationToken; use tracing::debug; +#[cfg(not(target_arch = "wasm32"))] +type WorkerJoinHandle = Pin> + Send>>; + +#[cfg(target_arch = "wasm32")] +type WorkerJoinHandle = Pin>>>; + /// A pausable worker which can be paused and restarted on forks. /// /// Used to allow a [`super::Worker`] to be paused while saving its state when /// dropping a tokio runtime to be able to restart with the same state on a new runtime. This is /// used to stop all threads before a fork to avoid deadlocks in child. -#[derive(Debug)] pub enum PausableWorker { Running { - handle: JoinHandle, + handle: WorkerJoinHandle, stop_token: CancellationToken, }, Paused { @@ -27,6 +35,19 @@ pub enum PausableWorker { InvalidState, } +impl std::fmt::Debug for PausableWorker { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Running { .. } => f.debug_struct("PausableWorker::Running").finish(), + Self::Paused { worker } => f + .debug_struct("PausableWorker::Paused") + .field("worker", worker) + .finish(), + Self::InvalidState => write!(f, "PausableWorker::InvalidState"), + } + } +} + #[derive(Debug)] pub enum PausableWorkerError { InvalidState, @@ -54,17 +75,23 @@ impl PausableWorker { Self::Paused { worker } } - /// Start the worker on the given runtime. + /// Start the worker using the given spawn capability. /// - /// The worker's main loop will be run on the runtime. - pub fn start(&mut self, rt: &Runtime) -> Result<(), PausableWorkerError> { - #[cfg(target_arch = "wasm32")] - return Ok(()); - #[cfg(not(target_arch = "wasm32"))] + /// The worker's main loop will be spawned via the provided spawner. + /// `ctx` is the platform-specific runtime context (e.g. `tokio::runtime::Handle` + /// on native, `()` on wasm). + pub fn start( + &mut self, + spawner: &S, + ctx: &S::RuntimeContext, + ) -> Result<(), PausableWorkerError> + where + S::JoinHandle: 'static, + { match self { PausableWorker::Running { .. } => Ok(()), - PausableWorker::Paused { worker } => { - debug!(?worker, "Starting pausable worker"); + PausableWorker::Paused { worker: _ } => { + debug!(?self, "Starting pausable worker"); let PausableWorker::Paused { mut worker } = std::mem::replace(self, PausableWorker::InvalidState) else { @@ -76,40 +103,44 @@ impl PausableWorker { // will be replaced by a valid state. let stop_token = CancellationToken::new(); let cloned_token = stop_token.clone(); - let handle = rt.spawn(async move { - // First iteration using initial_trigger - select! { - // Always check for cancellation first, to reduce time-to-pause in case - // the initial trigger is always ready. - biased; - - _ = cloned_token.cancelled() => { - return worker; - } - _ = worker.initial_trigger() => { - worker.run().await; - } - } - - // Regular iterations - loop { + let handle = spawner.spawn( + async move { + // First iteration using initial_trigger select! { - // Always check for cancellation first, to reduce time-to-pause in case - // the trigger is always ready. + // Always check for cancellation first, to reduce time-to-pause in + // case the initial trigger is always ready. biased; - _ = cloned_token.cancelled() => { - break; + return worker; } - _ = worker.trigger() => { + _ = worker.initial_trigger() => { worker.run().await; } } - } - worker - }); - *self = PausableWorker::Running { handle, stop_token }; + // Regular iterations + loop { + select! { + // Always check for cancellation first, to reduce time-to-pause + // in case the trigger is always ready. + biased; + _ = cloned_token.cancelled() => { + break; + } + _ = worker.trigger() => { + worker.run().await; + } + } + } + worker + }, + ctx, + ); + + *self = PausableWorker::Running { + handle: Box::pin(handle), + stop_token, + }; Ok(()) } PausableWorker::InvalidState => Err(PausableWorkerError::InvalidState), @@ -141,7 +172,6 @@ impl PausableWorker { *self = PausableWorker::Paused { worker }; Ok(()) } else { - // The task has been aborted and the worker can't be retrieved. *self = PausableWorker::InvalidState; Err(PausableWorkerError::TaskAborted) } @@ -172,6 +202,7 @@ mod tests { use tokio::{runtime::Builder, time::sleep}; use super::*; + use crate::shared_runtime::RuntimeSpawner; use std::{ sync::mpsc::{channel, Sender}, time::Duration, @@ -201,9 +232,11 @@ mod tests { let (sender, receiver) = channel::(); let worker = TestWorker { state: 0, sender }; let runtime = Builder::new_multi_thread().enable_time().build().unwrap(); + let handle = runtime.handle().clone(); + let spawner = RuntimeSpawner; let mut pausable_worker = PausableWorker::new(worker); - pausable_worker.start(&runtime).unwrap(); + pausable_worker.start(&spawner, &handle).unwrap(); assert_eq!(receiver.recv().unwrap(), 0); runtime.block_on(async { pausable_worker.pause().await.unwrap() }); @@ -212,7 +245,7 @@ mod tests { for message in receiver.try_iter() { next_message = message + 1; } - pausable_worker.start(&runtime).unwrap(); + pausable_worker.start(&spawner, &handle).unwrap(); assert_eq!(receiver.recv().unwrap(), next_message); } } diff --git a/libdd-trace-stats/src/stats_exporter.rs b/libdd-trace-stats/src/stats_exporter.rs index e0130804cd..d8372a024d 100644 --- a/libdd-trace-stats/src/stats_exporter.rs +++ b/libdd-trace-stats/src/stats_exporter.rs @@ -11,7 +11,7 @@ use std::{ use crate::span_concentrator::{FlushableConcentrator, SpanConcentrator}; use async_trait::async_trait; -use libdd_capabilities::{HttpClientTrait, MaybeSend}; +use libdd_capabilities::{HttpClientCapability, MaybeSend, SleepCapability}; use libdd_common::Endpoint; use libdd_shared_runtime::Worker; use libdd_trace_protobuf::pb; @@ -54,19 +54,24 @@ impl<'a> From<&'a StatsMetadata> for TracerHeaderTags<'a> { /// An exporter that concentrates and sends stats to the agent. /// -/// `H` is the HTTP client implementation, see [`HttpClientTrait`]. Leaf crates -/// pin it to a concrete type. +/// `Cap` is the capabilities bundle (HTTP + sleep). Leaf crates pin it to a +/// concrete type (`NativeCapabilities` or `WasmCapabilities`). #[derive(Debug)] -pub struct StatsExporter { +pub struct StatsExporter< + Cap: HttpClientCapability + SleepCapability, + Con: FlushableConcentrator = SpanConcentrator, +> { flush_interval: time::Duration, - concentrator: Arc>, + concentrator: Arc>, endpoint: Endpoint, meta: StatsMetadata, sequence_id: AtomicU64, - client: H, + capabilities: Cap, } -impl StatsExporter { +impl + StatsExporter +{ /// Return a new StatsExporter /// /// - `flush_interval` the interval on which the concentrator is flushed @@ -78,10 +83,10 @@ impl StatsExporter { /// concentrator pub fn new( flush_interval: time::Duration, - concentrator: Arc>, + concentrator: Arc>, meta: StatsMetadata, endpoint: Endpoint, - client: H, + capabilities: Cap, ) -> Self { Self { flush_interval, @@ -89,7 +94,7 @@ impl StatsExporter { endpoint, meta, sequence_id: AtomicU64::new(0), - client, + capabilities, } } @@ -124,7 +129,7 @@ impl StatsExporter { ); let result = send_with_retry( - &self.client, + &self.capabilities, &self.endpoint, body, &headers, @@ -164,12 +169,12 @@ impl StatsExporter { #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] impl< - H: HttpClientTrait + MaybeSend + Sync + Debug + 'static, - C: FlushableConcentrator + Send + Debug, - > Worker for StatsExporter + Cap: HttpClientCapability + SleepCapability + MaybeSend + Sync + 'static, + Con: FlushableConcentrator + Send + Debug, + > Worker for StatsExporter { async fn trigger(&mut self) { - tokio::time::sleep(self.flush_interval).await; + self.capabilities.sleep(self.flush_interval).await; } /// Flush and send stats on every trigger. @@ -358,17 +363,17 @@ mod tests { then.status(200).body(""); }); + let caps = NativeCapabilities::new(); let stats_exporter = StatsExporter::::new( // Use smaller buckets duration to speed up test Duration::from_secs(1), Arc::new(Mutex::new(get_test_concentrator())), get_test_metadata(), Endpoint::from_url(stats_url_from_agent_url(&server.url("/")).unwrap()), - NativeCapabilities::new_client(), + caps.clone(), ); - let _handle = shared_runtime - .spawn_worker(stats_exporter, true) + .spawn_worker(stats_exporter, true, &caps) .expect("Failed to spawn worker"); // Wait for stats to be flushed @@ -400,16 +405,17 @@ mod tests { let buckets_duration = Duration::from_secs(10); + let caps = NativeCapabilities::new(); let stats_exporter = StatsExporter::::new( buckets_duration, Arc::new(Mutex::new(get_test_concentrator())), get_test_metadata(), Endpoint::from_url(stats_url_from_agent_url(&server.url("/")).unwrap()), - NativeCapabilities::new_client(), + caps.clone(), ); let _handle = shared_runtime - .spawn_worker(stats_exporter, true) + .spawn_worker(stats_exporter, true, &caps) .expect("Failed to spawn worker"); shared_runtime.shutdown(None).unwrap(); diff --git a/libdd-trace-utils/src/send_data/mod.rs b/libdd-trace-utils/src/send_data/mod.rs index c884566d17..39cc4eefff 100644 --- a/libdd-trace-utils/src/send_data/mod.rs +++ b/libdd-trace-utils/src/send_data/mod.rs @@ -11,7 +11,7 @@ use anyhow::{anyhow, Context}; use futures::stream::FuturesUnordered; use futures::StreamExt; use http::{header::CONTENT_TYPE, HeaderMap, HeaderValue}; -use libdd_capabilities::HttpClientTrait; +use libdd_capabilities::{HttpClientCapability, SleepCapability}; use libdd_common::{ header::{ APPLICATION_MSGPACK, APPLICATION_PROTOBUF, DATADOG_SEND_REAL_HTTP_STATUS, @@ -59,10 +59,10 @@ use zstd::stream::write::Encoder; /// /// send_data.set_retry_strategy(retry_strategy); /// -/// // Send the data (caller picks the HTTP client implementation) -/// use libdd_capabilities::HttpClientTrait; -/// let client = libdd_capabilities_impl::NativeCapabilities::new_client(); -/// let result = send_data.send(&client).await; +/// // Send the data (caller picks the capabilities implementation) +/// use libdd_capabilities::HttpClientCapability; +/// let capabilities = libdd_capabilities_impl::NativeCapabilities::new_client(); +/// let result = send_data.send(&capabilities).await; /// } /// ``` pub struct SendData { @@ -224,25 +224,28 @@ impl SendData { /// # Returns /// /// A `SendDataResult` instance containing the result of the operation. - pub async fn send(&self, client: &H) -> SendDataResult { - self.send_internal(client, None).await + pub async fn send( + &self, + capabilities: &C, + ) -> SendDataResult { + self.send_internal(capabilities, None).await } - async fn send_internal( + async fn send_internal( &self, - client: &H, + capabilities: &C, endpoint: Option, ) -> SendDataResult { if self.use_protobuf() { - self.send_with_protobuf(client, endpoint).await + self.send_with_protobuf(capabilities, endpoint).await } else { - self.send_with_msgpack(client, endpoint).await + self.send_with_msgpack(capabilities, endpoint).await } } - async fn send_payload( + async fn send_payload( &self, - client: &H, + capabilities: &C, chunks: u64, payload: Vec, headers: HeaderMap, @@ -252,7 +255,7 @@ impl SendData { let payload_len = u64::try_from(payload.len()).unwrap(); ( send_with_retry( - client, + capabilities, endpoint.unwrap_or(&self.target), payload, &headers, @@ -293,9 +296,9 @@ impl SendData { } } - async fn send_with_protobuf( + async fn send_with_protobuf( &self, - client: &H, + capabilities: &C, endpoint: Option, ) -> SendDataResult { let mut result = SendDataResult::default(); @@ -325,7 +328,7 @@ impl SendData { let (response, bytes_sent, chunks) = self .send_payload( - client, + capabilities, chunks, final_payload, request_headers, @@ -341,9 +344,9 @@ impl SendData { } } - async fn send_with_msgpack( + async fn send_with_msgpack( &self, - client: &H, + capabilities: &C, endpoint: Option, ) -> SendDataResult { let mut result = SendDataResult::default(); @@ -365,7 +368,7 @@ impl SendData { }; futures.push(self.send_payload( - client, + capabilities, chunks, payload, headers, @@ -384,7 +387,7 @@ impl SendData { let payload = msgpack_encoder::v04::to_vec(payload); futures.push(self.send_payload( - client, + capabilities, chunks, payload, headers, @@ -405,7 +408,7 @@ impl SendData { }; futures.push(self.send_payload( - client, + capabilities, chunks, payload, headers, @@ -460,7 +463,7 @@ mod tests { use crate::tracer_header_tags::TracerHeaderTags; use httpmock::prelude::*; use httpmock::MockServer; - use libdd_capabilities::HttpClientTrait; + use libdd_capabilities::HttpClientCapability; use libdd_capabilities_impl::NativeCapabilities; use libdd_common::Endpoint; use libdd_trace_protobuf::pb::Span; diff --git a/libdd-trace-utils/src/send_with_retry/mod.rs b/libdd-trace-utils/src/send_with_retry/mod.rs index 7ee59e9bf1..4509c95bd7 100644 --- a/libdd-trace-utils/src/send_with_retry/mod.rs +++ b/libdd-trace-utils/src/send_with_retry/mod.rs @@ -9,9 +9,8 @@ pub use retry_strategy::{RetryBackoffType, RetryStrategy}; use bytes::Bytes; use http::HeaderMap; -use libdd_capabilities::{HttpClientTrait, HttpError}; +use libdd_capabilities::{HttpClientCapability, HttpError, SleepCapability}; use libdd_common::Endpoint; -#[cfg(not(target_arch = "wasm32"))] use std::time::Duration; use tracing::{debug, error}; @@ -69,7 +68,7 @@ impl std::error::Error for SendWithRetryError {} /// /// ```rust, no_run /// # use libdd_common::Endpoint; -/// # use libdd_capabilities::HttpClientTrait; +/// # use libdd_capabilities::{HttpClientCapability, SleepCapability}; /// # use libdd_trace_utils::send_with_retry::*; /// # async fn run() -> SendWithRetryResult { /// let payload: Vec = vec![0, 1, 2, 3]; @@ -83,19 +82,18 @@ impl std::error::Error for SendWithRetryError {} /// http::HeaderValue::from_static("application/msgpack"), /// ); /// let retry_strategy = RetryStrategy::new(3, 10, RetryBackoffType::Exponential, Some(5)); -/// let client = libdd_capabilities_impl::NativeCapabilities::new_client(); -/// send_with_retry(&client, &target, payload, &headers, &retry_strategy).await +/// let capabilities = libdd_capabilities_impl::NativeCapabilities::new_client(); +/// send_with_retry(&capabilities, &target, payload, &headers, &retry_strategy).await /// # } /// ``` -pub async fn send_with_retry( - client: &H, +pub async fn send_with_retry( + capabilities: &C, target: &Endpoint, payload: Vec, headers: &HeaderMap, retry_strategy: &RetryStrategy, ) -> SendWithRetryResult { let mut request_attempt = 0; - #[cfg(not(target_arch = "wasm32"))] let timeout = Duration::from_millis(target.timeout_ms); debug!( @@ -130,10 +128,11 @@ pub async fn send_with_retry( } }; - #[cfg(not(target_arch = "wasm32"))] - let result = tokio::time::timeout(timeout, client.request(req)).await; - #[cfg(target_arch = "wasm32")] - let result: Result, std::convert::Infallible> = Ok(client.request(req).await); + let result = tokio::select! { + biased; + r = capabilities.request(req) => Ok(r), + _ = capabilities.sleep(timeout) => Err(()), + }; match result { Ok(Ok(response)) => { @@ -158,7 +157,7 @@ pub async fn send_with_retry( remaining_retries = retry_strategy.max_retries() - request_attempt, "Retrying after error status code" ); - retry_strategy.delay(request_attempt).await; + retry_strategy.delay(request_attempt, capabilities).await; continue; } else { error!( @@ -191,7 +190,7 @@ pub async fn send_with_retry( remaining_retries = retry_strategy.max_retries() - request_attempt, "Retrying after request error" ); - retry_strategy.delay(request_attempt).await; + retry_strategy.delay(request_attempt, capabilities).await; continue; } else { let classified_error = match e { @@ -223,7 +222,7 @@ pub async fn send_with_retry( remaining_retries = retry_strategy.max_retries() - request_attempt, "Retrying after timeout" ); - retry_strategy.delay(request_attempt).await; + retry_strategy.delay(request_attempt, capabilities).await; continue; } else { error!( @@ -242,7 +241,7 @@ mod tests { use super::*; use crate::test_utils::poll_for_mock_hit; use httpmock::MockServer; - use libdd_capabilities::HttpClientTrait; + use libdd_capabilities::HttpClientCapability; use libdd_capabilities_impl::NativeCapabilities; #[cfg_attr(miri, ignore)] @@ -273,11 +272,11 @@ mod tests { }; let strategy = RetryStrategy::new(0, 2, RetryBackoffType::Constant, None); - let client = NativeCapabilities::new_client(); + let capabilities = NativeCapabilities::new_client(); tokio::spawn(async move { let result = send_with_retry( - &client, + &capabilities, &target_endpoint, vec![0, 1, 2, 3], &HeaderMap::new(), @@ -322,11 +321,11 @@ mod tests { }; let strategy = RetryStrategy::new(2, 250, RetryBackoffType::Constant, None); - let client = NativeCapabilities::new_client(); + let capabilities = NativeCapabilities::new_client(); tokio::spawn(async move { let result = send_with_retry( - &client, + &capabilities, &target_endpoint, vec![0, 1, 2, 3], &HeaderMap::new(), @@ -371,11 +370,11 @@ mod tests { RetryBackoffType::Constant, None, ); - let client = NativeCapabilities::new_client(); + let capabilities = NativeCapabilities::new_client(); tokio::spawn(async move { let result = send_with_retry( - &client, + &capabilities, &target_endpoint, vec![0, 1, 2, 3], &HeaderMap::new(), @@ -420,11 +419,11 @@ mod tests { }; let strategy = RetryStrategy::new(2, 10, RetryBackoffType::Constant, None); - let client = NativeCapabilities::new_client(); + let capabilities = NativeCapabilities::new_client(); tokio::spawn(async move { let result = send_with_retry( - &client, + &capabilities, &target_endpoint, vec![0, 1, 2, 3], &HeaderMap::new(), diff --git a/libdd-trace-utils/src/send_with_retry/retry_strategy.rs b/libdd-trace-utils/src/send_with_retry/retry_strategy.rs index fba2191b8e..daeefdf30e 100644 --- a/libdd-trace-utils/src/send_with_retry/retry_strategy.rs +++ b/libdd-trace-utils/src/send_with_retry/retry_strategy.rs @@ -4,8 +4,8 @@ //! Types used when calling [`super::send_with_retry`] to configure the retry logic. use std::time::Duration; -#[cfg(not(target_arch = "wasm32"))] -use tokio::time::sleep; + +use libdd_capabilities::sleep::SleepCapability; /// Enum representing the type of backoff to use for the delay between retries. #[derive(Debug, Clone)] @@ -17,12 +17,6 @@ pub enum RetryBackoffType { Constant, /// The delay is doubled for each attempt. Exponential, - /// No delay between retries. Intended for wasm where `tokio::time::sleep` is unavailable. - /// Should be paired with `max_retries: 0` to avoid spamming the target. - /// - /// Temporary workaround: a proper solution would introduce a `SleepTrait` capability so that - /// wasm can delegate to a JS-side timer (e.g. `setTimeout`). - Disabled, } /// Struct representing the retry strategy for sending data. @@ -45,23 +39,11 @@ pub struct RetryStrategy { impl Default for RetryStrategy { fn default() -> Self { - #[cfg(not(target_arch = "wasm32"))] - { - RetryStrategy { - max_retries: 5, - delay_ms: Duration::from_millis(100), - backoff_type: RetryBackoffType::Exponential, - jitter: None, - } - } - #[cfg(target_arch = "wasm32")] - { - RetryStrategy { - max_retries: 0, - delay_ms: Duration::ZERO, - backoff_type: RetryBackoffType::Disabled, - jitter: None, - } + RetryStrategy { + max_retries: 5, + delay_ms: Duration::from_millis(100), + backoff_type: RetryBackoffType::Exponential, + jitter: None, } } } @@ -109,30 +91,21 @@ impl RetryStrategy { /// # Arguments /// /// * `attempt`: The number of the current attempt (1-indexed). - pub(crate) async fn delay(&self, attempt: u32) { - if matches!(self.backoff_type, RetryBackoffType::Disabled) { - return; - } - - #[cfg(not(target_arch = "wasm32"))] - { - let delay = match self.backoff_type { - RetryBackoffType::Exponential => self.delay_ms * 2u32.pow(attempt - 1), - RetryBackoffType::Constant => self.delay_ms, - RetryBackoffType::Linear => self.delay_ms + (self.delay_ms * (attempt - 1)), - RetryBackoffType::Disabled => unreachable!(), - }; + /// * `capabilities`: Provides the sleep capability for the delay. + pub(crate) async fn delay(&self, attempt: u32, capabilities: &C) { + let delay = match self.backoff_type { + RetryBackoffType::Exponential => self.delay_ms * 2u32.pow(attempt - 1), + RetryBackoffType::Constant => self.delay_ms, + RetryBackoffType::Linear => self.delay_ms + (self.delay_ms * (attempt - 1)), + }; - if let Some(jitter) = self.jitter { - let jitter = rand::random::() % jitter.as_millis() as u64; - sleep(delay + Duration::from_millis(jitter)).await; - } else { - sleep(delay).await; - } - } - #[cfg(target_arch = "wasm32")] - { - let _ = attempt; + if let Some(jitter) = self.jitter { + let jitter = rand::random::() % jitter.as_millis() as u64; + capabilities + .sleep(delay + Duration::from_millis(jitter)) + .await; + } else { + capabilities.sleep(delay).await; } } @@ -146,6 +119,7 @@ impl RetryStrategy { // For tests RetryStrategy tests the observed delay should be approximate. mod tests { use super::*; + use libdd_capabilities_impl::NativeSleepCapability; use tokio::time::Instant; // This tolerance is on the higher side to account for github's runners not having consistent @@ -162,9 +136,10 @@ mod tests { backoff_type: RetryBackoffType::Constant, jitter: None, }; + let sleeper = NativeSleepCapability; let start = Instant::now(); - retry_strategy.delay(1).await; + retry_strategy.delay(1, &sleeper).await; let elapsed = start.elapsed(); assert!( @@ -177,7 +152,7 @@ mod tests { ); let start = Instant::now(); - retry_strategy.delay(2).await; + retry_strategy.delay(2, &sleeper).await; let elapsed = start.elapsed(); assert!( @@ -199,9 +174,10 @@ mod tests { backoff_type: RetryBackoffType::Linear, jitter: None, }; + let sleeper = NativeSleepCapability; let start = Instant::now(); - retry_strategy.delay(1).await; + retry_strategy.delay(1, &sleeper).await; let elapsed = start.elapsed(); assert!( @@ -214,7 +190,7 @@ mod tests { ); let start = Instant::now(); - retry_strategy.delay(3).await; + retry_strategy.delay(3, &sleeper).await; let elapsed = start.elapsed(); // For the Linear strategy, the delay for the 3rd attempt should be delay_ms + (delay_ms * @@ -239,9 +215,10 @@ mod tests { backoff_type: RetryBackoffType::Exponential, jitter: None, }; + let sleeper = NativeSleepCapability; let start = Instant::now(); - retry_strategy.delay(1).await; + retry_strategy.delay(1, &sleeper).await; let elapsed = start.elapsed(); assert!( @@ -254,7 +231,7 @@ mod tests { ); let start = Instant::now(); - retry_strategy.delay(3).await; + retry_strategy.delay(3, &sleeper).await; let elapsed = start.elapsed(); // For the Exponential strategy, the delay for the 3rd attempt should be delay_ms * 2^(3-1) // = delay_ms * 4. @@ -277,9 +254,10 @@ mod tests { backoff_type: RetryBackoffType::Constant, jitter: Some(Duration::from_millis(50)), }; + let sleeper = NativeSleepCapability; let start = Instant::now(); - retry_strategy.delay(1).await; + retry_strategy.delay(1, &sleeper).await; let elapsed = start.elapsed(); // The delay should be between delay_ms and delay_ms + jitter diff --git a/libdd-trace-utils/src/stats_utils.rs b/libdd-trace-utils/src/stats_utils.rs index ffa9a72ba0..708b7ee883 100644 --- a/libdd-trace-utils/src/stats_utils.rs +++ b/libdd-trace-utils/src/stats_utils.rs @@ -8,7 +8,7 @@ pub use mini_agent::*; mod mini_agent { use bytes::{Buf, Bytes}; use http_body_util::BodyExt; - use libdd_capabilities::HttpClientTrait; + use libdd_capabilities::HttpClientCapability; use libdd_common::http_common; use libdd_common::Endpoint; use libdd_trace_protobuf::pb; @@ -63,7 +63,7 @@ mod mini_agent { } } - pub async fn send_stats_payload( + pub async fn send_stats_payload( data: Vec, target: &Endpoint, api_key: &str, diff --git a/libdd-trace-utils/src/test_utils/datadog_test_agent.rs b/libdd-trace-utils/src/test_utils/datadog_test_agent.rs index 9710700b05..dd8351493e 100644 --- a/libdd-trace-utils/src/test_utils/datadog_test_agent.rs +++ b/libdd-trace-utils/src/test_utils/datadog_test_agent.rs @@ -210,7 +210,7 @@ impl DatadogAgentContainerBuilder { /// Basic usage: /// /// ```no_run -/// use libdd_capabilities::HttpClientTrait; +/// use libdd_capabilities::HttpClientCapability; /// use libdd_capabilities_impl::NativeCapabilities; /// use libdd_common::Endpoint; /// use libdd_trace_utils::send_data::SendData; diff --git a/libdd-trace-utils/tests/test_send_data.rs b/libdd-trace-utils/tests/test_send_data.rs index 75b98fc146..37ae09d691 100644 --- a/libdd-trace-utils/tests/test_send_data.rs +++ b/libdd-trace-utils/tests/test_send_data.rs @@ -6,7 +6,7 @@ mod tracing_integration_tests { use http_body_util::BodyExt; #[cfg(target_os = "linux")] use hyper::Uri; - use libdd_capabilities_impl::{HttpClientTrait, NativeCapabilities}; + use libdd_capabilities_impl::{HttpClientCapability, NativeCapabilities}; #[cfg(target_os = "linux")] use libdd_common::connector::uds::socket_path_to_uri; use libdd_common::{http_common, Endpoint}; From b89ebfcf0e5095e815838c5d2dd281414b315892 Mon Sep 17 00:00:00 2001 From: Jules Wiriath Date: Mon, 27 Apr 2026 18:50:55 +0200 Subject: [PATCH 02/14] chore: rename sleeper to capabilites --- .../src/send_with_retry/retry_strategy.rs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/libdd-trace-utils/src/send_with_retry/retry_strategy.rs b/libdd-trace-utils/src/send_with_retry/retry_strategy.rs index daeefdf30e..22ac83d76f 100644 --- a/libdd-trace-utils/src/send_with_retry/retry_strategy.rs +++ b/libdd-trace-utils/src/send_with_retry/retry_strategy.rs @@ -136,10 +136,10 @@ mod tests { backoff_type: RetryBackoffType::Constant, jitter: None, }; - let sleeper = NativeSleepCapability; + let capabilities = NativeSleepCapability; let start = Instant::now(); - retry_strategy.delay(1, &sleeper).await; + retry_strategy.delay(1, &capabilities).await; let elapsed = start.elapsed(); assert!( @@ -152,7 +152,7 @@ mod tests { ); let start = Instant::now(); - retry_strategy.delay(2, &sleeper).await; + retry_strategy.delay(2, &capabilities).await; let elapsed = start.elapsed(); assert!( @@ -174,10 +174,10 @@ mod tests { backoff_type: RetryBackoffType::Linear, jitter: None, }; - let sleeper = NativeSleepCapability; + let capabilities = NativeSleepCapability; let start = Instant::now(); - retry_strategy.delay(1, &sleeper).await; + retry_strategy.delay(1, &capabilities).await; let elapsed = start.elapsed(); assert!( @@ -190,7 +190,7 @@ mod tests { ); let start = Instant::now(); - retry_strategy.delay(3, &sleeper).await; + retry_strategy.delay(3, &capabilities).await; let elapsed = start.elapsed(); // For the Linear strategy, the delay for the 3rd attempt should be delay_ms + (delay_ms * @@ -215,10 +215,10 @@ mod tests { backoff_type: RetryBackoffType::Exponential, jitter: None, }; - let sleeper = NativeSleepCapability; + let capabilities = NativeSleepCapability; let start = Instant::now(); - retry_strategy.delay(1, &sleeper).await; + retry_strategy.delay(1, &capabilities).await; let elapsed = start.elapsed(); assert!( @@ -231,7 +231,7 @@ mod tests { ); let start = Instant::now(); - retry_strategy.delay(3, &sleeper).await; + retry_strategy.delay(3, &capabilities).await; let elapsed = start.elapsed(); // For the Exponential strategy, the delay for the 3rd attempt should be delay_ms * 2^(3-1) // = delay_ms * 4. @@ -254,10 +254,10 @@ mod tests { backoff_type: RetryBackoffType::Constant, jitter: Some(Duration::from_millis(50)), }; - let sleeper = NativeSleepCapability; + let capabilities = NativeSleepCapability; let start = Instant::now(); - retry_strategy.delay(1, &sleeper).await; + retry_strategy.delay(1, &capabilities).await; let elapsed = start.elapsed(); // The delay should be between delay_ms and delay_ms + jitter From 029bd3f63af9363d753ce8b36916f6537fc467e9 Mon Sep 17 00:00:00 2001 From: Jules Wiriath Date: Tue, 28 Apr 2026 18:36:40 +0200 Subject: [PATCH 03/14] feat: laziely instantiate the httpclient in native so that it's closer to a ZST --- libdd-capabilities-impl/src/http.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/libdd-capabilities-impl/src/http.rs b/libdd-capabilities-impl/src/http.rs index a08b5a878e..e6af5b853c 100644 --- a/libdd-capabilities-impl/src/http.rs +++ b/libdd-capabilities-impl/src/http.rs @@ -4,6 +4,8 @@ //! Native HTTP client implementation backed by hyper. mod native { + use std::sync::{Arc, OnceLock}; + use libdd_capabilities::http::{HttpClientCapability, HttpError}; use libdd_capabilities::maybe_send::MaybeSend; use libdd_common::connector::Connector; @@ -13,19 +15,21 @@ mod native { #[derive(Clone)] pub struct NativeHttpClient { - client: GenericHttpClient, + client: Arc>>, } impl std::fmt::Debug for NativeHttpClient { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("NativeHttpClient").finish() + f.debug_struct("NativeHttpClient") + .field("initialized", &self.client.get().is_some()) + .finish() } } impl HttpClientCapability for NativeHttpClient { fn new_client() -> Self { Self { - client: new_default_client(), + client: Arc::new(OnceLock::new()), } } @@ -35,7 +39,7 @@ mod native { req: http::Request, ) -> impl std::future::Future, HttpError>> + MaybeSend { - let client = self.client.clone(); + let client = self.client.get_or_init(new_default_client).clone(); async move { let hyper_req = req.map(Body::from_bytes); From 87094d876af267cc39f7ddd920754146694792f0 Mon Sep 17 00:00:00 2001 From: Jules Wiriath Date: Wed, 29 Apr 2026 12:00:04 +0200 Subject: [PATCH 04/14] feat: use type of the SpawnCapability directly on Native --- Cargo.lock | 1 + libdd-shared-runtime/Cargo.toml | 2 +- libdd-shared-runtime/src/lib.rs | 7 ++++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 838632edd0..e2ddfc9264 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3234,6 +3234,7 @@ dependencies = [ "async-trait", "futures", "libdd-capabilities", + "libdd-capabilities-impl", "libdd-common", "tokio", "tokio-util", diff --git a/libdd-shared-runtime/Cargo.toml b/libdd-shared-runtime/Cargo.toml index 390958c9e7..0beb449d1f 100644 --- a/libdd-shared-runtime/Cargo.toml +++ b/libdd-shared-runtime/Cargo.toml @@ -28,4 +28,4 @@ libdd-common = { version = "4.0.0", path = "../libdd-common", default-features = regex-lite = ["libdd-common/regex-lite"] [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -tokio = { version = "1.23", features = ["rt-multi-thread"] } +libdd-capabilities-impl = { path = "../libdd-capabilities-impl", version = "1.0.0" } diff --git a/libdd-shared-runtime/src/lib.rs b/libdd-shared-runtime/src/lib.rs index 7cb7b83225..56f2796477 100644 --- a/libdd-shared-runtime/src/lib.rs +++ b/libdd-shared-runtime/src/lib.rs @@ -30,7 +30,12 @@ pub use worker::Worker; /// `C: SpawnCapability` so the constraint /// is satisfied on every target without `#[cfg]` on individual where clauses. #[cfg(not(target_arch = "wasm32"))] -pub type SpawnRuntimeContext = tokio::runtime::Handle; +use libdd_capabilities::SpawnCapability; +#[cfg(not(target_arch = "wasm32"))] +use libdd_capabilities_impl::NativeSpawnCapability; + +#[cfg(not(target_arch = "wasm32"))] +pub type SpawnRuntimeContext = ::RuntimeContext; /// See the non-wasm variant for documentation. #[cfg(target_arch = "wasm32")] From 895dc461a69008b587147c375ce5c919d453fa9b Mon Sep 17 00:00:00 2001 From: Jules Wiriath Date: Thu, 30 Apr 2026 13:27:22 +0200 Subject: [PATCH 05/14] fix: remove RuntimeSpawner which was effectively a duplicate NativeSpawnCapability --- .../src/shared_runtime/mod.rs | 61 ++++--------------- .../src/shared_runtime/pausable_worker.rs | 4 +- 2 files changed, 13 insertions(+), 52 deletions(-) diff --git a/libdd-shared-runtime/src/shared_runtime/mod.rs b/libdd-shared-runtime/src/shared_runtime/mod.rs index 8bab3c37cd..82f1f3cc56 100644 --- a/libdd-shared-runtime/src/shared_runtime/mod.rs +++ b/libdd-shared-runtime/src/shared_runtime/mod.rs @@ -25,11 +25,8 @@ use tracing::{debug, error}; #[cfg(not(target_arch = "wasm32"))] mod native { use super::*; - use core::pin::Pin; - use core::task::{Context, Poll}; - use libdd_capabilities::spawn::{SpawnCapability, SpawnError}; - use libdd_capabilities::MaybeSend; - use std::future::Future; + use libdd_capabilities::spawn::SpawnCapability; + use libdd_capabilities_impl::NativeSpawnCapability; use std::sync::atomic::Ordering; use tokio::runtime::{Builder, Runtime}; @@ -40,40 +37,6 @@ mod native { .build() } - /// Internal spawner for fork recovery paths. - /// - /// The runtime handle is passed at spawn time via [`SpawnCapability::RuntimeContext`], - /// not stored, so the spawner is always using the current (potentially rebuilt) runtime. - #[derive(Clone, Debug)] - pub(crate) struct RuntimeSpawner; - - pub(crate) struct RuntimeJoinHandle(tokio::task::JoinHandle); - - impl Future for RuntimeJoinHandle { - type Output = Result; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match Pin::new(&mut self.get_mut().0).poll(cx) { - Poll::Ready(Ok(val)) => Poll::Ready(Ok(val)), - Poll::Ready(Err(e)) => Poll::Ready(Err(SpawnError::new(e.to_string()))), - Poll::Pending => Poll::Pending, - } - } - } - - impl SpawnCapability for RuntimeSpawner { - type RuntimeContext = tokio::runtime::Handle; - type JoinHandle = RuntimeJoinHandle; - - fn spawn(&self, future: F, ctx: &tokio::runtime::Handle) -> RuntimeJoinHandle - where - F: Future + MaybeSend + 'static, - T: MaybeSend + 'static, - { - RuntimeJoinHandle(ctx.spawn(future)) - } - } - impl SharedRuntime { pub(in super::super) fn new_native() -> Result { Ok(Self { @@ -204,7 +167,7 @@ mod native { .clone(); drop(runtime_lock); - let spawner = RuntimeSpawner; + let spawner = NativeSpawnCapability; let mut workers_lock = self.workers.lock_or_panic(); for worker_entry in workers_lock.iter_mut() { @@ -234,7 +197,7 @@ mod native { .clone(); drop(runtime_lock); - let spawner = RuntimeSpawner; + let spawner = NativeSpawnCapability; let mut workers_lock = self.workers.lock_or_panic(); workers_lock.retain(|entry| entry.restart_on_fork); @@ -297,9 +260,6 @@ mod native { } } -#[cfg(all(not(target_arch = "wasm32"), test))] -pub(crate) use native::RuntimeSpawner; - type BoxedWorker = Box; #[derive(Debug)] @@ -533,6 +493,7 @@ impl SharedRuntime { mod tests { use super::*; use async_trait::async_trait; + use libdd_capabilities_impl::NativeSpawnCapability; use std::sync::mpsc::{channel, Receiver, Sender}; use std::time::Duration; use tokio::time::sleep; @@ -578,7 +539,7 @@ mod tests { #[test] fn test_spawn_worker() { let shared_runtime = SharedRuntime::new().unwrap(); - let spawner = RuntimeSpawner; + let spawner = NativeSpawnCapability; let (worker, receiver) = make_test_worker(); let result = shared_runtime.spawn_worker(worker, true, &spawner); @@ -598,7 +559,7 @@ mod tests { fn test_worker_handle_stop() { let rt = tokio::runtime::Runtime::new().unwrap(); let shared_runtime = SharedRuntime::new().unwrap(); - let spawner = RuntimeSpawner; + let spawner = NativeSpawnCapability; let (worker, receiver) = make_test_worker(); let handle = shared_runtime.spawn_worker(worker, true, &spawner).unwrap(); @@ -628,7 +589,7 @@ mod tests { #[test] fn test_before_and_after_fork_parent() { let shared_runtime = SharedRuntime::new().unwrap(); - let spawner = RuntimeSpawner; + let spawner = NativeSpawnCapability; let (worker, receiver) = make_test_worker(); shared_runtime.spawn_worker(worker, true, &spawner).unwrap(); @@ -660,7 +621,7 @@ mod tests { #[test] fn test_after_fork_child() { let shared_runtime = SharedRuntime::new().unwrap(); - let spawner = RuntimeSpawner; + let spawner = NativeSpawnCapability; let (worker, receiver) = make_test_worker(); shared_runtime.spawn_worker(worker, true, &spawner).unwrap(); @@ -692,7 +653,7 @@ mod tests { #[test] fn test_shutdown() { let shared_runtime = SharedRuntime::new().unwrap(); - let spawner = RuntimeSpawner; + let spawner = NativeSpawnCapability; let (worker, receiver) = make_test_worker(); shared_runtime.spawn_worker(worker, true, &spawner).unwrap(); @@ -717,7 +678,7 @@ mod tests { #[test] fn test_after_fork_child_drops_worker_not_restart_on_fork() { let shared_runtime = SharedRuntime::new().unwrap(); - let spawner = RuntimeSpawner; + let spawner = NativeSpawnCapability; let (worker, receiver) = make_test_worker(); shared_runtime diff --git a/libdd-shared-runtime/src/shared_runtime/pausable_worker.rs b/libdd-shared-runtime/src/shared_runtime/pausable_worker.rs index b5ae7e1ff1..27043f7248 100644 --- a/libdd-shared-runtime/src/shared_runtime/pausable_worker.rs +++ b/libdd-shared-runtime/src/shared_runtime/pausable_worker.rs @@ -202,7 +202,7 @@ mod tests { use tokio::{runtime::Builder, time::sleep}; use super::*; - use crate::shared_runtime::RuntimeSpawner; + use libdd_capabilities_impl::NativeSpawnCapability; use std::{ sync::mpsc::{channel, Sender}, time::Duration, @@ -233,7 +233,7 @@ mod tests { let worker = TestWorker { state: 0, sender }; let runtime = Builder::new_multi_thread().enable_time().build().unwrap(); let handle = runtime.handle().clone(); - let spawner = RuntimeSpawner; + let spawner = NativeSpawnCapability; let mut pausable_worker = PausableWorker::new(worker); pausable_worker.start(&spawner, &handle).unwrap(); From c9f97e274467648c20607ad7d81083e85df7dbee Mon Sep 17 00:00:00 2001 From: Jules Wiriath Date: Thu, 30 Apr 2026 16:58:43 +0200 Subject: [PATCH 06/14] fix: remove spawner --- Cargo.lock | 2 + libdd-capabilities-impl/README.md | 5 +- libdd-capabilities-impl/src/lib.rs | 24 +---- libdd-capabilities-impl/src/spawn.rs | 45 ++------ libdd-capabilities/src/lib.rs | 2 +- libdd-capabilities/src/spawn.rs | 29 +---- libdd-data-pipeline/benches/trace_buffer.rs | 4 +- libdd-data-pipeline/src/agent_info/fetcher.rs | 17 +-- libdd-data-pipeline/src/telemetry/mod.rs | 5 +- libdd-data-pipeline/src/trace_buffer/mod.rs | 36 ++----- .../src/trace_exporter/builder.rs | 27 ++--- libdd-data-pipeline/src/trace_exporter/mod.rs | 26 ++--- .../src/trace_exporter/stats.rs | 27 ++--- libdd-shared-runtime/Cargo.toml | 4 + libdd-shared-runtime/src/lib.rs | 18 ---- .../src/shared_runtime/mod.rs | 75 ++++++------- .../src/shared_runtime/pausable_worker.rs | 101 +++++++++--------- libdd-trace-stats/src/stats_exporter.rs | 4 +- 18 files changed, 145 insertions(+), 306 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e2ddfc9264..8fd4a2f3e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3233,12 +3233,14 @@ version = "0.1.0" dependencies = [ "async-trait", "futures", + "futures-util", "libdd-capabilities", "libdd-capabilities-impl", "libdd-common", "tokio", "tokio-util", "tracing", + "wasm-bindgen-futures", ] [[package]] diff --git a/libdd-capabilities-impl/README.md b/libdd-capabilities-impl/README.md index 47dcdacf7a..030b3cb23d 100644 --- a/libdd-capabilities-impl/README.md +++ b/libdd-capabilities-impl/README.md @@ -10,11 +10,12 @@ Native implementations of `libdd-capabilities` traits. - **`NativeHttpClient`**: HTTP client backed by hyper and the `libdd-common` connector infrastructure (supports Unix sockets, HTTPS with rustls, Windows named pipes). - **`NativeSleepCapability`**: Sleep backed by `tokio::time::sleep`. -- **`NativeSpawnCapability`**: Task spawning backed by `tokio::runtime::Handle::spawn`. + +Task spawning is handled internally by `SharedRuntime` and is not exposed as a capability trait. ## Types -- **`NativeCapabilities`**: Bundle struct that implements all capability traits using native backends. Delegates to `NativeHttpClient`, `NativeSleepCapability`, and `NativeSpawnCapability`. +- **`NativeCapabilities`**: Bundle struct that implements HTTP and sleep capability traits using native backends. Delegates to `NativeHttpClient` and `NativeSleepCapability`. ## Usage diff --git a/libdd-capabilities-impl/src/lib.rs b/libdd-capabilities-impl/src/lib.rs index 4031fc0c2e..35ecf8c062 100644 --- a/libdd-capabilities-impl/src/lib.rs +++ b/libdd-capabilities-impl/src/lib.rs @@ -16,14 +16,14 @@ use std::time::Duration; pub use http::NativeHttpClient; use libdd_capabilities::{http::HttpError, MaybeSend}; -pub use libdd_capabilities::{HttpClientCapability, SleepCapability, SpawnCapability}; +pub use libdd_capabilities::{HttpClientCapability, SleepCapability}; pub use sleep::NativeSleepCapability; -pub use spawn::{NativeJoinHandle, NativeSpawnCapability}; +pub use spawn::NativeSpawnCapability; // kept for backwards compatibility /// Bundle struct for native platform capabilities. /// -/// Delegates to [`NativeHttpClient`] for HTTP, [`NativeSleepCapability`] for -/// sleep, and [`NativeSpawnCapability`] for task spawning. +/// Delegates to [`NativeHttpClient`] for HTTP and [`NativeSleepCapability`] for +/// sleep. Task spawning is handled internally by `SharedRuntime`. /// /// Individual capability traits keep minimal per-function bounds (e.g. /// functions that only need HTTP require just `H: HttpClientCapability`, not the @@ -33,7 +33,6 @@ pub use spawn::{NativeJoinHandle, NativeSpawnCapability}; pub struct NativeCapabilities { http: NativeHttpClient, sleep: NativeSleepCapability, - spawn: NativeSpawnCapability, } impl Default for NativeCapabilities { @@ -47,7 +46,6 @@ impl NativeCapabilities { Self { http: NativeHttpClient::new_client(), sleep: NativeSleepCapability, - spawn: NativeSpawnCapability, } } } @@ -57,7 +55,6 @@ impl HttpClientCapability for NativeCapabilities { Self { http: NativeHttpClient::new_client(), sleep: NativeSleepCapability, - spawn: NativeSpawnCapability, } } @@ -74,16 +71,3 @@ impl SleepCapability for NativeCapabilities { self.sleep.sleep(duration) } } - -impl SpawnCapability for NativeCapabilities { - type RuntimeContext = tokio::runtime::Handle; - type JoinHandle = NativeJoinHandle; - - fn spawn(&self, future: F, ctx: &tokio::runtime::Handle) -> NativeJoinHandle - where - F: Future + MaybeSend + 'static, - T: MaybeSend + 'static, - { - self.spawn.spawn(future, ctx) - } -} diff --git a/libdd-capabilities-impl/src/spawn.rs b/libdd-capabilities-impl/src/spawn.rs index be71879e49..e61d8d297d 100644 --- a/libdd-capabilities-impl/src/spawn.rs +++ b/libdd-capabilities-impl/src/spawn.rs @@ -1,45 +1,12 @@ // Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 -//! Native spawn implementation backed by `tokio::runtime::Handle::spawn`. - -use core::future::Future; -use core::pin::Pin; -use core::task::{Context, Poll}; - -use libdd_capabilities::maybe_send::MaybeSend; -use libdd_capabilities::spawn::{SpawnCapability, SpawnError}; -use tokio::task::JoinHandle; +//! Native spawn implementation is now handled internally by `SharedRuntime`. +//! +//! This module is kept for backwards compatibility but the type is no longer +//! used by capability bundles or consumer code. +/// Marker type retained for backwards compatibility. +/// Task spawning is now handled internally by `SharedRuntime`. #[derive(Clone, Debug)] pub struct NativeSpawnCapability; - -impl SpawnCapability for NativeSpawnCapability { - type RuntimeContext = tokio::runtime::Handle; - type JoinHandle = NativeJoinHandle; - - fn spawn(&self, future: F, ctx: &tokio::runtime::Handle) -> NativeJoinHandle - where - F: Future + MaybeSend + 'static, - T: MaybeSend + 'static, - { - NativeJoinHandle(ctx.spawn(future)) - } -} - -/// Newtype wrapping `tokio::task::JoinHandle` that surfaces -/// `Result`, mapping tokio's `JoinError` (panic / abort) -/// into the executor-agnostic [`SpawnError`] from `libdd-capabilities`. -pub struct NativeJoinHandle(JoinHandle); - -impl Future for NativeJoinHandle { - type Output = Result; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match Pin::new(&mut self.get_mut().0).poll(cx) { - Poll::Ready(Ok(val)) => Poll::Ready(Ok(val)), - Poll::Ready(Err(e)) => Poll::Ready(Err(SpawnError::new(e.to_string()))), - Poll::Pending => Poll::Pending, - } - } -} diff --git a/libdd-capabilities/src/lib.rs b/libdd-capabilities/src/lib.rs index c6e21f5dce..340fbf2c6c 100644 --- a/libdd-capabilities/src/lib.rs +++ b/libdd-capabilities/src/lib.rs @@ -10,7 +10,7 @@ pub mod spawn; pub use self::http::{HttpClientCapability, HttpError}; pub use self::sleep::SleepCapability; -pub use self::spawn::{SpawnCapability, SpawnError}; +pub use self::spawn::SpawnError; pub use ::http::{Request, Response}; pub use bytes::Bytes; pub use maybe_send::MaybeSend; diff --git a/libdd-capabilities/src/spawn.rs b/libdd-capabilities/src/spawn.rs index 8076306a90..fffb1ef967 100644 --- a/libdd-capabilities/src/spawn.rs +++ b/libdd-capabilities/src/spawn.rs @@ -1,15 +1,12 @@ // Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 -//! Spawn capability trait. +//! Spawn-related types shared across platforms. //! -//! Abstracts task spawning so that native code can use `tokio::spawn` -//! while wasm delegates to `wasm_bindgen_futures::spawn_local` with a -//! `RemoteHandle` for join/cancel semantics. +//! Task spawning is handled internally by `SharedRuntime`; this module only +//! provides the executor-agnostic [`SpawnError`] type used in join handles. -use crate::maybe_send::MaybeSend; use core::fmt; -use core::future::Future; /// Executor-agnostic error returned when a spawned task is aborted or panics. #[derive(Debug)] @@ -30,23 +27,3 @@ impl fmt::Display for SpawnError { } impl core::error::Error for SpawnError {} - -pub trait SpawnCapability: Clone + std::fmt::Debug { - /// Platform-specific context passed to [`spawn`](Self::spawn). - /// - /// On native this is typically `tokio::runtime::Handle` — the spawner uses - /// it to schedule the future on the correct runtime. On wasm this is `()` - /// because `spawn_local` does not need an external handle. - type RuntimeContext; - - /// Handle to a spawned task. - /// - /// Awaiting the handle yields `Ok(T)` on success, or `Err(SpawnError)` if - /// the task panicked or was aborted. - type JoinHandle: Future> + MaybeSend; - - fn spawn(&self, future: F, ctx: &Self::RuntimeContext) -> Self::JoinHandle - where - F: Future + MaybeSend + 'static, - T: MaybeSend + 'static; -} diff --git a/libdd-data-pipeline/benches/trace_buffer.rs b/libdd-data-pipeline/benches/trace_buffer.rs index 634542ee34..5f180a28f1 100644 --- a/libdd-data-pipeline/benches/trace_buffer.rs +++ b/libdd-data-pipeline/benches/trace_buffer.rs @@ -6,7 +6,6 @@ use std::sync::Arc; use std::time::Duration; use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; -use libdd_capabilities_impl::NativeCapabilities; use libdd_data_pipeline::trace_buffer::{Export, TraceBuffer, TraceBufferConfig, TraceChunk}; use libdd_data_pipeline::trace_exporter::{ agent_response::AgentResponse, error::TraceExporterError, @@ -45,8 +44,7 @@ fn setup_buffer() -> (Arc, Arc>) { .span_flush_threshold(500) .max_flush_interval(Duration::from_secs(2)); let (buf, worker) = TraceBuffer::new(cfg, Box::new(|_| {}), Box::new(SleepExport)); - rt.spawn_worker(worker, true, &NativeCapabilities::new()) - .expect("spawn_worker"); + rt.spawn_worker(worker, true).expect("spawn_worker"); (rt, Arc::new(buf)) } diff --git a/libdd-data-pipeline/src/agent_info/fetcher.rs b/libdd-data-pipeline/src/agent_info/fetcher.rs index 87fbc8b4be..0776003317 100644 --- a/libdd-data-pipeline/src/agent_info/fetcher.rs +++ b/libdd-data-pipeline/src/agent_info/fetcher.rs @@ -167,7 +167,7 @@ async fn fetch_and_hash_response( /// ); /// // Start the fetcher on a shared runtime /// let runtime = libdd_shared_runtime::SharedRuntime::new()?; -/// runtime.spawn_worker(fetcher, true, &capabilities)?; +/// runtime.spawn_worker(fetcher, true)?; /// /// // Get the Arc to access the info /// let agent_info_arc = agent_info::get_agent_info(); @@ -580,10 +580,7 @@ mod single_threaded_tests { ); assert!(agent_info::get_agent_info().is_none()); let shared_runtime = SharedRuntime::new().unwrap(); - let spawner = NativeCapabilities::new(); - shared_runtime - .spawn_worker(fetcher, true, &spawner) - .unwrap(); + shared_runtime.spawn_worker(fetcher, true).unwrap(); // Wait until the info is fetched let start = std::time::Instant::now(); @@ -666,10 +663,7 @@ mod single_threaded_tests { AgentInfoFetcher::::new(endpoint, Duration::from_secs(3600)); let shared_runtime = SharedRuntime::new().unwrap(); - let spawner = NativeCapabilities::new(); - shared_runtime - .spawn_worker(fetcher, true, &spawner) - .unwrap(); + shared_runtime.spawn_worker(fetcher, true).unwrap(); // Create a mock HTTP response with the new agent state let response = http::Response::builder() @@ -750,10 +744,7 @@ mod single_threaded_tests { AgentInfoFetcher::::new(endpoint, Duration::from_secs(3600)); // Very long interval let shared_runtime = SharedRuntime::new().unwrap(); - let spawner = NativeCapabilities::new(); - shared_runtime - .spawn_worker(fetcher, true, &spawner) - .unwrap(); + shared_runtime.spawn_worker(fetcher, true).unwrap(); // Create a mock HTTP response with the same agent state let response = http::Response::builder() diff --git a/libdd-data-pipeline/src/telemetry/mod.rs b/libdd-data-pipeline/src/telemetry/mod.rs index 0cfe5c6b2a..9bf4177b5a 100644 --- a/libdd-data-pipeline/src/telemetry/mod.rs +++ b/libdd-data-pipeline/src/telemetry/mod.rs @@ -328,7 +328,7 @@ mod tests { use httpmock::Method::POST; use httpmock::MockServer; use libdd_capabilities::HttpError; - use libdd_capabilities_impl::NativeCapabilities; + use libdd_shared_runtime::{SharedRuntime, WorkerHandle}; use libdd_trace_utils::test_utils::poll_for_mock_hits; // Use `regex::Regex` directly here because `httpmock`'s `body_matches` @@ -350,9 +350,8 @@ mod tests { .set_heartbeat(100) .set_debug_enabled(true) .build(); - let spawner = NativeCapabilities::new(); let handle = runtime - .spawn_worker(worker, true, &spawner) + .spawn_worker(worker, true) .expect("Failed to spawn worker"); (client, handle) } diff --git a/libdd-data-pipeline/src/trace_buffer/mod.rs b/libdd-data-pipeline/src/trace_buffer/mod.rs index d6b169476a..f3239abc3f 100644 --- a/libdd-data-pipeline/src/trace_buffer/mod.rs +++ b/libdd-data-pipeline/src/trace_buffer/mod.rs @@ -13,8 +13,7 @@ use std::{ time::{Duration, Instant}, }; -use libdd_capabilities::{HttpClientCapability, MaybeSend, SleepCapability, SpawnCapability}; -use libdd_shared_runtime::SpawnRuntimeContext; +use libdd_capabilities::{HttpClientCapability, MaybeSend, SleepCapability}; use libdd_shared_runtime::Worker; use crate::trace_exporter::{ @@ -569,39 +568,18 @@ pub trait Export: Send + Debug { } #[derive(Debug)] -pub struct DefaultExport< - C: HttpClientCapability - + SleepCapability - + SpawnCapability - + MaybeSend - + Sync - + 'static, -> { +pub struct DefaultExport { trace_exporter: TraceExporter, } -impl< - C: HttpClientCapability - + SleepCapability - + SpawnCapability - + MaybeSend - + Sync - + 'static, - > DefaultExport -{ +impl DefaultExport { pub fn new(trace_exporter: TraceExporter) -> Self { Self { trace_exporter } } } -impl< - C: HttpClientCapability - + SleepCapability - + SpawnCapability - + MaybeSend - + Sync - + 'static, - > Export for DefaultExport +impl + Export for DefaultExport { fn export_trace_chunks( &mut self, @@ -723,7 +701,6 @@ mod tests { use std::sync::Arc; use std::time::Duration; - use libdd_capabilities_impl::NativeCapabilities; use libdd_shared_runtime::SharedRuntime; use crate::trace_buffer::{Export, TraceBuffer, TraceBufferConfig}; @@ -777,8 +754,7 @@ mod tests { ), Box::new(AssertExporter(assert_export, sem.clone())), ); - rt.spawn_worker(worker, true, &NativeCapabilities::new()) - .unwrap(); + rt.spawn_worker(worker, true).unwrap(); (rt, sem, sender) } diff --git a/libdd-data-pipeline/src/trace_exporter/builder.rs b/libdd-data-pipeline/src/trace_exporter/builder.rs index eba1418b53..bd2e330602 100644 --- a/libdd-data-pipeline/src/trace_exporter/builder.rs +++ b/libdd-data-pipeline/src/trace_exporter/builder.rs @@ -18,10 +18,10 @@ use crate::trace_exporter::{ TracerMetadata, INFO_ENDPOINT, }; use arc_swap::ArcSwap; -use libdd_capabilities::{HttpClientCapability, MaybeSend, SleepCapability, SpawnCapability}; +use libdd_capabilities::{HttpClientCapability, MaybeSend, SleepCapability}; use libdd_common::{parse_uri, tag, Endpoint}; use libdd_dogstatsd_client::new; -use libdd_shared_runtime::{SharedRuntime, SpawnRuntimeContext}; +use libdd_shared_runtime::SharedRuntime; use std::sync::Arc; use std::time::Duration; @@ -274,14 +274,7 @@ impl TraceExporterBuilder { } #[allow(missing_docs)] - pub fn build< - C: HttpClientCapability - + SleepCapability - + SpawnCapability - + MaybeSend - + Sync - + 'static, - >( + pub fn build( self, ) -> Result, TraceExporterError> { if !Self::is_inputs_outputs_formats_compatible(self.input_format, self.output_format) { @@ -337,7 +330,7 @@ impl TraceExporterBuilder { let (info_fetcher, observer) = AgentInfoFetcher::::new(info_endpoint.clone(), Duration::from_secs(5 * 60)); let handle = shared_runtime - .spawn_worker(info_fetcher, false, &capabilities) + .spawn_worker(info_fetcher, false) .map_err(|e| { TraceExporterError::Builder(BuilderErrorKind::InvalidConfiguration( e.to_string(), @@ -388,13 +381,11 @@ impl TraceExporterBuilder { }); match telemetry { Some(Ok((client_tel, worker))) => { - let handle = shared_runtime - .spawn_worker(worker, false, &capabilities) - .map_err(|e| { - TraceExporterError::Builder(BuilderErrorKind::InvalidConfiguration( - e.to_string(), - )) - })?; + let handle = shared_runtime.spawn_worker(worker, false).map_err(|e| { + TraceExporterError::Builder(BuilderErrorKind::InvalidConfiguration( + e.to_string(), + )) + })?; shared_runtime.block_on(client_tel.start()).map_err(|e| { TraceExporterError::Builder(BuilderErrorKind::InvalidConfiguration( e.to_string(), diff --git a/libdd-data-pipeline/src/trace_exporter/mod.rs b/libdd-data-pipeline/src/trace_exporter/mod.rs index 07b594ce78..95c13889c7 100644 --- a/libdd-data-pipeline/src/trace_exporter/mod.rs +++ b/libdd-data-pipeline/src/trace_exporter/mod.rs @@ -34,11 +34,11 @@ use bytes::Bytes; use http::header::HeaderMap; use http::uri::PathAndQuery; use http::Uri; -use libdd_capabilities::{HttpClientCapability, MaybeSend, SleepCapability, SpawnCapability}; +use libdd_capabilities::{HttpClientCapability, MaybeSend, SleepCapability}; use libdd_common::tag::Tag; use libdd_common::Endpoint; use libdd_dogstatsd_client::Client; -use libdd_shared_runtime::{SharedRuntime, SpawnRuntimeContext, WorkerHandle}; +use libdd_shared_runtime::{SharedRuntime, WorkerHandle}; use libdd_trace_utils::msgpack_decoder; use libdd_trace_utils::send_with_retry::{ send_with_retry, RetryStrategy, SendWithRetryError, SendWithRetryResult, @@ -207,17 +207,11 @@ impl From for DeserInputFormat { } } -/// `C` is the capabilities bundle (HTTP, sleep, spawn). Leaf crates +/// `C` is the capabilities bundle (HTTP, sleep). Leaf crates /// pin it to a concrete type (`NativeCapabilities` or `WasmCapabilities`). +/// Task spawning is handled internally by `SharedRuntime`. #[derive(Debug)] -pub struct TraceExporter< - C: HttpClientCapability - + SleepCapability - + SpawnCapability - + MaybeSend - + Sync - + 'static, -> { +pub struct TraceExporter { endpoint: Endpoint, metadata: TracerMetadata, input_format: TraceExporterInputFormat, @@ -243,15 +237,7 @@ pub struct TraceExporter< otlp_config: Option, } -impl< - C: HttpClientCapability - + SleepCapability - + SpawnCapability - + MaybeSend - + Sync - + 'static, - > TraceExporter -{ +impl TraceExporter { #[allow(missing_docs)] pub fn builder() -> TraceExporterBuilder { TraceExporterBuilder::default() diff --git a/libdd-data-pipeline/src/trace_exporter/stats.rs b/libdd-data-pipeline/src/trace_exporter/stats.rs index 96584a6558..9f63597de1 100644 --- a/libdd-data-pipeline/src/trace_exporter/stats.rs +++ b/libdd-data-pipeline/src/trace_exporter/stats.rs @@ -10,11 +10,11 @@ #[cfg(not(target_arch = "wasm32"))] use crate::agent_info::schema::AgentInfo; use arc_swap::ArcSwap; -use libdd_capabilities::{HttpClientCapability, MaybeSend, SleepCapability, SpawnCapability}; +use libdd_capabilities::{HttpClientCapability, MaybeSend, SleepCapability}; #[cfg(not(target_arch = "wasm32"))] use libdd_common::Endpoint; use libdd_common::MutexExt; -use libdd_shared_runtime::{SharedRuntime, SpawnRuntimeContext, WorkerHandle}; +use libdd_shared_runtime::{SharedRuntime, WorkerHandle}; use libdd_trace_stats::span_concentrator::SpanConcentrator; #[cfg(not(target_arch = "wasm32"))] use libdd_trace_stats::stats_exporter::{StatsExporter, StatsMetadata}; @@ -72,12 +72,7 @@ fn get_span_kinds_for_stats(agent_info: &Arc) -> Vec { /// /// Should only be used if the agent enabled stats computation pub(crate) fn start_stats_computation< - C: HttpClientCapability - + SleepCapability - + SpawnCapability - + MaybeSend - + Sync - + 'static, + C: HttpClientCapability + SleepCapability + MaybeSend + Sync + 'static, >( ctx: &StatsContext, client_side_stats: &ArcSwap, @@ -106,12 +101,7 @@ pub(crate) fn start_stats_computation< #[cfg(not(target_arch = "wasm32"))] /// Create stats exporter and worker, start the worker, and update the state fn create_and_start_stats_worker< - C: HttpClientCapability - + SleepCapability - + SpawnCapability - + MaybeSend - + Sync - + 'static, + C: HttpClientCapability + SleepCapability + MaybeSend + Sync + 'static, >( ctx: &StatsContext, bucket_size: Duration, @@ -128,7 +118,7 @@ fn create_and_start_stats_worker< ); let worker_handle = ctx .shared_runtime - .spawn_worker(stats_exporter, false, &capabilities) + .spawn_worker(stats_exporter, false) .map_err(|e| anyhow::anyhow!(e))?; // Update the stats computation state with the new worker components. @@ -168,12 +158,7 @@ pub(crate) fn stop_stats_computation( #[cfg(not(target_arch = "wasm32"))] /// Handle stats computation when agent changes from disabled to enabled pub(crate) fn handle_stats_disabled_by_agent< - C: HttpClientCapability - + SleepCapability - + SpawnCapability - + MaybeSend - + Sync - + 'static, + C: HttpClientCapability + SleepCapability + MaybeSend + Sync + 'static, >( ctx: &StatsContext, agent_info: &Arc, diff --git a/libdd-shared-runtime/Cargo.toml b/libdd-shared-runtime/Cargo.toml index 0beb449d1f..81a1cc0be3 100644 --- a/libdd-shared-runtime/Cargo.toml +++ b/libdd-shared-runtime/Cargo.toml @@ -29,3 +29,7 @@ regex-lite = ["libdd-common/regex-lite"] [target.'cfg(not(target_arch = "wasm32"))'.dependencies] libdd-capabilities-impl = { path = "../libdd-capabilities-impl", version = "1.0.0" } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen-futures = "0.4" +futures-util = { version = "0.3", default-features = false, features = ["channel"] } diff --git a/libdd-shared-runtime/src/lib.rs b/libdd-shared-runtime/src/lib.rs index 56f2796477..e9dc0ee642 100644 --- a/libdd-shared-runtime/src/lib.rs +++ b/libdd-shared-runtime/src/lib.rs @@ -22,21 +22,3 @@ pub mod worker; // Top-level re-exports for convenience pub use shared_runtime::{SharedRuntime, SharedRuntimeError, WorkerHandle, WorkerHandleError}; pub use worker::Worker; - -/// The concrete [`SpawnCapability::RuntimeContext`] type for the current platform. -/// -/// On native this is `tokio::runtime::Handle`; on wasm it is `()`. -/// Generic code that calls [`SharedRuntime::spawn_worker`] should bound -/// `C: SpawnCapability` so the constraint -/// is satisfied on every target without `#[cfg]` on individual where clauses. -#[cfg(not(target_arch = "wasm32"))] -use libdd_capabilities::SpawnCapability; -#[cfg(not(target_arch = "wasm32"))] -use libdd_capabilities_impl::NativeSpawnCapability; - -#[cfg(not(target_arch = "wasm32"))] -pub type SpawnRuntimeContext = ::RuntimeContext; - -/// See the non-wasm variant for documentation. -#[cfg(target_arch = "wasm32")] -pub type SpawnRuntimeContext = (); diff --git a/libdd-shared-runtime/src/shared_runtime/mod.rs b/libdd-shared-runtime/src/shared_runtime/mod.rs index 82f1f3cc56..1e64e89260 100644 --- a/libdd-shared-runtime/src/shared_runtime/mod.rs +++ b/libdd-shared-runtime/src/shared_runtime/mod.rs @@ -25,8 +25,7 @@ use tracing::{debug, error}; #[cfg(not(target_arch = "wasm32"))] mod native { use super::*; - use libdd_capabilities::spawn::SpawnCapability; - use libdd_capabilities_impl::NativeSpawnCapability; + use libdd_capabilities::spawn::SpawnError; use std::sync::atomic::Ordering; use tokio::runtime::{Builder, Runtime}; @@ -60,7 +59,7 @@ mod native { .clone()) } - /// Spawn a PausableWorker using the provided spawn capability. + /// Spawn a PausableWorker on this runtime. /// /// The worker will be tracked by this SharedRuntime and will be paused/resumed /// during fork operations (native only). @@ -69,14 +68,10 @@ mod native { /// /// # Errors /// Returns an error if the worker cannot be started. - pub fn spawn_worker< - T: Worker + Sync + 'static, - S: SpawnCapability + 'static, - >( + pub fn spawn_worker( &self, worker: T, restart_on_fork: bool, - spawner: &S, ) -> Result { let boxed_worker: BoxedWorker = Box::new(worker); debug!(?boxed_worker, "Spawning worker on SharedRuntime"); @@ -94,7 +89,11 @@ mod native { let mut workers_guard = self.workers.lock_or_panic(); if let Some(ref handle) = runtime_handle { - if let Err(e) = pausable_worker.start(spawner, handle) { + let h = handle.clone(); + if let Err(e) = pausable_worker.start(|future| { + let jh = h.spawn(future); + Box::pin(async { jh.await.map_err(|e| SpawnError::new(e.to_string())) }) + }) { return Err(e.into()); } } @@ -167,11 +166,14 @@ mod native { .clone(); drop(runtime_lock); - let spawner = NativeSpawnCapability; let mut workers_lock = self.workers.lock_or_panic(); for worker_entry in workers_lock.iter_mut() { - worker_entry.worker.start(&spawner, &handle)?; + let h = handle.clone(); + worker_entry.worker.start(|future| { + let jh = h.spawn(future); + Box::pin(async { jh.await.map_err(|e| SpawnError::new(e.to_string())) }) + })?; } Ok(()) @@ -197,14 +199,17 @@ mod native { .clone(); drop(runtime_lock); - let spawner = NativeSpawnCapability; let mut workers_lock = self.workers.lock_or_panic(); workers_lock.retain(|entry| entry.restart_on_fork); for worker_entry in workers_lock.iter_mut() { worker_entry.worker.reset(); - worker_entry.worker.start(&spawner, &handle)?; + let h = handle.clone(); + worker_entry.worker.start(|future| { + let jh = h.spawn(future); + Box::pin(async { jh.await.map_err(|e| SpawnError::new(e.to_string())) }) + })?; } Ok(()) @@ -384,8 +389,8 @@ impl From for SharedRuntimeError { /// spawned on it. It provides methods to safely pause workers before forking and /// restart them after fork in both parent and child processes. /// -/// On wasm32, no tokio runtime is created. Workers are spawned via the caller-provided -/// [`SpawnCapability`] which delegates to `spawn_local` on the JS event loop. +/// On wasm32, no tokio runtime is created. Workers are spawned via `spawn_local` +/// on the JS event loop. /// /// # Mutex lock order /// When locking both [Self::runtime] and [Self::workers], the mutex must be locked in the order of @@ -402,7 +407,7 @@ impl SharedRuntime { /// Create a new SharedRuntime. /// /// On native, this creates a tokio multi-thread runtime. On wasm32, no runtime - /// is created (workers are spawned on the JS event loop via [`SpawnCapability`]). + /// is created (workers are spawned on the JS event loop via `spawn_local`). /// /// # Errors /// Returns an error if the tokio runtime cannot be created (native only). @@ -422,18 +427,12 @@ impl SharedRuntime { } } - /// Spawn a PausableWorker using the provided spawn capability (wasm variant). - /// - /// On wasm the runtime context is `()` — workers are spawned on the JS event loop. + /// Spawn a PausableWorker on the JS event loop (wasm variant). #[cfg(target_arch = "wasm32")] - pub fn spawn_worker< - T: Worker + Sync + 'static, - S: libdd_capabilities::spawn::SpawnCapability + 'static, - >( + pub fn spawn_worker( &self, worker: T, restart_on_fork: bool, - spawner: &S, ) -> Result { use std::sync::atomic::Ordering; @@ -443,7 +442,12 @@ impl SharedRuntime { let mut workers_guard = self.workers.lock_or_panic(); - if let Err(e) = pausable_worker.start(spawner, &()) { + if let Err(e) = pausable_worker.start(|future| { + use futures_util::FutureExt; + let (remote, handle) = future.remote_handle(); + wasm_bindgen_futures::spawn_local(remote); + Box::pin(async { Ok(handle.await) }) + }) { return Err(e.into()); } @@ -493,7 +497,6 @@ impl SharedRuntime { mod tests { use super::*; use async_trait::async_trait; - use libdd_capabilities_impl::NativeSpawnCapability; use std::sync::mpsc::{channel, Receiver, Sender}; use std::time::Duration; use tokio::time::sleep; @@ -539,10 +542,9 @@ mod tests { #[test] fn test_spawn_worker() { let shared_runtime = SharedRuntime::new().unwrap(); - let spawner = NativeSpawnCapability; let (worker, receiver) = make_test_worker(); - let result = shared_runtime.spawn_worker(worker, true, &spawner); + let result = shared_runtime.spawn_worker(worker, true); assert!(result.is_ok()); assert_eq!(shared_runtime.workers.lock_or_panic().len(), 1); @@ -559,10 +561,9 @@ mod tests { fn test_worker_handle_stop() { let rt = tokio::runtime::Runtime::new().unwrap(); let shared_runtime = SharedRuntime::new().unwrap(); - let spawner = NativeSpawnCapability; let (worker, receiver) = make_test_worker(); - let handle = shared_runtime.spawn_worker(worker, true, &spawner).unwrap(); + let handle = shared_runtime.spawn_worker(worker, true).unwrap(); assert_eq!(shared_runtime.workers.lock_or_panic().len(), 1); // Wait for at least one run before stopping @@ -589,10 +590,9 @@ mod tests { #[test] fn test_before_and_after_fork_parent() { let shared_runtime = SharedRuntime::new().unwrap(); - let spawner = NativeSpawnCapability; let (worker, receiver) = make_test_worker(); - shared_runtime.spawn_worker(worker, true, &spawner).unwrap(); + shared_runtime.spawn_worker(worker, true).unwrap(); // Let the worker run until state > 0 so that preservation is observable let mut state_before_fork = 0; @@ -621,10 +621,9 @@ mod tests { #[test] fn test_after_fork_child() { let shared_runtime = SharedRuntime::new().unwrap(); - let spawner = NativeSpawnCapability; let (worker, receiver) = make_test_worker(); - shared_runtime.spawn_worker(worker, true, &spawner).unwrap(); + shared_runtime.spawn_worker(worker, true).unwrap(); // Let the worker run until state > 0 so that the reset is observable let mut state_before_fork = 0; @@ -653,10 +652,9 @@ mod tests { #[test] fn test_shutdown() { let shared_runtime = SharedRuntime::new().unwrap(); - let spawner = NativeSpawnCapability; let (worker, receiver) = make_test_worker(); - shared_runtime.spawn_worker(worker, true, &spawner).unwrap(); + shared_runtime.spawn_worker(worker, true).unwrap(); // Wait for at least one run before shutting down receiver @@ -678,12 +676,9 @@ mod tests { #[test] fn test_after_fork_child_drops_worker_not_restart_on_fork() { let shared_runtime = SharedRuntime::new().unwrap(); - let spawner = NativeSpawnCapability; let (worker, receiver) = make_test_worker(); - shared_runtime - .spawn_worker(worker, false, &spawner) - .unwrap(); + shared_runtime.spawn_worker(worker, false).unwrap(); // Wait for the worker to run at least once receiver diff --git a/libdd-shared-runtime/src/shared_runtime/pausable_worker.rs b/libdd-shared-runtime/src/shared_runtime/pausable_worker.rs index 27043f7248..fd858c4e53 100644 --- a/libdd-shared-runtime/src/shared_runtime/pausable_worker.rs +++ b/libdd-shared-runtime/src/shared_runtime/pausable_worker.rs @@ -5,7 +5,7 @@ use crate::worker::Worker; use core::pin::Pin; -use libdd_capabilities::spawn::{SpawnCapability, SpawnError}; +use libdd_capabilities::spawn::SpawnError; use libdd_capabilities::MaybeSend; use std::fmt::Display; use std::future::Future; @@ -14,8 +14,12 @@ use tokio_util::sync::CancellationToken; use tracing::debug; #[cfg(not(target_arch = "wasm32"))] -type WorkerJoinHandle = Pin> + Send>>; +type WorkerFuture = Pin + Send + 'static>>; +#[cfg(target_arch = "wasm32")] +type WorkerFuture = Pin + 'static>>; +#[cfg(not(target_arch = "wasm32"))] +type WorkerJoinHandle = Pin> + Send>>; #[cfg(target_arch = "wasm32")] type WorkerJoinHandle = Pin>>>; @@ -75,19 +79,15 @@ impl PausableWorker { Self::Paused { worker } } - /// Start the worker using the given spawn capability. + /// Start the worker using the given spawn function. /// - /// The worker's main loop will be spawned via the provided spawner. - /// `ctx` is the platform-specific runtime context (e.g. `tokio::runtime::Handle` - /// on native, `()` on wasm). - pub fn start( + /// The worker's main loop will be spawned via the provided closure. + /// `SharedRuntime` constructs the appropriate platform-specific closure + /// (tokio on native, spawn_local on wasm). + pub fn start( &mut self, - spawner: &S, - ctx: &S::RuntimeContext, - ) -> Result<(), PausableWorkerError> - where - S::JoinHandle: 'static, - { + spawn_fn: impl FnOnce(WorkerFuture) -> WorkerJoinHandle, + ) -> Result<(), PausableWorkerError> { match self { PausableWorker::Running { .. } => Ok(()), PausableWorker::Paused { worker: _ } => { @@ -99,48 +99,38 @@ impl PausableWorker { return Ok(()); }; - // Worker is temporarily in an invalid state, but since this block is failsafe it - // will be replaced by a valid state. let stop_token = CancellationToken::new(); let cloned_token = stop_token.clone(); - let handle = spawner.spawn( - async move { - // First iteration using initial_trigger + let future = Box::pin(async move { + // First iteration using initial_trigger + select! { + biased; + _ = cloned_token.cancelled() => { + return worker; + } + _ = worker.initial_trigger() => { + worker.run().await; + } + } + + // Regular iterations + loop { select! { - // Always check for cancellation first, to reduce time-to-pause in - // case the initial trigger is always ready. biased; _ = cloned_token.cancelled() => { - return worker; + break; } - _ = worker.initial_trigger() => { + _ = worker.trigger() => { worker.run().await; } } + } + worker + }); - // Regular iterations - loop { - select! { - // Always check for cancellation first, to reduce time-to-pause - // in case the trigger is always ready. - biased; - _ = cloned_token.cancelled() => { - break; - } - _ = worker.trigger() => { - worker.run().await; - } - } - } - worker - }, - ctx, - ); - - *self = PausableWorker::Running { - handle: Box::pin(handle), - stop_token, - }; + let handle = spawn_fn(future); + + *self = PausableWorker::Running { handle, stop_token }; Ok(()) } PausableWorker::InvalidState => Err(PausableWorkerError::InvalidState), @@ -199,15 +189,26 @@ impl PausableWorker { #[cfg(test)] mod tests { use async_trait::async_trait; + use libdd_capabilities::spawn::SpawnError; use tokio::{runtime::Builder, time::sleep}; use super::*; - use libdd_capabilities_impl::NativeSpawnCapability; use std::{ sync::mpsc::{channel, Sender}, time::Duration, }; + fn tokio_spawn_fn( + handle: &tokio::runtime::Handle, + ) -> impl FnOnce(WorkerFuture>) -> WorkerJoinHandle> + { + let h = handle.clone(); + move |future| { + let jh = h.spawn(future); + Box::pin(async { jh.await.map_err(|e| SpawnError::new(e.to_string())) }) + } + } + /// Test worker incrementing the state and sending it with the sender. #[derive(Debug)] struct TestWorker { @@ -233,10 +234,10 @@ mod tests { let worker = TestWorker { state: 0, sender }; let runtime = Builder::new_multi_thread().enable_time().build().unwrap(); let handle = runtime.handle().clone(); - let spawner = NativeSpawnCapability; - let mut pausable_worker = PausableWorker::new(worker); + let mut pausable_worker: PausableWorker> = + PausableWorker::new(Box::new(worker)); - pausable_worker.start(&spawner, &handle).unwrap(); + pausable_worker.start(tokio_spawn_fn(&handle)).unwrap(); assert_eq!(receiver.recv().unwrap(), 0); runtime.block_on(async { pausable_worker.pause().await.unwrap() }); @@ -245,7 +246,7 @@ mod tests { for message in receiver.try_iter() { next_message = message + 1; } - pausable_worker.start(&spawner, &handle).unwrap(); + pausable_worker.start(tokio_spawn_fn(&handle)).unwrap(); assert_eq!(receiver.recv().unwrap(), next_message); } } diff --git a/libdd-trace-stats/src/stats_exporter.rs b/libdd-trace-stats/src/stats_exporter.rs index d8372a024d..a715167b01 100644 --- a/libdd-trace-stats/src/stats_exporter.rs +++ b/libdd-trace-stats/src/stats_exporter.rs @@ -373,7 +373,7 @@ mod tests { caps.clone(), ); let _handle = shared_runtime - .spawn_worker(stats_exporter, true, &caps) + .spawn_worker(stats_exporter, true) .expect("Failed to spawn worker"); // Wait for stats to be flushed @@ -415,7 +415,7 @@ mod tests { ); let _handle = shared_runtime - .spawn_worker(stats_exporter, true, &caps) + .spawn_worker(stats_exporter, true) .expect("Failed to spawn worker"); shared_runtime.shutdown(None).unwrap(); From 23441762ee9cb019be3e8cfaac3a0d5ffe066fdb Mon Sep 17 00:00:00 2001 From: Jules Wiriath Date: Thu, 7 May 2026 15:41:49 +0200 Subject: [PATCH 07/14] fix: remove references to spawn capability --- libdd-capabilities-impl/Cargo.toml | 2 +- libdd-capabilities-impl/src/lib.rs | 4 +--- libdd-capabilities-impl/src/spawn.rs | 12 ------------ 3 files changed, 2 insertions(+), 16 deletions(-) delete mode 100644 libdd-capabilities-impl/src/spawn.rs diff --git a/libdd-capabilities-impl/Cargo.toml b/libdd-capabilities-impl/Cargo.toml index 67f17808bf..0f9fe0bab9 100644 --- a/libdd-capabilities-impl/Cargo.toml +++ b/libdd-capabilities-impl/Cargo.toml @@ -20,7 +20,7 @@ bytes = "1" http = "1" libdd-capabilities = { path = "../libdd-capabilities", version = "1.0.0" } libdd-common = { path = "../libdd-common", version = "4.0.0", default-features = false } -tokio = { version = "1", features = ["time", "rt"] } +tokio = { version = "1", features = ["time"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] http-body-util = "0.1" diff --git a/libdd-capabilities-impl/src/lib.rs b/libdd-capabilities-impl/src/lib.rs index 35ecf8c062..db155d20cd 100644 --- a/libdd-capabilities-impl/src/lib.rs +++ b/libdd-capabilities-impl/src/lib.rs @@ -4,12 +4,11 @@ //! Native capability implementations for libdatadog. //! //! `NativeCapabilities` is the bundle struct that implements all capability -//! traits using platform-native backends (hyper for HTTP, tokio for spawn, +//! traits using platform-native backends (hyper for HTTP, tokio for sleep, //! etc.). Leaf crates (FFI, benchmarks) pin this type as the generic parameter. mod http; pub mod sleep; -pub mod spawn; use core::future::Future; use std::time::Duration; @@ -18,7 +17,6 @@ pub use http::NativeHttpClient; use libdd_capabilities::{http::HttpError, MaybeSend}; pub use libdd_capabilities::{HttpClientCapability, SleepCapability}; pub use sleep::NativeSleepCapability; -pub use spawn::NativeSpawnCapability; // kept for backwards compatibility /// Bundle struct for native platform capabilities. /// diff --git a/libdd-capabilities-impl/src/spawn.rs b/libdd-capabilities-impl/src/spawn.rs deleted file mode 100644 index e61d8d297d..0000000000 --- a/libdd-capabilities-impl/src/spawn.rs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ -// SPDX-License-Identifier: Apache-2.0 - -//! Native spawn implementation is now handled internally by `SharedRuntime`. -//! -//! This module is kept for backwards compatibility but the type is no longer -//! used by capability bundles or consumer code. - -/// Marker type retained for backwards compatibility. -/// Task spawning is now handled internally by `SharedRuntime`. -#[derive(Clone, Debug)] -pub struct NativeSpawnCapability; From e0578e67c963eefa1cb9a45dc118a6a0e3fcb589 Mon Sep 17 00:00:00 2001 From: Jules Wiriath Date: Thu, 7 May 2026 15:57:06 +0200 Subject: [PATCH 08/14] revert: we still need tokio dependancy explicit here --- libdd-shared-runtime/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/libdd-shared-runtime/Cargo.toml b/libdd-shared-runtime/Cargo.toml index 81a1cc0be3..29b9becf06 100644 --- a/libdd-shared-runtime/Cargo.toml +++ b/libdd-shared-runtime/Cargo.toml @@ -29,6 +29,7 @@ regex-lite = ["libdd-common/regex-lite"] [target.'cfg(not(target_arch = "wasm32"))'.dependencies] libdd-capabilities-impl = { path = "../libdd-capabilities-impl", version = "1.0.0" } +tokio = { version = "1.23", features = ["rt-multi-thread"] } [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen-futures = "0.4" From 28e873e4914100bb8cea1fcf53a4dc6023be283d Mon Sep 17 00:00:00 2001 From: Jules Wiriath Date: Thu, 7 May 2026 15:57:48 +0200 Subject: [PATCH 09/14] docs: add comment explaining stuff --- libdd-shared-runtime/src/shared_runtime/mod.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libdd-shared-runtime/src/shared_runtime/mod.rs b/libdd-shared-runtime/src/shared_runtime/mod.rs index 1e64e89260..d632ea833b 100644 --- a/libdd-shared-runtime/src/shared_runtime/mod.rs +++ b/libdd-shared-runtime/src/shared_runtime/mod.rs @@ -80,12 +80,16 @@ mod native { // Lock runtime first to synchronize with before_fork (which does // runtime.take() then workers.lock()), following the documented mutex // lock order. If the runtime has been taken (fork window), skip starting - // the worker, after_fork_parent/child will start it on the new runtime. + // the worker; after_fork_parent/child will start it on the new runtime. let runtime_handle = self .runtime .lock_or_panic() .as_ref() .map(|rt| rt.handle().clone()); + // Hold the workers lock while starting the worker to avoid a race with + // before_fork: without this, before_fork could run after the worker is + // started but before it's added to the list, not pausing the worker + // before the runtime is dropped. let mut workers_guard = self.workers.lock_or_panic(); if let Some(ref handle) = runtime_handle { From a11018bcf5423d75eee29043a9ab1edc64543bf8 Mon Sep 17 00:00:00 2001 From: Jules Wiriath Date: Thu, 7 May 2026 16:03:57 +0200 Subject: [PATCH 10/14] feat: made tokio_spawn_fn a util function for native tasks instead of just a test util --- .../src/shared_runtime/mod.rs | 20 +++----------- .../src/shared_runtime/pausable_worker.rs | 26 ++++++++++--------- 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/libdd-shared-runtime/src/shared_runtime/mod.rs b/libdd-shared-runtime/src/shared_runtime/mod.rs index d632ea833b..dd2c8c42be 100644 --- a/libdd-shared-runtime/src/shared_runtime/mod.rs +++ b/libdd-shared-runtime/src/shared_runtime/mod.rs @@ -25,7 +25,7 @@ use tracing::{debug, error}; #[cfg(not(target_arch = "wasm32"))] mod native { use super::*; - use libdd_capabilities::spawn::SpawnError; + use pausable_worker::tokio_spawn_fn; use std::sync::atomic::Ordering; use tokio::runtime::{Builder, Runtime}; @@ -93,11 +93,7 @@ mod native { let mut workers_guard = self.workers.lock_or_panic(); if let Some(ref handle) = runtime_handle { - let h = handle.clone(); - if let Err(e) = pausable_worker.start(|future| { - let jh = h.spawn(future); - Box::pin(async { jh.await.map_err(|e| SpawnError::new(e.to_string())) }) - }) { + if let Err(e) = pausable_worker.start(tokio_spawn_fn(handle)) { return Err(e.into()); } } @@ -173,11 +169,7 @@ mod native { let mut workers_lock = self.workers.lock_or_panic(); for worker_entry in workers_lock.iter_mut() { - let h = handle.clone(); - worker_entry.worker.start(|future| { - let jh = h.spawn(future); - Box::pin(async { jh.await.map_err(|e| SpawnError::new(e.to_string())) }) - })?; + worker_entry.worker.start(tokio_spawn_fn(&handle))?; } Ok(()) @@ -209,11 +201,7 @@ mod native { for worker_entry in workers_lock.iter_mut() { worker_entry.worker.reset(); - let h = handle.clone(); - worker_entry.worker.start(|future| { - let jh = h.spawn(future); - Box::pin(async { jh.await.map_err(|e| SpawnError::new(e.to_string())) }) - })?; + worker_entry.worker.start(tokio_spawn_fn(&handle))?; } Ok(()) diff --git a/libdd-shared-runtime/src/shared_runtime/pausable_worker.rs b/libdd-shared-runtime/src/shared_runtime/pausable_worker.rs index fd858c4e53..7610fa7725 100644 --- a/libdd-shared-runtime/src/shared_runtime/pausable_worker.rs +++ b/libdd-shared-runtime/src/shared_runtime/pausable_worker.rs @@ -23,6 +23,20 @@ type WorkerJoinHandle = Pin> + #[cfg(target_arch = "wasm32")] type WorkerJoinHandle = Pin>>>; +/// Build the spawn closure used by [`PausableWorker::start`] on native, backed by +/// `tokio::runtime::Handle::spawn`. Maps tokio's `JoinError` into the +/// executor-agnostic [`SpawnError`]. +#[cfg(not(target_arch = "wasm32"))] +pub(super) fn tokio_spawn_fn( + handle: &tokio::runtime::Handle, +) -> impl FnOnce(WorkerFuture) -> WorkerJoinHandle { + let h = handle.clone(); + move |future| { + let jh = h.spawn(future); + Box::pin(async { jh.await.map_err(|e| SpawnError::new(e.to_string())) }) + } +} + /// A pausable worker which can be paused and restarted on forks. /// /// Used to allow a [`super::Worker`] to be paused while saving its state when @@ -189,7 +203,6 @@ impl PausableWorker { #[cfg(test)] mod tests { use async_trait::async_trait; - use libdd_capabilities::spawn::SpawnError; use tokio::{runtime::Builder, time::sleep}; use super::*; @@ -198,17 +211,6 @@ mod tests { time::Duration, }; - fn tokio_spawn_fn( - handle: &tokio::runtime::Handle, - ) -> impl FnOnce(WorkerFuture>) -> WorkerJoinHandle> - { - let h = handle.clone(); - move |future| { - let jh = h.spawn(future); - Box::pin(async { jh.await.map_err(|e| SpawnError::new(e.to_string())) }) - } - } - /// Test worker incrementing the state and sending it with the sender. #[derive(Debug)] struct TestWorker { From 8d462af97b33a79c17b71378db97b72b988d9585 Mon Sep 17 00:00:00 2001 From: Jules Wiriath Date: Thu, 7 May 2026 17:55:40 +0200 Subject: [PATCH 11/14] feat: use sleep capability for the fetcher --- libdd-capabilities-impl/src/lib.rs | 7 +++ libdd-capabilities-impl/src/sleep.rs | 4 ++ libdd-capabilities/src/sleep.rs | 9 +++ libdd-data-pipeline/src/agent_info/fetcher.rs | 63 +++++++++++-------- .../src/trace_exporter/builder.rs | 34 +++++----- 5 files changed, 70 insertions(+), 47 deletions(-) diff --git a/libdd-capabilities-impl/src/lib.rs b/libdd-capabilities-impl/src/lib.rs index db155d20cd..d90d19034b 100644 --- a/libdd-capabilities-impl/src/lib.rs +++ b/libdd-capabilities-impl/src/lib.rs @@ -65,6 +65,13 @@ impl HttpClientCapability for NativeCapabilities { } impl SleepCapability for NativeCapabilities { + fn new() -> Self { + Self { + http: NativeHttpClient::new_client(), + sleep: NativeSleepCapability, + } + } + fn sleep(&self, duration: Duration) -> impl Future + MaybeSend { self.sleep.sleep(duration) } diff --git a/libdd-capabilities-impl/src/sleep.rs b/libdd-capabilities-impl/src/sleep.rs index 2510e335ba..ddf9db7122 100644 --- a/libdd-capabilities-impl/src/sleep.rs +++ b/libdd-capabilities-impl/src/sleep.rs @@ -13,6 +13,10 @@ use libdd_capabilities::sleep::SleepCapability; pub struct NativeSleepCapability; impl SleepCapability for NativeSleepCapability { + fn new() -> Self { + Self + } + fn sleep(&self, duration: Duration) -> impl Future + MaybeSend { tokio::time::sleep(duration) } diff --git a/libdd-capabilities/src/sleep.rs b/libdd-capabilities/src/sleep.rs index 9486193f12..a11e548d06 100644 --- a/libdd-capabilities/src/sleep.rs +++ b/libdd-capabilities/src/sleep.rs @@ -11,5 +11,14 @@ use core::future::Future; use std::time::Duration; pub trait SleepCapability: Clone + std::fmt::Debug { + /// Construct a new sleeper. + /// + /// Stateless impls return a unit struct; stateful impls (mock clocks, + /// virtual time sources, etc.) should return a sensible default. Callers + /// that don't have an instance handy can use the static-style + /// `C::new().sleep(duration)` pattern, mirroring `HttpClientCapability`'s + /// `new_client()` + `request(&self)` shape. + fn new() -> Self; + fn sleep(&self, duration: Duration) -> impl Future + MaybeSend; } diff --git a/libdd-data-pipeline/src/agent_info/fetcher.rs b/libdd-data-pipeline/src/agent_info/fetcher.rs index 0776003317..f13d50c1d9 100644 --- a/libdd-data-pipeline/src/agent_info/fetcher.rs +++ b/libdd-data-pipeline/src/agent_info/fetcher.rs @@ -10,7 +10,7 @@ use super::{ use anyhow::{anyhow, Result}; use async_trait::async_trait; use bytes::Bytes; -use libdd_capabilities::{HttpClientCapability, MaybeSend}; +use libdd_capabilities::{HttpClientCapability, MaybeSend, SleepCapability}; use libdd_common::Endpoint; use libdd_shared_runtime::Worker; use sha2::{Digest, Sha256}; @@ -18,7 +18,6 @@ use std::marker::PhantomData; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; -use tokio::time::sleep; use tracing::{debug, warn}; /// Whether the agent reported the same value or not. @@ -35,13 +34,13 @@ pub enum FetchInfoStatus { /// If either the agent state hash or container tags hash is different from the current one: /// - Return a `FetchInfoStatus::NewState` of the info struct /// - Else return `FetchInfoStatus::SameState` -async fn fetch_info_with_state_and_container_tags( +async fn fetch_info_with_state_and_container_tags( info_endpoint: &Endpoint, current_state_hash: Option<&str>, current_container_tags_hash: Option<&str>, ) -> Result { let (new_state_hash, body_data, container_tags_hash) = - fetch_and_hash_response::(info_endpoint).await?; + fetch_and_hash_response::(info_endpoint).await?; if current_state_hash.is_some_and(|state| state == new_state_hash) && (current_container_tags_hash.is_none() @@ -65,11 +64,11 @@ async fn fetch_info_with_state_and_container_tags( /// If the state hash is different from the current one: /// - Return a `FetchInfoStatus::NewState` of the info struct /// - Else return `FetchInfoStatus::SameState` -pub async fn fetch_info_with_state( +pub async fn fetch_info_with_state( info_endpoint: &Endpoint, current_state_hash: Option<&str>, ) -> Result { - fetch_info_with_state_and_container_tags::(info_endpoint, current_state_hash, None).await + fetch_info_with_state_and_container_tags::(info_endpoint, current_state_hash, None).await } /// Fetch the info endpoint once and return the info. @@ -93,10 +92,10 @@ pub async fn fetch_info_with_state( /// # Ok(()) /// # } /// ``` -pub async fn fetch_info( +pub async fn fetch_info( info_endpoint: &Endpoint, ) -> Result> { - match fetch_info_with_state::(info_endpoint, None).await? { + match fetch_info_with_state::(info_endpoint, None).await? { FetchInfoStatus::NewState(info) => Ok(info), // Should never be reached since there is no previous state. FetchInfoStatus::SameState => Err(anyhow!("Invalid state header")), @@ -107,7 +106,7 @@ pub async fn fetch_info( /// /// Returns a tuple of (state_hash, response_body_bytes, container_tags_hash). /// The hash is calculated using SHA256 to match the agent's calculation method. -async fn fetch_and_hash_response( +async fn fetch_and_hash_response( info_endpoint: &Endpoint, ) -> Result<(String, bytes::Bytes, Option)> { let req = info_endpoint @@ -116,10 +115,18 @@ async fn fetch_and_hash_response( .map_err(|e| anyhow!("Failed to build request: {}", e))?; let timeout = Duration::from_millis(info_endpoint.timeout_ms); - let client = H::new_client(); - let res = tokio::time::timeout(timeout, client.request(req)) - .await - .map_err(|_| anyhow!("Request to /info timed out after {:?}", timeout))??; + let client = C::new_client(); + let sleeper = ::new(); + // Runtime-agnostic timeout: race the request against a capability-driven + // sleep instead of `tokio::time::timeout`, which requires a tokio reactor + // (not available on wasm where we run on the JS event loop). + let res = tokio::select! { + biased; + result = client.request(req) => result?, + _ = sleeper.sleep(timeout) => { + return Err(anyhow!("Request to /info timed out after {:?}", timeout)); + } + }; // Extract the Datadog-Container-Tags-Hash header let container_tags_hash = res @@ -158,12 +165,11 @@ async fn fetch_and_hash_response( /// // Define the endpoint /// use libdd_data_pipeline::agent_info; /// let endpoint = libdd_common::Endpoint::from_url("http://localhost:8126/info".parse().unwrap()); -/// let capabilities = NativeCapabilities::new_client(); /// // Create the fetcher /// let (mut fetcher, _response_observer) = libdd_data_pipeline::agent_info::AgentInfoFetcher::< /// NativeCapabilities, /// >::new( -/// endpoint, std::time::Duration::from_secs(5 * 60) +/// endpoint, std::time::Duration::from_secs(5 * 60), /// ); /// // Start the fetcher on a shared runtime /// let runtime = libdd_shared_runtime::SharedRuntime::new()?; @@ -182,20 +188,20 @@ async fn fetch_and_hash_response( /// # Ok(()) /// # } /// ``` -/// `H` is the HTTP client implementation, see [`HttpClientCapability`]. Leaf crates -/// pin it to a concrete type. +/// `C` is the capability bundle, see [`HttpClientCapability`] and [`SleepCapability`]. +/// Leaf crates pin it to a concrete type. #[derive(Debug)] -pub struct AgentInfoFetcher { +pub struct AgentInfoFetcher { info_endpoint: Endpoint, refresh_interval: Duration, trigger_rx: Option>, trigger_tx: mpsc::Sender<()>, - /// `H` must live on the struct because `Worker::run(&mut self)` (a fixed - /// trait signature) calls `fetch_info_with_state::()` internally. - _phantom: PhantomData, + /// `C` lives on the struct because `Worker::run(&mut self)` (a fixed trait + /// signature) calls `fetch_info_with_state::()` internally. + _phantom: PhantomData, } -impl AgentInfoFetcher { +impl AgentInfoFetcher { /// Return a new `AgentInfoFetcher` fetching the `info_endpoint` on each `refresh_interval` /// and updating the stored info. /// @@ -230,7 +236,9 @@ impl AgentInfoFetcher { #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl Worker for AgentInfoFetcher { +impl Worker + for AgentInfoFetcher +{ async fn initial_trigger(&mut self) { // Skip initial wait if cache is not populated if AGENT_INFO_CACHE.load().is_none() { @@ -240,6 +248,7 @@ impl Worker for AgentInfoF } async fn trigger(&mut self) { + let sleeper = ::new(); // Wait for either a manual trigger or the refresh interval match &mut self.trigger_rx { Some(trigger_rx) => { @@ -252,12 +261,12 @@ impl Worker for AgentInfoF } } // Regular periodic fetch timer - _ = sleep(self.refresh_interval) => {} + _ = sleeper.sleep(self.refresh_interval) => {} } } None => { // If the trigger channel is closed we only use timed fetch. - sleep(self.refresh_interval).await; + sleeper.sleep(self.refresh_interval).await; } } } @@ -277,7 +286,7 @@ impl Worker for AgentInfoF } } -impl AgentInfoFetcher { +impl AgentInfoFetcher { /// Fetch agent info and update cache if needed async fn fetch_and_update(&self) { let current_info = AGENT_INFO_CACHE.load(); @@ -285,7 +294,7 @@ impl AgentInfoFetcher { let current_container_tags_hash = current_info .as_ref() .and_then(|info| info.info.container_tags_hash.as_deref()); - let res = fetch_info_with_state_and_container_tags::( + let res = fetch_info_with_state_and_container_tags::( &self.info_endpoint, current_hash, current_container_tags_hash, diff --git a/libdd-data-pipeline/src/trace_exporter/builder.rs b/libdd-data-pipeline/src/trace_exporter/builder.rs index bd2e330602..16fc4f98aa 100644 --- a/libdd-data-pipeline/src/trace_exporter/builder.rs +++ b/libdd-data-pipeline/src/trace_exporter/builder.rs @@ -320,29 +320,23 @@ impl TraceExporterBuilder { let capabilities = C::new_client(); // --- Platform-specific worker setup --- - // The blocks below spawn background workers on native and create - // lightweight stubs on wasm. The `#[cfg]` interleaving is inherent to - // the platform split; each block is kept small to stay readable. + // The blocks below spawn background workers via `SharedRuntime`. On + // native, workers run on the tokio runtime; on wasm, they run on the JS + // event loop via `spawn_local`. Telemetry remains native-only for now. let info_endpoint = Endpoint::from_url(add_path(&agent_url, INFO_ENDPOINT)); - #[cfg(not(target_arch = "wasm32"))] - let (info_fetcher_handle, info_response_observer) = { - let (info_fetcher, observer) = - AgentInfoFetcher::::new(info_endpoint.clone(), Duration::from_secs(5 * 60)); - let handle = shared_runtime - .spawn_worker(info_fetcher, false) - .map_err(|e| { - TraceExporterError::Builder(BuilderErrorKind::InvalidConfiguration( - e.to_string(), - )) - })?; - (handle, observer) - }; - // On wasm the AgentInfoFetcher is not spawned yet (it requires spawn_local), - // but we still need the ResponseObserver for header checks. - #[cfg(target_arch = "wasm32")] - let (_info_fetcher, info_response_observer) = + let (info_fetcher, info_response_observer) = AgentInfoFetcher::::new(info_endpoint, Duration::from_secs(5 * 60)); + let info_fetcher_handle = shared_runtime + .spawn_worker(info_fetcher, false) + .map_err(|e| { + TraceExporterError::Builder(BuilderErrorKind::InvalidConfiguration(e.to_string())) + })?; + // The handle is currently only tracked for shutdown on native; on wasm + // it is dropped here (the worker keeps running on the JS event loop + // until the page/module is torn down). + #[cfg(target_arch = "wasm32")] + let _ = info_fetcher_handle; #[allow(unused_mut)] let mut stats = StatsComputationStatus::Disabled; From 96420ade4eb3992ea0acf76e249377d8b4975587 Mon Sep 17 00:00:00 2001 From: Jules Wiriath Date: Thu, 7 May 2026 18:01:07 +0200 Subject: [PATCH 12/14] fix: keep runtime locked while spawning workers --- .../src/shared_runtime/mod.rs | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/libdd-shared-runtime/src/shared_runtime/mod.rs b/libdd-shared-runtime/src/shared_runtime/mod.rs index dd2c8c42be..42f52837aa 100644 --- a/libdd-shared-runtime/src/shared_runtime/mod.rs +++ b/libdd-shared-runtime/src/shared_runtime/mod.rs @@ -77,23 +77,20 @@ mod native { debug!(?boxed_worker, "Spawning worker on SharedRuntime"); let mut pausable_worker = PausableWorker::new(boxed_worker); - // Lock runtime first to synchronize with before_fork (which does - // runtime.take() then workers.lock()), following the documented mutex - // lock order. If the runtime has been taken (fork window), skip starting - // the worker; after_fork_parent/child will start it on the new runtime. - let runtime_handle = self - .runtime - .lock_or_panic() - .as_ref() - .map(|rt| rt.handle().clone()); - // Hold the workers lock while starting the worker to avoid a race with - // before_fork: without this, before_fork could run after the worker is - // started but before it's added to the list, not pausing the worker - // before the runtime is dropped. + // Lock runtime first, then workers, following the documented mutex + // lock order (matches before_fork). Both guards are held across + // start+push so that before_fork cannot interleave between them: + // otherwise before_fork could take the runtime, drop it, and miss + // our (not-yet-pushed) worker, leaving us with a worker running on + // a torn-down runtime that before_fork never paused. If the + // runtime has been taken (fork window already passed), we skip + // starting; after_fork_parent/child will start the worker on the + // new runtime. + let runtime_guard = self.runtime.lock_or_panic(); let mut workers_guard = self.workers.lock_or_panic(); - if let Some(ref handle) = runtime_handle { - if let Err(e) = pausable_worker.start(tokio_spawn_fn(handle)) { + if let Some(rt) = runtime_guard.as_ref() { + if let Err(e) = pausable_worker.start(tokio_spawn_fn(rt.handle())) { return Err(e.into()); } } From b1796a725e3fcc37fecf479ea98def96a7f5da16 Mon Sep 17 00:00:00 2001 From: Jules Wiriath Date: Mon, 11 May 2026 12:18:00 +0200 Subject: [PATCH 13/14] chore: fmt --- libdd-data-pipeline/src/agent_info/fetcher.rs | 2 +- libdd-data-pipeline/src/trace_exporter/builder.rs | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/libdd-data-pipeline/src/agent_info/fetcher.rs b/libdd-data-pipeline/src/agent_info/fetcher.rs index f13d50c1d9..dfca7f276a 100644 --- a/libdd-data-pipeline/src/agent_info/fetcher.rs +++ b/libdd-data-pipeline/src/agent_info/fetcher.rs @@ -169,7 +169,7 @@ async fn fetch_and_hash_response( /// let (mut fetcher, _response_observer) = libdd_data_pipeline::agent_info::AgentInfoFetcher::< /// NativeCapabilities, /// >::new( -/// endpoint, std::time::Duration::from_secs(5 * 60), +/// endpoint, std::time::Duration::from_secs(5 * 60) /// ); /// // Start the fetcher on a shared runtime /// let runtime = libdd_shared_runtime::SharedRuntime::new()?; diff --git a/libdd-data-pipeline/src/trace_exporter/builder.rs b/libdd-data-pipeline/src/trace_exporter/builder.rs index 16fc4f98aa..b4a00e3a14 100644 --- a/libdd-data-pipeline/src/trace_exporter/builder.rs +++ b/libdd-data-pipeline/src/trace_exporter/builder.rs @@ -327,11 +327,14 @@ impl TraceExporterBuilder { let info_endpoint = Endpoint::from_url(add_path(&agent_url, INFO_ENDPOINT)); let (info_fetcher, info_response_observer) = AgentInfoFetcher::::new(info_endpoint, Duration::from_secs(5 * 60)); - let info_fetcher_handle = shared_runtime - .spawn_worker(info_fetcher, false) - .map_err(|e| { - TraceExporterError::Builder(BuilderErrorKind::InvalidConfiguration(e.to_string())) - })?; + let info_fetcher_handle = + shared_runtime + .spawn_worker(info_fetcher, false) + .map_err(|e| { + TraceExporterError::Builder(BuilderErrorKind::InvalidConfiguration( + e.to_string(), + )) + })?; // The handle is currently only tracked for shutdown on native; on wasm // it is dropped here (the worker keeps running on the JS event loop // until the page/module is torn down). From d8094e295cdf3098f116b45186f4b7b683230ffe Mon Sep 17 00:00:00 2001 From: Jules Wiriath Date: Mon, 11 May 2026 13:10:09 +0200 Subject: [PATCH 14/14] fix: aws-lc-sys and ring were both present --- libdd-data-pipeline/Cargo.toml | 4 +++- libdd-shared-runtime/Cargo.toml | 5 ++++- libdd-telemetry/Cargo.toml | 6 +++--- libdd-trace-stats/Cargo.toml | 6 +++--- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/libdd-data-pipeline/Cargo.toml b/libdd-data-pipeline/Cargo.toml index 05f1705e16..707b3dbebb 100644 --- a/libdd-data-pipeline/Cargo.toml +++ b/libdd-data-pipeline/Cargo.toml @@ -33,7 +33,7 @@ uuid = { version = "1.10.0", features = ["v4"] } tokio-util = "0.7.11" libdd-capabilities = { path = "../libdd-capabilities", version = "1.0.0" } libdd-common = { version = "4.0.0", path = "../libdd-common", default-features = false } -libdd-shared-runtime = { version = "0.1.0", path = "../libdd-shared-runtime" } +libdd-shared-runtime = { version = "0.1.0", path = "../libdd-shared-runtime", default-features = false } libdd-telemetry = { version = "5.0.0", path = "../libdd-telemetry", default-features = false, optional = true} libdd-trace-protobuf = { version = "3.0.1", path = "../libdd-trace-protobuf" } libdd-trace-stats = { version = "2.0.0", path = "../libdd-trace-stats", default-features = false } @@ -85,6 +85,7 @@ telemetry = ["libdd-telemetry"] https = [ "libdd-common/https", "libdd-capabilities-impl/https", + "libdd-shared-runtime/https", "libdd-telemetry?/https", "libdd-trace-stats/https", "libdd-trace-utils/https", @@ -93,6 +94,7 @@ https = [ fips = [ "libdd-common/fips", "libdd-capabilities-impl/fips", + "libdd-shared-runtime/fips", "libdd-telemetry?/fips", "libdd-trace-stats/fips", "libdd-trace-utils/fips", diff --git a/libdd-shared-runtime/Cargo.toml b/libdd-shared-runtime/Cargo.toml index 29b9becf06..62714ed9d5 100644 --- a/libdd-shared-runtime/Cargo.toml +++ b/libdd-shared-runtime/Cargo.toml @@ -25,10 +25,13 @@ libdd-capabilities = { path = "../libdd-capabilities", version = "1.0.0" } libdd-common = { version = "4.0.0", path = "../libdd-common", default-features = false } [features] +default = ["https"] +https = ["libdd-capabilities-impl/https"] +fips = ["libdd-capabilities-impl/fips"] regex-lite = ["libdd-common/regex-lite"] [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -libdd-capabilities-impl = { path = "../libdd-capabilities-impl", version = "1.0.0" } +libdd-capabilities-impl = { path = "../libdd-capabilities-impl", version = "1.0.0", default-features = false } tokio = { version = "1.23", features = ["rt-multi-thread"] } [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/libdd-telemetry/Cargo.toml b/libdd-telemetry/Cargo.toml index f7f58afeb0..29ae444d5a 100644 --- a/libdd-telemetry/Cargo.toml +++ b/libdd-telemetry/Cargo.toml @@ -14,8 +14,8 @@ bench = false [features] default = ["tracing"] tracing = ["tracing/std"] -https = ["libdd-common/https"] -fips = ["libdd-common/fips"] +https = ["libdd-common/https", "libdd-shared-runtime/https"] +fips = ["libdd-common/fips", "libdd-shared-runtime/fips"] [dependencies] anyhow = { version = "1.0" } @@ -33,7 +33,7 @@ uuid = { version = "1.3", features = ["v4"] } hashbrown = "0.15" bytes = "1.4" libdd-common = { version = "4.0.0", path = "../libdd-common", default-features = false } -libdd-shared-runtime = { version = "0.1.0", path = "../libdd-shared-runtime" } +libdd-shared-runtime = { version = "0.1.0", path = "../libdd-shared-runtime", default-features = false } libdd-ddsketch = { version = "1.0.1", path = "../libdd-ddsketch" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/libdd-trace-stats/Cargo.toml b/libdd-trace-stats/Cargo.toml index 43604cadf0..d9546555b4 100644 --- a/libdd-trace-stats/Cargo.toml +++ b/libdd-trace-stats/Cargo.toml @@ -14,7 +14,7 @@ anyhow = "1.0" libdd-capabilities = { path = "../libdd-capabilities", version = "1.0.0" } libdd-common = { version = "4.0.0", path = "../libdd-common", default-features = false } libdd-ddsketch = { version = "1.0.1", path = "../libdd-ddsketch" } -libdd-shared-runtime = { version = "0.1.0", path = "../libdd-shared-runtime" } +libdd-shared-runtime = { version = "0.1.0", path = "../libdd-shared-runtime", default-features = false } libdd-trace-protobuf = { version = "3.0.1", path = "../libdd-trace-protobuf" } libdd-trace-obfuscation = { version = "2.0.0", path = "../libdd-trace-obfuscation", default-features = false } libdd-trace-utils = { version = "3.0.1", path = "../libdd-trace-utils", default-features = false } @@ -47,5 +47,5 @@ tokio = { version = "1.23", features = ["rt-multi-thread", "macros", "test-util" [features] default = ["https"] -https = ["libdd-common/https", "libdd-capabilities-impl/https"] -fips = ["libdd-common/fips", "libdd-capabilities-impl/fips"] +https = ["libdd-common/https", "libdd-capabilities-impl/https", "libdd-shared-runtime/https"] +fips = ["libdd-common/fips", "libdd-capabilities-impl/fips", "libdd-shared-runtime/fips"]