Skip to content

Commit a21890b

Browse files
authored
[SVLS-8211] feat: Add timeout for requests to span_dedup_service (#986)
## Problem Span dedup service sometimes fails to return the result and thus logs the error: > DD_EXTENSION | ERROR | Failed to send check_and_add response: true I see this error in our Self Monitoring and a customer's account. Also I believe it causes extension to fail to receive traces from the tracer, causing missing traces. This is because the caller of span dedup is in `process_traces()`, which is the function that handles the tracer's HTTP request to send traces. If this function fails to get span dedup result and gets stuck, the HTTP request will time out. ## This PR While I don't yet know what causes the error, this PR adds a patch to mitigate the impact: 1. Change log level from `error` to `warn` 2. Add a timeout of 5 seconds to the span dedup check, so that if the caller doesn't get an answer soon, it defaults to treating the trace as not a duplicate, which is the most common case. ## Testing To merge this PR then check log in self monitoring, as it's hard to run high-volume tests in self monitoring from a non-main branch.
1 parent 3a596ed commit a21890b

5 files changed

Lines changed: 52 additions & 20 deletions

File tree

bottlecap/src/config/env.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,13 @@ pub struct EnvConfig {
417417
/// Default is `false`.
418418
#[serde(deserialize_with = "deserialize_optional_bool_from_anything")]
419419
pub compute_trace_stats_on_extension: Option<bool>,
420+
/// @env `DD_SPAN_DEDUP_TIMEOUT`
421+
///
422+
/// The timeout for the span deduplication service to check if a span key exists, in seconds.
423+
/// For now, this is a temporary field added to debug the failure of `check_and_add()` in span dedup service.
424+
/// Do not use this field extensively in production.
425+
#[serde(deserialize_with = "deserialize_optional_duration_from_seconds_ignore_zero")]
426+
pub span_dedup_timeout: Option<Duration>,
420427
/// @env `DD_API_KEY_SECRET_RELOAD_INTERVAL`
421428
///
422429
/// The interval at which the Datadog API key is reloaded, in seconds.
@@ -640,6 +647,7 @@ fn merge_config(config: &mut Config, env_config: &EnvConfig) {
640647
merge_option_to_value!(config, env_config, capture_lambda_payload);
641648
merge_option_to_value!(config, env_config, capture_lambda_payload_max_depth);
642649
merge_option_to_value!(config, env_config, compute_trace_stats_on_extension);
650+
merge_option!(config, env_config, span_dedup_timeout);
643651
merge_option!(config, env_config, api_key_secret_reload_interval);
644652
merge_option_to_value!(config, env_config, serverless_appsec_enabled);
645653
merge_option!(config, env_config, appsec_rules);
@@ -835,6 +843,7 @@ mod tests {
835843
jail.set_env("DD_CAPTURE_LAMBDA_PAYLOAD", "true");
836844
jail.set_env("DD_CAPTURE_LAMBDA_PAYLOAD_MAX_DEPTH", "5");
837845
jail.set_env("DD_COMPUTE_TRACE_STATS_ON_EXTENSION", "true");
846+
jail.set_env("DD_SPAN_DEDUP_TIMEOUT", "5");
838847
jail.set_env("DD_API_KEY_SECRET_RELOAD_INTERVAL", "10");
839848
jail.set_env("DD_SERVERLESS_APPSEC_ENABLED", "true");
840849
jail.set_env("DD_APPSEC_RULES", "/path/to/rules.json");
@@ -988,6 +997,7 @@ mod tests {
988997
capture_lambda_payload: true,
989998
capture_lambda_payload_max_depth: 5,
990999
compute_trace_stats_on_extension: true,
1000+
span_dedup_timeout: Some(Duration::from_secs(5)),
9911001
api_key_secret_reload_interval: Some(Duration::from_secs(10)),
9921002
serverless_appsec_enabled: true,
9931003
appsec_rules: Some("/path/to/rules.json".to_string()),

bottlecap/src/config/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ pub struct Config {
346346
pub capture_lambda_payload: bool,
347347
pub capture_lambda_payload_max_depth: u32,
348348
pub compute_trace_stats_on_extension: bool,
349+
pub span_dedup_timeout: Option<Duration>,
349350
pub api_key_secret_reload_interval: Option<Duration>,
350351

351352
pub serverless_appsec_enabled: bool,
@@ -451,6 +452,7 @@ impl Default for Config {
451452
capture_lambda_payload: false,
452453
capture_lambda_payload_max_depth: 10,
453454
compute_trace_stats_on_extension: false,
455+
span_dedup_timeout: None,
454456
api_key_secret_reload_interval: None,
455457

456458
serverless_appsec_enabled: false,

bottlecap/src/config/yaml.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -995,6 +995,7 @@ api_security_sample_delay: 60 # Seconds
995995
capture_lambda_payload: true,
996996
capture_lambda_payload_max_depth: 5,
997997
compute_trace_stats_on_extension: true,
998+
span_dedup_timeout: None,
998999
api_key_secret_reload_interval: None,
9991000

10001001
serverless_appsec_enabled: true,

bottlecap/src/traces/span_dedup_service.rs

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
use tokio::sync::{mpsc, oneshot};
5-
use tracing::error;
5+
use tokio::time::Duration;
6+
use tracing::warn;
67

78
use crate::traces::span_dedup::{DedupKey, Deduper};
89

@@ -12,6 +13,8 @@ pub enum DedupError {
1213
SendError(mpsc::error::SendError<DedupCommand>),
1314
#[error("Failed to receive response from deduper: {0}")]
1415
RecvError(oneshot::error::RecvError),
16+
#[error("Timeout waiting for response from deduper")]
17+
Timeout,
1518
}
1619

1720
pub enum DedupCommand {
@@ -34,14 +37,29 @@ impl DedupHandle {
3437
///
3538
/// # Errors
3639
///
37-
/// Returns an error if the command cannot be sent to the deduper service
38-
/// or if the response cannot be received.
39-
pub async fn check_and_add(&self, key: DedupKey) -> Result<bool, DedupError> {
40+
/// Returns an error if the command cannot be sent to the deduper service,
41+
/// if the response cannot be received, or if the operation times out after 5 seconds.
42+
pub async fn check_and_add(
43+
&self,
44+
key: DedupKey,
45+
timeout: Option<Duration>,
46+
) -> Result<bool, DedupError> {
4047
let (response_tx, response_rx) = oneshot::channel();
4148
self.tx
4249
.send(DedupCommand::CheckAndAdd(key, response_tx))
4350
.map_err(DedupError::SendError)?;
44-
response_rx.await.map_err(DedupError::RecvError)
51+
52+
// Sometimes the dedup service fails to send a response for unknown reasons, so we
53+
// add a timeout to avoid blocking the caller forever. We may remove the
54+
// timeout if we can figure out and fix the root cause.
55+
if let Some(timeout) = timeout {
56+
tokio::time::timeout(timeout, response_rx)
57+
.await
58+
.map_err(|_| DedupError::Timeout)?
59+
.map_err(DedupError::RecvError)
60+
} else {
61+
response_rx.await.map_err(DedupError::RecvError)
62+
}
4563
}
4664
}
4765

@@ -77,7 +95,7 @@ impl DedupService {
7795
DedupCommand::CheckAndAdd(key, response_tx) => {
7896
let was_added = self.deduper.check_and_add(key);
7997
if let Err(e) = response_tx.send(was_added) {
80-
error!("Failed to send check_and_add response: {e:?}");
98+
warn!("Failed to send check_and_add response: {e:?}");
8199
}
82100
}
83101
}
@@ -112,17 +130,17 @@ mod tests {
112130
let key2 = DedupKey::new(100, 456);
113131

114132
// First call should return true (key was added)
115-
assert!(handle.check_and_add(key1).await.unwrap());
133+
assert!(handle.check_and_add(key1, None).await.unwrap());
116134

117135
// Second call should return false (key already exists)
118-
assert!(!handle.check_and_add(key1).await.unwrap());
136+
assert!(!handle.check_and_add(key1, None).await.unwrap());
119137

120138
// Different key should return true again
121-
assert!(handle.check_and_add(key2).await.unwrap());
139+
assert!(handle.check_and_add(key2, None).await.unwrap());
122140

123141
// Calling again on already-added keys should return false
124-
assert!(!handle.check_and_add(key1).await.unwrap());
125-
assert!(!handle.check_and_add(key2).await.unwrap());
142+
assert!(!handle.check_and_add(key1, None).await.unwrap());
143+
assert!(!handle.check_and_add(key2, None).await.unwrap());
126144
}
127145

128146
#[tokio::test]
@@ -139,17 +157,17 @@ mod tests {
139157
let key4 = DedupKey::new(4, 40);
140158

141159
// Add 3 keys
142-
assert!(handle.check_and_add(key1).await.unwrap());
143-
assert!(handle.check_and_add(key2).await.unwrap());
144-
assert!(handle.check_and_add(key3).await.unwrap());
160+
assert!(handle.check_and_add(key1, None).await.unwrap());
161+
assert!(handle.check_and_add(key2, None).await.unwrap());
162+
assert!(handle.check_and_add(key3, None).await.unwrap());
145163

146164
// Add a 4th key, should evict the oldest (key1)
147-
assert!(handle.check_and_add(key4).await.unwrap());
165+
assert!(handle.check_and_add(key4, None).await.unwrap());
148166

149167
// Now key1 should be addable again (was evicted)
150-
assert!(handle.check_and_add(key1).await.unwrap());
168+
assert!(handle.check_and_add(key1, None).await.unwrap());
151169

152170
// But key2 should now be evicted
153-
assert!(handle.check_and_add(key2).await.unwrap());
171+
assert!(handle.check_and_add(key2, None).await.unwrap());
154172
}
155173
}

bottlecap/src/traces/trace_agent.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use tokio::sync::{
1919
};
2020
use tokio_util::sync::CancellationToken;
2121
use tower_http::limit::RequestBodyLimitLayer;
22-
use tracing::{debug, error};
22+
use tracing::{debug, error, warn};
2323

2424
use crate::traces::trace_processor::SendingTraceProcessor;
2525
use crate::{
@@ -545,7 +545,8 @@ impl TraceAgent {
545545
for mut span in original_chunk {
546546
// Check for duplicates
547547
let key = DedupKey::new(span.trace_id, span.span_id);
548-
let should_keep = match deduper.check_and_add(key).await {
548+
let should_keep = match deduper.check_and_add(key, config.span_dedup_timeout).await
549+
{
549550
Ok(should_keep) => {
550551
if !should_keep {
551552
debug!(
@@ -556,7 +557,7 @@ impl TraceAgent {
556557
should_keep
557558
}
558559
Err(e) => {
559-
error!("Failed to check span in deduper, keeping span: {e}");
560+
warn!("Failed to check span in deduper, keeping span: {e}");
560561
true
561562
}
562563
};

0 commit comments

Comments
 (0)