Skip to content

Commit 6efdb89

Browse files
Lewis-Eclaude
andcommitted
Add integration test for concurrent TCP + named pipe transports
Boots one mini agent with both a non-zero dd_apm_receiver_port and a named pipe configured, sends a service-tagged trace through each, and asserts both service-name needles appear in the mock backend's request bodies. Covers the dropped receiver_port=0 override in config.rs and the serve() supervisor's responsibility to keep both accept loops alive. create_test_trace_payload now takes Option<&str> so the dual-transport test can distinguish payloads by service name without a second helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4df57c7 commit 6efdb89

3 files changed

Lines changed: 125 additions & 17 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/target
22
/.idea
3+
/.vscode
34
/CLAUDE.md
45
/AGENTS.md

crates/datadog-trace-agent/tests/common/helpers.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,18 @@ use libdd_trace_utils::test_utils::create_test_json_span;
1010
use std::time::{Duration, UNIX_EPOCH};
1111
use tokio::time::timeout;
1212

13-
/// Create a simple test trace payload as msgpack bytes
14-
pub fn create_test_trace_payload() -> Vec<u8> {
13+
/// Create a simple test trace payload as msgpack bytes. Pass `Some(name)` to
14+
/// override the default service ("test-service"); dual-transport tests use
15+
/// distinct service names to distinguish payloads that flow through
16+
/// different listeners but share a single flusher pipeline.
17+
pub fn create_test_trace_payload(service: Option<&str>) -> Vec<u8> {
1518
let start = UNIX_EPOCH.elapsed().unwrap().as_nanos() as i64;
16-
let json_span = create_test_json_span(11, 222, 0, start, false);
17-
rmp_serde::to_vec(&vec![vec![json_span]]).expect("Failed to serialize test trace")
19+
let mut span = create_test_json_span(11, 222, 0, start, false);
20+
if let Some(name) = service {
21+
span["service"] = serde_json::Value::String(name.into());
22+
span["meta"]["service"] = serde_json::Value::String(name.into());
23+
}
24+
rmp_serde::to_vec(&vec![vec![span]]).expect("Failed to serialize test trace")
1825
}
1926

2027
/// Send an HTTP request over TCP and return the response

crates/datadog-trace-agent/tests/integration_test.rs

Lines changed: 113 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ async fn test_mini_agent_tcp_handles_requests() {
179179

180180
// Start the mini agent
181181
let agent_handle = tokio::spawn(async move {
182-
let (_shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
182+
let (_shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
183183
let _ = mini_agent.start_mini_agent(shutdown_rx, None).await;
184184
});
185185

@@ -240,7 +240,7 @@ async fn test_mini_agent_tcp_handles_requests() {
240240
);
241241

242242
// Test /v0.4/traces endpoint with real trace data
243-
let trace_payload = create_test_trace_payload();
243+
let trace_payload = create_test_trace_payload(None);
244244
let trace_response =
245245
send_tcp_request(test_port, "/v0.4/traces", "POST", Some(trace_payload), &[])
246246
.await
@@ -279,7 +279,7 @@ async fn test_mini_agent_named_pipe_handles_requests() {
279279

280280
// Start the mini agent
281281
let agent_handle = tokio::spawn(async move {
282-
let (_shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
282+
let (_shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
283283
let _ = mini_agent.start_mini_agent(shutdown_rx, None).await;
284284
});
285285

@@ -322,7 +322,7 @@ async fn test_mini_agent_named_pipe_handles_requests() {
322322
);
323323

324324
// Test /v0.4/traces endpoint with real trace data
325-
let trace_payload = create_test_trace_payload();
325+
let trace_payload = create_test_trace_payload(None);
326326
let trace_response =
327327
send_named_pipe_request(&pipe_path, "/v0.4/traces", "POST", Some(trace_payload))
328328
.await
@@ -353,7 +353,7 @@ async fn test_mini_agent_tcp_with_real_flushers() {
353353
let (mini_agent, stats_concentrator_service_handle) =
354354
create_mini_agent_with_real_flushers(config);
355355

356-
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
356+
let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
357357
let agent_handle = tokio::spawn(async move {
358358
let _ = mini_agent
359359
.start_mini_agent(shutdown_rx, Some(stats_concentrator_service_handle))
@@ -377,7 +377,7 @@ async fn test_mini_agent_tcp_with_real_flushers() {
377377
);
378378

379379
// Send trace data
380-
let trace_payload = create_test_trace_payload();
380+
let trace_payload = create_test_trace_payload(None);
381381
let trace_response =
382382
send_tcp_request(test_port, "/v0.4/traces", "POST", Some(trace_payload), &[])
383383
.await
@@ -389,7 +389,7 @@ async fn test_mini_agent_tcp_with_real_flushers() {
389389
verify_trace_request(&mock_server);
390390

391391
// Trigger shutdown to force flush in progress concentrator buckets
392-
let _ = shutdown_tx.send(());
392+
let _ = shutdown_tx.send(true);
393393
let _ = agent_handle.await;
394394
verify_stats_request(&mock_server); // Stats generator should generate stats from trace payload
395395
}
@@ -410,7 +410,7 @@ async fn test_concentrator_task_death_shuts_down_mini_agent() {
410410
create_mini_agent_with_real_flushers(config);
411411
let abort_handle = stats_concentrator_service_handle.abort_handle();
412412

413-
let (_shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
413+
let (_shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
414414
let agent_handle = tokio::spawn(async move {
415415
mini_agent
416416
.start_mini_agent(shutdown_rx, Some(stats_concentrator_service_handle))
@@ -463,7 +463,7 @@ async fn test_mini_agent_tcp_with_real_flushers_and_tracer_computed_stats() {
463463
create_mini_agent_with_real_flushers(config);
464464

465465
let agent_handle = tokio::spawn(async move {
466-
let (_shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
466+
let (_shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
467467
let _ = mini_agent.start_mini_agent(shutdown_rx, None).await;
468468
});
469469

@@ -484,7 +484,7 @@ async fn test_mini_agent_tcp_with_real_flushers_and_tracer_computed_stats() {
484484
);
485485

486486
// Send trace data
487-
let trace_payload = create_test_trace_payload();
487+
let trace_payload = create_test_trace_payload(None);
488488
let trace_response = send_tcp_request(
489489
test_port,
490490
"/v0.4/traces",
@@ -524,7 +524,7 @@ async fn test_mini_agent_named_pipe_with_real_flushers() {
524524
let (mini_agent, _stats_concentrator_service_handle) =
525525
create_mini_agent_with_real_flushers(config);
526526

527-
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
527+
let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
528528
let agent_handle = tokio::spawn(async move {
529529
let _ = mini_agent.start_mini_agent(shutdown_rx, None).await;
530530
});
@@ -546,7 +546,7 @@ async fn test_mini_agent_named_pipe_with_real_flushers() {
546546
);
547547

548548
// Send trace data via named pipe
549-
let trace_payload = create_test_trace_payload();
549+
let trace_payload = create_test_trace_payload(None);
550550
let trace_response =
551551
send_named_pipe_request(pipe_name, "/v0.4/traces", "POST", Some(trace_payload))
552552
.await
@@ -558,7 +558,107 @@ async fn test_mini_agent_named_pipe_with_real_flushers() {
558558
verify_trace_request(&mock_server);
559559

560560
// Trigger shutdown to force flush in progress concentrator buckets
561-
let _ = shutdown_tx.send(());
561+
let _ = shutdown_tx.send(true);
562562
let _ = agent_handle.await;
563563
verify_stats_request(&mock_server);
564564
}
565+
566+
#[cfg(all(test, windows, feature = "windows-pipes"))]
567+
#[tokio::test]
568+
#[serial]
569+
async fn test_mini_agent_dual_transport_with_real_flushers() {
570+
let mock_server = MockServer::start().await;
571+
tokio::time::sleep(Duration::from_millis(50)).await;
572+
573+
let pipe_name = r"\\.\pipe\dd_trace_dual_transport_test";
574+
let tcp_port: u16 = 8130;
575+
576+
let mut config = create_tcp_test_config(tcp_port);
577+
configure_mock_endpoints(&mut config, &mock_server.url());
578+
config.dd_apm_windows_pipe_name = Some(pipe_name.to_string());
579+
// Both transports are deliberately set on the same agent: a non-zero TCP
580+
// port AND a pipe name. They must come up concurrently.
581+
let config = Arc::new(config);
582+
583+
let (mini_agent, stats_concentrator_service_handle) =
584+
create_mini_agent_with_real_flushers(config);
585+
let agent_handle = tokio::spawn(async move {
586+
let (_shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
587+
let _ = mini_agent
588+
.start_mini_agent(shutdown_rx, Some(stats_concentrator_service_handle))
589+
.await;
590+
});
591+
592+
// Readiness on each transport, sequentially (TCP then pipe).
593+
let mut tcp_ready = false;
594+
for _ in 0..20 {
595+
tokio::time::sleep(Duration::from_millis(50)).await;
596+
if let Ok(response) = send_tcp_request(tcp_port, "/info", "GET", None, &[]).await
597+
&& response.status().is_success()
598+
{
599+
tcp_ready = true;
600+
break;
601+
}
602+
}
603+
assert!(
604+
tcp_ready,
605+
"TCP listener did not bind on port {tcp_port} when pipe is also configured \
606+
— config may be overriding receiver_port to 0 when a pipe name is set"
607+
);
608+
609+
let mut pipe_ready = false;
610+
for _ in 0..20 {
611+
tokio::time::sleep(Duration::from_millis(50)).await;
612+
if let Ok(response) = send_named_pipe_request(pipe_name, "/info", "GET", None).await
613+
&& response.status().is_success()
614+
{
615+
pipe_ready = true;
616+
break;
617+
}
618+
}
619+
assert!(
620+
pipe_ready,
621+
"Named pipe listener did not come up at {pipe_name} when TCP is also configured"
622+
);
623+
624+
// One trace per transport, distinguishable by service name.
625+
let tcp_payload = create_test_trace_payload(Some("dual-tcp-svc"));
626+
let pipe_payload = create_test_trace_payload(Some("dual-pipe-svc"));
627+
628+
let tcp_response = send_tcp_request(tcp_port, "/v0.4/traces", "POST", Some(tcp_payload), &[])
629+
.await
630+
.expect("Failed to send /v0.4/traces request over TCP");
631+
assert_eq!(tcp_response.status(), StatusCode::OK);
632+
633+
let pipe_response =
634+
send_named_pipe_request(pipe_name, "/v0.4/traces", "POST", Some(pipe_payload))
635+
.await
636+
.expect("Failed to send /v0.4/traces request over named pipe");
637+
assert_eq!(pipe_response.status(), StatusCode::OK);
638+
639+
tokio::time::sleep(FLUSH_WAIT_DURATION).await;
640+
641+
// Both payloads must reach the same backend through the shared flusher
642+
// pipeline. The flusher may batch them into one POST or two; either is
643+
// fine, what matters is that both service-name needles show up.
644+
let trace_reqs = mock_server.get_requests_for_path("/api/v0.2/traces");
645+
assert!(
646+
!trace_reqs.is_empty(),
647+
"no trace POST reached backend; expected traces from both transports"
648+
);
649+
let mut all_bytes = Vec::new();
650+
for req in &trace_reqs {
651+
assert_eq!(req.method, "POST");
652+
all_bytes.extend_from_slice(&req.body);
653+
}
654+
assert!(
655+
all_bytes.windows(12).any(|w| w == b"dual-tcp-svc"),
656+
"TCP-side trace did not reach backend"
657+
);
658+
assert!(
659+
all_bytes.windows(13).any(|w| w == b"dual-pipe-svc"),
660+
"pipe-side trace did not reach backend"
661+
);
662+
663+
agent_handle.abort();
664+
}

0 commit comments

Comments
 (0)