Skip to content

Fix trace and StatsD connections over Windows named pipes - #12898

Draft
Lewis-E wants to merge 6 commits into
masterfrom
lewis.lewis/java-named-pipe-statsd-fixes
Draft

Lewis-E wants to merge 6 commits into
masterfrom
lewis.lewis/java-named-pipe-statsd-fixes

Conversation

@Lewis-E

@Lewis-E Lewis-E commented Oct 15, 2026 •

Copy link
Copy Markdown

What Does This Do

This fixes the tracer's agent connections over Windows named pipes, and makes the StatsD connection recover when DogStatsD starts late or goes away. Each commit is one change, and its message has the details.

  1. The StatsD connection warns, instead of logging at debug, when it gives up connecting.
  2. The StatsD connection keeps retrying its first connection forever, doubling the delay from 10 s to a 60 s cap. Before, it gave up after 20 attempts.
  3. The StatsD connection recreates its client once writes have kept failing for at least 30 s: 3 or more errors, each less than 60 s after the previous one.
  4. NamedPipeSocketFactory opens a new pipe handle for every socket instead of returning one shared NamedPipeSocket per pipe. This reverses Reuse named pipe sockets #3493.
  5. OkHttp clients that use a named pipe don't keep idle connections, so each request opens and closes its own pipe connection. For named pipes this replaces the maxRunningRequests connection pool, while the dispatcher limits stay as they are.
  6. DDAgentStatsDConnection uses port 0 when the config sets a named pipe and no port. Before, it threw NullPointerException.

Motivation

Customers on two Azure products hit these bugs.

Azure Functions with named pipes (SVLS-8247): Java tracing over a named pipe has never worked. Functions on a shared Windows plan need per-function pipes: the sandbox blocks loopback connections to other apps, while TCP ports still conflict across apps on one instance.

  • Trace uploads hang. A thread dump from our self-monitoring function showed OkHttp's pooled-connection health check (RealConnection.isHealthy) blocked in a pipe read on the shared NamedPipeSocket. NamedPipeSocket.setSoTimeout does nothing, so that read never times out. Traces, stats, remote config, and telemetry all queued behind that one handle, and the tracer never logged a trace as sent or failed until the JVM restarted. Removing only DD_TRACE_PIPE_NAME made the same app send traces over TCP right away.
  • Tracer metrics never connect. DDAgentStatsDConnection left the port null when a pipe was set, and .port(port) threw NullPointerException, so tracer health metrics and JMXFetch JVM metrics never went out over a pipe. The StatsD bug is in every version we checked, 1.30.0 through 1.65.1, and the shared socket dates from Reuse named pipe sockets #3493 in 2022.

Azure App Service overlapping restarts (SVLS-9057, escalation SLES-2826): during an overlapped IIS recycle, the old and new worker share the DogStatsD pipe. In our repro, the new JVM's StatsD client kept failing for 50 minutes after the old worker's DogStatsD exited, even though the new worker's own DogStatsD was serving the pipe. The java-dogstatsd-client never reopens a named pipe. With commits 2 and 3 in a repro build, the first connection failed once and then connected, and after the overlap the client recreated itself. Metrics came back about 34 s after the old DogStatsD exited.

Removing #3493's shared socket is safe for the agents the tracer connects to. #3493 shared one socket per pipe because "named pipes can be opened at most twice (1 client and 1 server)", and noted that it was untested on Windows. A Windows pipe server can create several instances of one pipe name. The .NET tracer opens a new pipe stream for every request to the same trace agent and serverless-compat pipes, and it doesn't hang. The StatsD client opens its own pipe, with a different name, so the shared HTTP socket never protected it.

Additional Notes

Design choices and known gaps:

  • Reads on a NamedPipeSocket still can't time out. With one connection per request, a hung agent blocks only the request waiting on it, not every agent request. Honoring setSoTimeout would take overlapped IO, which this PR doesn't attempt. Nothing logs a hung request either, because it never fails. The only default-level sign is RemoteWriter's rate-limited "Dropped a kept trace due to a buffer overflow" warning once the queue fills, and that reads as too much traffic, not a stuck agent. In App Service, Recover Azure App Service agents after overlapping restarts #12899's health check detects a hung trace agent on its own pipe handle and logs it.
  • Several pipe connections can now be open at once, such as a trace flush and a remote config poll. We haven't seen "All pipe instances are busy" errors, but this hasn't run on Windows under load yet.
  • The StatsD recreate is in the tracer's connection wrapper, DDAgentStatsDConnection. The alternative is reconnect-on-write-failure in java-dogstatsd-client, which would also help customer StatsD clients. If that's the better home, commit 3 can leave this PR and move there. The other commits don't depend on it.
  • The recreate can drop metrics still queued in the old client.
  • The port fix fills in 0 only when no port is configured, so setups that set both a pipe and a port behave as before. Port 0 also gives the pipe client the settings the tracer uses for Unix domain sockets: an 8 KB packet size, the socket timeout and buffer settings, and entity ID none.
  • NamedPipeSocketFactory has a package-private File constructor so that its test can use a regular file as the pipe on any OS.

Tests: the new NamedPipeSocketFactoryTest failed on master with the shared socket. DDAgentStatsDConnectionTest is new and covers the retry delay, the write-failure streak, and the named-pipe port, which failed before commit 6. Commits 1 to 3 ran in the App Service repro on Windows. Commits 4 to 6 haven't run on Windows yet. Next is an Azure Functions end-to-end run with a dev build of this branch, on shared Elastic Premium and Dedicated plans.

#12899 (App Service agent supervision and logging) is stacked on this branch. datadog-serverless-compat-java#43 gives each Windows Azure Function its own named pipes, and it requires the first dd-trace-java release with this PR.

Contributor Checklist

Jira ticket: SVLS-8247 [SVLS-9057]

When the StatsD connection ran out of connect retries it logged at debug
and stopped for the life of the JVM, so default customer logs showed no
sign that tracer metrics were never sent.

Warn instead, with the StatsD address, the number of attempts, and the
last error.
When the DogStatsD pipe or port was missing at startup, the StatsD
connection retried 20 times, 10 seconds apart, then gave up for the life
of the JVM, so a DogStatsD that came up later was never used. In Azure App
Service this happens when the tracer starts before DogStatsD listens.

- Retry forever, doubling the delay from 10 seconds to a cap of 60
  seconds. Drop the jitter on retries so the delay stays within the cap.
- Warn once on the first failure, log later attempts at debug, and log
  once at info when a connection succeeds after failures.
The StatsD client opens its named pipe once and never reopens it. After
the DogStatsD it was writing to went away, every write failed for the
life of the JVM, even once a new DogStatsD served the same pipe name. In
an Azure App Service overlapping restart, tracer metrics were still
missing 50 minutes after the old worker's DogStatsD exited.

- Track write errors in handle(). Errors more than 60 seconds apart start
  a new streak; a streak of at least 3 errors spanning at least 30
  seconds triggers a recreate. The 60 second gap is longer than the 30
  second tracer health metrics flush, so a dead pipe keeps one streak
  going, while brief bursts and isolated timeouts under load do not
  trigger it.
- Recreate on the task scheduler, not the client's own thread: close the
  client and reconnect through the existing connect path, which retries
  with backoff if the pipe is still missing.
- Warn on recreate, rate limited to once every 5 minutes.
NamedPipeSocketFactory kept one NamedPipeSocket per pipe and returned it
to every OkHttp connection (#3493). NamedPipeSocket ignores read timeouts,
so when OkHttp checks a pooled connection's health it blocks on an
unbounded pipe read, and every other agent request (traces, stats, remote
config, telemetry) waits behind it on the shared handle. On Azure
Functions with DD_TRACE_PIPE_NAME set, the tracer queued spans but never
sent or failed a trace upload until the JVM restarted.

Open a new pipe handle for each socket, as the .NET tracer does for each
request. #3493 shared the handle because a named pipe was thought to
allow only one client at a time; that change was untested on Windows, and
the .NET tracer's per-request pipe connections show the agent's pipe
server accepts several clients.
NamedPipeSocket can't time out a read, so when OkHttp checks a pooled
connection's health before reusing it, the check can block forever on the
pipe. Give named-pipe clients a connection pool that keeps no idle
connections, so each request opens and closes its own pipe connection, as
the .NET tracer does. For named pipes this replaces the maxRunningRequests
connection pool; the dispatcher limits are unchanged.
When a DogStatsD named pipe was set and no port was configured,
DDAgentStatsDConnection left the port null, and building the client threw
NullPointerException on .port(port). Tracer health metrics and JMXFetch
JVM metrics were never sent over a pipe.

Use port 0 when a named pipe is set and no port is configured. The client
connects to the pipe and ignores the port; port 0 also selects the same
packet size and socket settings the tracer uses for Unix domain sockets.
@datadog-datadog-prod-us1-2

datadog-datadog-prod-us1-2 Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

🎯 Code Coverage (details)
• Patch Coverage: 17.86%
• Overall Coverage: 61.24% (+1.43%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 4767300 | Docs | Give us feedback!

@dd-octo-sts

dd-octo-sts Bot commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

🟡 Java Benchmark SLOs — Performance SLO warning (near threshold)

Suite Status
Startup 🟡 warning

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 13.99 s 13.93 s [-0.5%; +1.5%] (no difference)
startup:insecure-bank:tracing:Agent 12.80 s 12.95 s [-1.7%; -0.6%] (maybe better)
startup:petclinic:appsec:Agent 17.08 s 16.92 s [-0.1%; +2.0%] (no difference)
startup:petclinic:iast:Agent 16.19 s 16.81 s [-7.9%; +0.5%] (no difference)
startup:petclinic:profiling:Agent 16.62 s 16.66 s [-1.1%; +0.6%] (no difference)
startup:petclinic:sca:Agent 17.02 s 16.96 s [-0.6%; +1.3%] (no difference)
startup:petclinic:tracing:Agent 16.19 s 16.00 s [+0.3%; +2.0%] (maybe worse)

Commit: 47673006 · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant