Skip to content

Named Pipe connection behavior matching Unix Stream behavior - #345

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 3 commits into
DataDog:masterfrom
Lewis-E:lewis/SVLS-8246/connect-named-pipe-on-write
Oct 27, 2026
Merged

gh-worker-dd-mergequeue-cf854d[bot] merged 3 commits into
DataDog:masterfrom
Lewis-E:lewis/SVLS-8246/connect-named-pipe-on-write

Conversation

@Lewis-E

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

Copy link
Copy Markdown
Contributor

Named pipes now connect the way Unix sockets do. Before, a missing pipe made build() fail with StatsDClientException: Failed to start StatsD client, and a pipe that broke later stayed broken. Now a missing pipe doesn't fail build(), and the client reopens the pipe after the server restarts. @vickenty suggested this in #344, which will rebase on this PR.

Implementation

NamedPipeClientChannel stores the pipe name and opens the pipe on the first write. When a write fails, it closes the pipe. The next write opens the pipe and tries again. After close(), the channel stays closed: writes throw ClosedChannelException without opening the pipe. This matches the behavior of UnixStreamClientChannel in this repo and the Windows pipe writer in datadog-go, which defers the connection to the first write and drops it after a write error. It also fits the DogStatsD Client API Standards lifecycle rule that constructors return clients ready to use.

Blast radius and default-path deltas

Affects clients using a named pipe address:

  • namedPipe(...)
  • DD_DOGSTATSD_PIPE_NAME
  • a DD_DOGSTATSD_URL that starts with \\.\pipe\

UDP and Unix socket clients don't run this code.

  1. After a write fails, the next write reopens the pipe. Before, a pipe that broke, for example after an Agent restart, made every later write fail until the application restarted.
  2. A pipe that doesn't exist at build() no longer throws. Each write while it's missing is dropped, counted in client telemetry, and passed to the error handler as a FileNotFoundException. This is a behavior change for any customers expecting a startup failure. Clients without an error handler use the default no-op handler. Their telemetry goes through the same pipe by default, so a wrong pipe name is silent unless telemetry has its own address.
  3. dd-trace-java builds its StatsD client with namedPipe(...) and retries build() every 10 seconds when it throws. That retry will no longer trigger from a named pipe issue; the channel reconnects instead. The tracer's error handler logs dropped writes through a rate-limited logger (once per 5 minutes).

Performance: with the pipe open, each write adds a null check and an uncontended lock. With the pipe missing, each dropped packet adds one failed open attempt, which doesn't wait, on top of the exception and error-handler call that a broken pipe already causes today. This matches UnixStreamClientChannel and datadog-go. Not benchmarked.

Tests

  • NonBlockingStatsDClientBuilderTest.missing_named_pipe_does_not_fail_build (all platforms) checks that a missing pipe builds and reports a FileNotFoundException on write. It fails on master.
  • NamedPipeTest.sends_to_pipe_created_after_build (Windows) checks that a pipe created after build() receives the next metric.
  • NamedPipeTest.reconnects_after_server_restart (Windows) closes the server, waits for the client to see the broken pipe, starts a new server on the same name, and checks that a later metric arrives.
  • NamedPipeClientChannelTest.write_after_close_throws_without_opening_the_pipe (all platforms) checks that a closed channel doesn't reopen the pipe.
  • NamedPipeDummyStatsDServer.close() now only marks the server closed, and the server's listening thread closes the pipe after its current read returns. Closing a synchronous pipe from the test thread can wait for that read, which hung the restart test while the client was idle.

The Windows tests run only in the windows-latest CI job.

See also: this comment, SVLS-8246, SVLS-9467.

A client built while the agent's named pipe didn't exist failed to build,
and a client whose pipe broke, for example after an agent restart, failed
every write until the application restarted. UDP and Unix sockets don't
fail this way.

NamedPipeClientChannel now opens the pipe on the first write. When a write
fails, it closes the pipe, and the next write opens it again. This matches
UnixStreamClientChannel and datadog-go's Windows pipe writer.

A missing pipe is now reported to the error handler for each dropped
write instead of failing build().

SVLS-8246
CloseHandle on a synchronous pipe can wait for the listening thread's
pending ReadFile. When a client is still connected and idle, as in
reconnects_after_server_restart, close() could then block until the
client sends again. close() now only marks the server closed, and the
listening thread closes the handle after its current read returns.
@Lewis-E Lewis-E changed the title Open the named pipe on first write and reopen it after a failure Named Pipe connection behavior matching Unix Stream behavior Oct 23, 2026
@Lewis-E
Lewis-E marked this pull request as ready for review October 23, 2026 19:09
@Lewis-E
Lewis-E requested a review from a team as a code owner October 23, 2026 19:09
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-23T19:15:37.765349Z 64cc96b Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 64cc96b513

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +51 to +52
if (fileChannel == null) {
randomAccessFile = new RandomAccessFile(pipe, "rw");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep explicit closure distinct from disconnection

close() calls disconnect(), which only nulls these fields, so the next write() always enters this branch and silently reopens the pipe. That violates the WritableByteChannel invariant that closure is permanent (and isOpen() also reports false before the first write), so a late internal write—such as one racing a nonblocking shutdown—can recreate the native handle after the client has closed it. Track an explicit closed flag and throw ClosedChannelException after close while reserving null for reconnectable disconnects.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ close() now marks the channel closed, so a later write() throws ClosedChannelException without opening the pipe.

@vickenty UnixStreamClientChannel has the same gap: its close() only calls disconnect(), so a later write() reconnects, and its isOpen() throws a NullPointerException before the first connect. Want to include that change in this PR while we're updating things?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, let's leep this PR scoped to windows pipes only.

close() only disconnected, so a later write reopened the pipe. A late
write during a non-blocking stop() could reopen it after the client
closed the channel. close() now marks the channel closed: writes throw
ClosedChannelException without opening the pipe, and isOpen() reports
false only after close(). A failed write still disconnects, and the next
write reopens the pipe.
Lewis-E added a commit to Lewis-E/java-dogstatsd-client that referenced this pull request Oct 23, 2026
A JVM agent can set system properties after startup but not environment
variables. serverless-compat-java will set dd.dogstatsd.pipe.name to a
per-function pipe on Azure Functions shared plans, so a client built with
default settings now uses it.

Precedence: explicit builder settings, then the system property, then the
existing DD_DOGSTATSD_URL, DD_DOGSTATSD_PIPE_NAME and DD_AGENT_HOST
environment variables.

The property pipe uses the same channel as every other named pipe. Merge
after DataDog#345, which opens that channel on the first write, so a client
built before its pipe exists doesn't fail build().

SVLS-8246
@Lewis-E
Lewis-E requested a review from vickenty October 26, 2026 18:16
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot merged commit dbccb10 into DataDog:master Oct 27, 2026
14 checks passed
Lewis-E added a commit to Lewis-E/java-dogstatsd-client that referenced this pull request Oct 27, 2026
A JVM agent can set system properties after startup but not environment
variables. serverless-compat-java will set dd.dogstatsd.pipe.name to a
per-function pipe on Azure Functions shared plans, so a client built with
default settings now uses it.

Precedence: explicit builder settings, then the system property, then the
existing DD_DOGSTATSD_URL, DD_DOGSTATSD_PIPE_NAME and DD_AGENT_HOST
environment variables.

The property pipe uses the same channel as every other named pipe. Since
DataDog#345 that channel opens on the first write, so a client built before its
pipe exists doesn't fail build().

SVLS-8246
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants