From 167c1f7044474f04298bce81c8d2b3db7b679747 Mon Sep 17 00:00:00 2001 From: "E. Lewis" Date: Thu, 15 Oct 2026 16:53:30 -0400 Subject: [PATCH 1/6] Warn when the StatsD client gives up connecting 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. --- .../metrics/impl/statsd/DDAgentStatsDConnection.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/products/metrics/metrics-lib/src/main/java/datadog/metrics/impl/statsd/DDAgentStatsDConnection.java b/products/metrics/metrics-lib/src/main/java/datadog/metrics/impl/statsd/DDAgentStatsDConnection.java index f64fdbd285c..f3fc8389722 100644 --- a/products/metrics/metrics-lib/src/main/java/datadog/metrics/impl/statsd/DDAgentStatsDConnection.java +++ b/products/metrics/metrics-lib/src/main/java/datadog/metrics/impl/statsd/DDAgentStatsDConnection.java @@ -171,7 +171,11 @@ private void doConnect() { AgentTaskScheduler.get() .scheduleWithJitter(ConnectTask.INSTANCE, this, RETRY_DELAY, SECONDS); } else { - log.debug("Max retries have been reached. Will not attempt again."); + log.warn( + "Unable to create StatsD client - {} - after {} attempts. Will not retry: {}", + statsDAddress(), + retries.get(), + e.getMessage()); } } catch (Throwable t) { if (log.isDebugEnabled()) { From 63ffe7146eac99b463f78bbd50b0c92d96498920 Mon Sep 17 00:00:00 2001 From: "E. Lewis" Date: Thu, 15 Oct 2026 16:53:40 -0400 Subject: [PATCH 2/6] Keep retrying the first StatsD connection 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. --- .../impl/statsd/DDAgentStatsDConnection.java | 43 +++++++++++++------ .../statsd/DDAgentStatsDConnectionTest.java | 18 ++++++++ 2 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 products/metrics/metrics-lib/src/test/java/datadog/metrics/impl/statsd/DDAgentStatsDConnectionTest.java diff --git a/products/metrics/metrics-lib/src/main/java/datadog/metrics/impl/statsd/DDAgentStatsDConnection.java b/products/metrics/metrics-lib/src/main/java/datadog/metrics/impl/statsd/DDAgentStatsDConnection.java index f3fc8389722..43ae9d050af 100644 --- a/products/metrics/metrics-lib/src/main/java/datadog/metrics/impl/statsd/DDAgentStatsDConnection.java +++ b/products/metrics/metrics-lib/src/main/java/datadog/metrics/impl/statsd/DDAgentStatsDConnection.java @@ -31,8 +31,8 @@ final class DDAgentStatsDConnection implements StatsDClientErrorHandler { private static final AgentThreadFactory STATSD_CLIENT_THREAD_FACTORY = new AgentThreadFactory(STATSD_CLIENT); - private static final int RETRY_DELAY = 10; - private static final int MAX_RETRIES = 20; + private static final long RETRY_DELAY = 10; + private static final long MAX_RETRY_DELAY = 60; private boolean usingDefaultPort; private volatile String host; @@ -159,24 +159,28 @@ private void doConnect() { try { statsd = clientBuilder.build(); - if (log.isDebugEnabled()) { + int failedAttempts = retries.getAndSet(0); + if (failedAttempts > 0) { + log.info( + "StatsD connected to {} after {} failed attempts", statsDAddress(), failedAttempts); + } else if (log.isDebugEnabled()) { log.debug("StatsD connected to {}", statsDAddress()); } } catch (final Exception e) { - if (retries.getAndIncrement() < MAX_RETRIES) { - if (log.isDebugEnabled()) { - log.debug( - "Scheduling StatsD connection in {} seconds - {}", RETRY_DELAY, statsDAddress()); - } - AgentTaskScheduler.get() - .scheduleWithJitter(ConnectTask.INSTANCE, this, RETRY_DELAY, SECONDS); - } else { + int failedAttempts = retries.incrementAndGet(); + long delaySeconds = connectRetryDelaySeconds(failedAttempts); + if (failedAttempts == 1) { log.warn( - "Unable to create StatsD client - {} - after {} attempts. Will not retry: {}", + "Unable to create StatsD client - {} - Will keep retrying, at least every {} seconds: {}", statsDAddress(), - retries.get(), + MAX_RETRY_DELAY, e.getMessage()); + } else if (log.isDebugEnabled()) { + log.debug( + "Scheduling StatsD connection in {} seconds - {}", delaySeconds, statsDAddress()); } + // no jitter, so the delay stays within MAX_RETRY_DELAY + AgentTaskScheduler.get().schedule(ConnectTask.INSTANCE, this, delaySeconds, SECONDS); } catch (Throwable t) { if (log.isDebugEnabled()) { // Display full stack traces on debug logs @@ -199,6 +203,19 @@ private void doConnect() { } } + /** + * Delay before the next connection attempt: doubles from {@link #RETRY_DELAY} up to {@link + * #MAX_RETRY_DELAY} seconds. Attempts never stop, so a DogStatsD that comes up late is still + * picked up. + */ + static long connectRetryDelaySeconds(int failedAttempts) { + long delaySeconds = RETRY_DELAY; + for (int i = 1; i < failedAttempts && delaySeconds < MAX_RETRY_DELAY; i++) { + delaySeconds *= 2; + } + return Math.min(delaySeconds, MAX_RETRY_DELAY); + } + @SuppressFBWarnings("DMI_HARDCODED_ABSOLUTE_FILENAME") private void discoverConnectionSettings() { if (namedPipe != null) { diff --git a/products/metrics/metrics-lib/src/test/java/datadog/metrics/impl/statsd/DDAgentStatsDConnectionTest.java b/products/metrics/metrics-lib/src/test/java/datadog/metrics/impl/statsd/DDAgentStatsDConnectionTest.java new file mode 100644 index 00000000000..7bfa9b97299 --- /dev/null +++ b/products/metrics/metrics-lib/src/test/java/datadog/metrics/impl/statsd/DDAgentStatsDConnectionTest.java @@ -0,0 +1,18 @@ +package datadog.metrics.impl.statsd; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class DDAgentStatsDConnectionTest { + + @Test + void connectRetryDelayDoublesUpToSixtySecondsAndNeverStops() { + assertEquals(10, DDAgentStatsDConnection.connectRetryDelaySeconds(1)); + assertEquals(20, DDAgentStatsDConnection.connectRetryDelaySeconds(2)); + assertEquals(40, DDAgentStatsDConnection.connectRetryDelaySeconds(3)); + assertEquals(60, DDAgentStatsDConnection.connectRetryDelaySeconds(4)); + assertEquals(60, DDAgentStatsDConnection.connectRetryDelaySeconds(1_000)); + assertEquals(60, DDAgentStatsDConnection.connectRetryDelaySeconds(Integer.MAX_VALUE)); + } +} From 036da257fa8d404ae723534b9c2e1e33bd91e3a6 Mon Sep 17 00:00:00 2001 From: "E. Lewis" Date: Thu, 15 Oct 2026 16:53:49 -0400 Subject: [PATCH 3/6] Recreate the StatsD client after writes keep failing 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. --- .../impl/statsd/DDAgentStatsDConnection.java | 75 +++++++++++++++++++ .../statsd/DDAgentStatsDConnectionTest.java | 48 ++++++++++++ 2 files changed, 123 insertions(+) diff --git a/products/metrics/metrics-lib/src/main/java/datadog/metrics/impl/statsd/DDAgentStatsDConnection.java b/products/metrics/metrics-lib/src/main/java/datadog/metrics/impl/statsd/DDAgentStatsDConnection.java index 43ae9d050af..1fcd25a5bba 100644 --- a/products/metrics/metrics-lib/src/main/java/datadog/metrics/impl/statsd/DDAgentStatsDConnection.java +++ b/products/metrics/metrics-lib/src/main/java/datadog/metrics/impl/statsd/DDAgentStatsDConnection.java @@ -3,6 +3,8 @@ import static datadog.trace.api.ConfigDefaults.DEFAULT_DOGSTATSD_SOCKET_PATH; import static datadog.trace.util.AgentThreadFactory.AgentThread.STATSD_CLIENT; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.concurrent.TimeUnit.NANOSECONDS; import static java.util.concurrent.TimeUnit.SECONDS; import com.timgroup.statsd.NoOpDirectStatsDClient; @@ -11,6 +13,7 @@ import datadog.common.filesystem.Files; import datadog.environment.OperatingSystem; import datadog.logging.IOLogger; +import datadog.logging.RatelimitedLogger; import datadog.trace.api.Config; import datadog.trace.util.AgentTaskScheduler; import datadog.trace.util.AgentThreadFactory; @@ -23,6 +26,7 @@ final class DDAgentStatsDConnection implements StatsDClientErrorHandler { private static final Logger log = LoggerFactory.getLogger(DDAgentStatsDConnection.class); private static final IOLogger ioLogger = new IOLogger(log); + private static final RatelimitedLogger recreateLogger = new RatelimitedLogger(log, 5, MINUTES); private static final com.timgroup.statsd.StatsDClient NO_OP = new NoOpDirectStatsDClient(); @@ -43,6 +47,7 @@ final class DDAgentStatsDConnection implements StatsDClientErrorHandler { private final AtomicInteger clientCount = new AtomicInteger(0); private final AtomicInteger errorCount = new AtomicInteger(0); private final AtomicInteger retries = new AtomicInteger(0); + private final WriteFailureStreak writeFailureStreak = new WriteFailureStreak(); volatile com.timgroup.statsd.StatsDClient statsd = NO_OP; @@ -59,6 +64,10 @@ public void handle(final Exception e) { errorCount.incrementAndGet(); String message = e.getClass().getSimpleName() + " in StatsD client - " + statsDAddress(); ioLogger.error(message, e); + if (writeFailureStreak.recordError(NANOSECONDS.toMillis(System.nanoTime()))) { + // handle() runs on a client thread, so close the client from another thread + AgentTaskScheduler.get().schedule(RecreateTask.INSTANCE, this, 0, MILLISECONDS); + } } public void acquire() { @@ -259,6 +268,29 @@ void handleDefaultPortChange(final int newPort) { } } + /** + * The client never reopens its named pipe or socket, so after the DogStatsD it was writing to + * goes away, every write fails even once a new DogStatsD serves the same name. + */ + private void recreateClient() { + synchronized (this) { + if (NO_OP != statsd && clientCount.get() > 0) { + recreateLogger.warn( + "StatsD client writes to {} have kept failing for at least {} seconds; recreating the client", + statsDAddress(), + MILLISECONDS.toSeconds(WriteFailureStreak.RECREATE_AFTER_MILLIS)); + try { + statsd.close(); + } catch (final Exception e) { + log.debug("Problem closing StatsD client - {}", statsDAddress(), e); + } finally { + statsd = NO_OP; + } + doConnect(); + } + } + } + private void doClose() { synchronized (this) { if (NO_OP != statsd && 0 == clientCount.get()) { @@ -284,6 +316,49 @@ private String statsDAddress() { return (null != host ? host : "") + (null != port && port > 0 ? ":" + port : ""); } + /** + * Tracks how long StatsD writes have kept failing. Errors more than {@link #MAX_ERROR_GAP_MILLIS} + * apart start a new streak, and a streak needs {@link #MIN_ERRORS} errors spanning {@link + * #RECREATE_AFTER_MILLIS}, so brief or isolated timeouts under load do not recreate the client. + */ + static final class WriteFailureStreak { + static final long RECREATE_AFTER_MILLIS = 30_000; + // longer than the 30 second tracer health metrics flush, so a dead pipe keeps one streak going + static final long MAX_ERROR_GAP_MILLIS = 60_000; + static final int MIN_ERRORS = 3; + + private int errors; + private long firstErrorMillis; + private long lastErrorMillis; + + /** + * @return true when the client should be recreated; the streak then starts over + */ + synchronized boolean recordError(long nowMillis) { + if (errors == 0 || nowMillis - lastErrorMillis > MAX_ERROR_GAP_MILLIS) { + errors = 0; + firstErrorMillis = nowMillis; + } + errors++; + lastErrorMillis = nowMillis; + if (errors >= MIN_ERRORS && nowMillis - firstErrorMillis >= RECREATE_AFTER_MILLIS) { + errors = 0; + return true; + } + return false; + } + } + + private static final class RecreateTask + implements AgentTaskScheduler.Task { + public static final RecreateTask INSTANCE = new RecreateTask(); + + @Override + public void run(final DDAgentStatsDConnection target) { + target.recreateClient(); + } + } + private static final class ConnectTask implements AgentTaskScheduler.Task { public static final ConnectTask INSTANCE = new ConnectTask(); diff --git a/products/metrics/metrics-lib/src/test/java/datadog/metrics/impl/statsd/DDAgentStatsDConnectionTest.java b/products/metrics/metrics-lib/src/test/java/datadog/metrics/impl/statsd/DDAgentStatsDConnectionTest.java index 7bfa9b97299..df493049353 100644 --- a/products/metrics/metrics-lib/src/test/java/datadog/metrics/impl/statsd/DDAgentStatsDConnectionTest.java +++ b/products/metrics/metrics-lib/src/test/java/datadog/metrics/impl/statsd/DDAgentStatsDConnectionTest.java @@ -1,6 +1,8 @@ package datadog.metrics.impl.statsd; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; @@ -15,4 +17,50 @@ void connectRetryDelayDoublesUpToSixtySecondsAndNeverStops() { assertEquals(60, DDAgentStatsDConnection.connectRetryDelaySeconds(1_000)); assertEquals(60, DDAgentStatsDConnection.connectRetryDelaySeconds(Integer.MAX_VALUE)); } + + @Test + void writeErrorsAboutFifteenSecondsApartRecreateAfterThirtySeconds() { + DDAgentStatsDConnection.WriteFailureStreak streak = + new DDAgentStatsDConnection.WriteFailureStreak(); + + assertFalse(streak.recordError(0)); + assertFalse(streak.recordError(15_000)); + assertTrue(streak.recordError(30_000)); + + // the streak starts over after a recreate + assertFalse(streak.recordError(45_000)); + assertFalse(streak.recordError(60_000)); + assertTrue(streak.recordError(75_000)); + } + + @Test + void isolatedWriteErrorsDoNotRecreate() { + DDAgentStatsDConnection.WriteFailureStreak streak = + new DDAgentStatsDConnection.WriteFailureStreak(); + + assertFalse(streak.recordError(0)); + assertFalse(streak.recordError(30_000)); + } + + @Test + void briefBurstOfWriteErrorsDoesNotRecreate() { + DDAgentStatsDConnection.WriteFailureStreak streak = + new DDAgentStatsDConnection.WriteFailureStreak(); + + for (long now = 0; now < 29_000; now += 100) { + assertFalse(streak.recordError(now)); + } + } + + @Test + void gapOfMoreThanSixtySecondsStartsANewStreak() { + DDAgentStatsDConnection.WriteFailureStreak streak = + new DDAgentStatsDConnection.WriteFailureStreak(); + + assertFalse(streak.recordError(0)); + assertFalse(streak.recordError(15_000)); + assertFalse(streak.recordError(75_001)); + assertFalse(streak.recordError(90_000)); + assertTrue(streak.recordError(105_001)); + } } From 47e60f9ec78defc46d5e604a21ab4d20a56c6c67 Mon Sep 17 00:00:00 2001 From: "E. Lewis" Date: Thu, 15 Oct 2026 16:59:36 -0400 Subject: [PATCH 4/6] Open a new named pipe connection for each socket 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. --- .../common/socket/NamedPipeSocketFactory.java | 21 +++++--------- .../socket/NamedPipeSocketFactoryTest.java | 28 +++++++++++++++++++ 2 files changed, 35 insertions(+), 14 deletions(-) create mode 100644 utils/socket-utils/src/test/java/datadog/common/socket/NamedPipeSocketFactoryTest.java diff --git a/utils/socket-utils/src/main/java/datadog/common/socket/NamedPipeSocketFactory.java b/utils/socket-utils/src/main/java/datadog/common/socket/NamedPipeSocketFactory.java index 02fe777d130..48da4d21003 100644 --- a/utils/socket-utils/src/main/java/datadog/common/socket/NamedPipeSocketFactory.java +++ b/utils/socket-utils/src/main/java/datadog/common/socket/NamedPipeSocketFactory.java @@ -4,13 +4,10 @@ import java.io.IOException; import java.net.InetAddress; import java.net.Socket; -import java.util.HashMap; -import java.util.Map; import javax.net.SocketFactory; public class NamedPipeSocketFactory extends SocketFactory { private static final String NAMED_PIPE_PREFIX = "\\\\.\\pipe\\"; - private static final Map fileToSocket = new HashMap<>(); private final File pipe; public NamedPipeSocketFactory(String pipeName) { @@ -19,19 +16,15 @@ public NamedPipeSocketFactory(String pipeName) { this.pipe = new File(pipeNameWithPrefix); } + NamedPipeSocketFactory(File pipe) { + this.pipe = pipe; + } + @Override public Socket createSocket() throws IOException { - // Getting a new socket is rare enough that simple synchronization is sufficient - synchronized (fileToSocket) { - NamedPipeSocket socket = fileToSocket.get(pipe); - - if (socket == null || socket.isClosed()) { - socket = new NamedPipeSocket(pipe); - fileToSocket.put(pipe, socket); - } - - return socket; - } + // Each connection gets its own pipe handle. A shared handle lets a blocked read on one + // connection, such as a pooled-connection health check, block every other request. + return new NamedPipeSocket(pipe); } @Override diff --git a/utils/socket-utils/src/test/java/datadog/common/socket/NamedPipeSocketFactoryTest.java b/utils/socket-utils/src/test/java/datadog/common/socket/NamedPipeSocketFactoryTest.java new file mode 100644 index 00000000000..d3251e822b0 --- /dev/null +++ b/utils/socket-utils/src/test/java/datadog/common/socket/NamedPipeSocketFactoryTest.java @@ -0,0 +1,28 @@ +package datadog.common.socket; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; + +import java.io.File; +import java.net.Socket; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class NamedPipeSocketFactoryTest { + + @Test + void createsANewSocketForEachConnection(@TempDir Path tempDir) throws Exception { + // a regular file stands in for the pipe, since NamedPipeSocket only opens it as a file + File pipe = tempDir.resolve("pipe").toFile(); + NamedPipeSocketFactory factory = new NamedPipeSocketFactory(pipe); + + try (Socket first = factory.createSocket(); + Socket second = factory.createSocket()) { + assertNotSame(first, second); + + first.close(); + assertFalse(second.isClosed()); + } + } +} From 3e1437c61f4e9c17cc9db85be4242eac5a07f365 Mon Sep 17 00:00:00 2001 From: "E. Lewis" Date: Thu, 15 Oct 2026 17:00:29 -0400 Subject: [PATCH 5/6] Don't reuse named pipe connections 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. --- .../java/datadog/communication/http/OkHttpUtils.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/communication/src/main/java/datadog/communication/http/OkHttpUtils.java b/communication/src/main/java/datadog/communication/http/OkHttpUtils.java index 44105ae772d..9dc5cbd4c41 100644 --- a/communication/src/main/java/datadog/communication/http/OkHttpUtils.java +++ b/communication/src/main/java/datadog/communication/http/OkHttpUtils.java @@ -177,12 +177,13 @@ private static OkHttpClient buildHttpClient( .dispatcher( dispatcher != null ? dispatcher : new Dispatcher(RejectingExecutorService.INSTANCE)); + final boolean useNamedPipe = unixDomainSocketPath == null && namedPipe != null; if (unixDomainSocketPath != null) { builder.socketFactory( new UnixDomainSocketFactory( new File(unixDomainSocketPath), useJdkUnixDomainSocket, agentConfiguredUsingDefault)); log.debug("Using UnixDomainSocket as http transport"); - } else if (namedPipe != null) { + } else if (useNamedPipe) { builder.socketFactory(new NamedPipeSocketFactory(namedPipe)); log.debug("Using NamedPipe as http transport"); } @@ -204,7 +205,11 @@ private static OkHttpClient buildHttpClient( builder.retryOnConnectionFailure(retryOnConnectionFailure); } - if (maxRunningRequests != null) { + if (useNamedPipe) { + // NamedPipeSocket can't time out a read, so OkHttp's health check on a pooled connection can + // block forever. Don't keep idle pipe connections; each request opens its own. + builder.connectionPool(new ConnectionPool(0, 1, SECONDS)); + } else if (maxRunningRequests != null) { // Reusing connections causes non daemon threads to be created which causes agent to prevent // app from exiting. See https://github.com/square/okhttp/issues/4029 for some details. builder.connectionPool(new ConnectionPool(maxRunningRequests, 1, SECONDS)); From 476730067b289950abb24e96d05ae58a70bff691 Mon Sep 17 00:00:00 2001 From: "E. Lewis" Date: Thu, 15 Oct 2026 17:02:23 -0400 Subject: [PATCH 6/6] Connect tracer StatsD over a named pipe without a port 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. --- .../metrics/impl/statsd/DDAgentStatsDConnection.java | 8 ++++++-- .../impl/statsd/DDAgentStatsDConnectionTest.java | 10 ++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/products/metrics/metrics-lib/src/main/java/datadog/metrics/impl/statsd/DDAgentStatsDConnection.java b/products/metrics/metrics-lib/src/main/java/datadog/metrics/impl/statsd/DDAgentStatsDConnection.java index 1fcd25a5bba..efb0c51f6cf 100644 --- a/products/metrics/metrics-lib/src/main/java/datadog/metrics/impl/statsd/DDAgentStatsDConnection.java +++ b/products/metrics/metrics-lib/src/main/java/datadog/metrics/impl/statsd/DDAgentStatsDConnection.java @@ -40,7 +40,7 @@ final class DDAgentStatsDConnection implements StatsDClientErrorHandler { private boolean usingDefaultPort; private volatile String host; - private volatile Integer port; + volatile Integer port; private final String namedPipe; private final boolean useAggregation; @@ -226,8 +226,12 @@ static long connectRetryDelaySeconds(int failedAttempts) { } @SuppressFBWarnings("DMI_HARDCODED_ABSOLUTE_FILENAME") - private void discoverConnectionSettings() { + void discoverConnectionSettings() { if (namedPipe != null) { + if (null == port) { + // the client ignores the port for a named pipe, but the builder needs one + port = 0; + } return; } diff --git a/products/metrics/metrics-lib/src/test/java/datadog/metrics/impl/statsd/DDAgentStatsDConnectionTest.java b/products/metrics/metrics-lib/src/test/java/datadog/metrics/impl/statsd/DDAgentStatsDConnectionTest.java index df493049353..4103f8f9542 100644 --- a/products/metrics/metrics-lib/src/test/java/datadog/metrics/impl/statsd/DDAgentStatsDConnectionTest.java +++ b/products/metrics/metrics-lib/src/test/java/datadog/metrics/impl/statsd/DDAgentStatsDConnectionTest.java @@ -18,6 +18,16 @@ void connectRetryDelayDoublesUpToSixtySecondsAndNeverStops() { assertEquals(60, DDAgentStatsDConnection.connectRetryDelaySeconds(Integer.MAX_VALUE)); } + @Test + void namedPipeConnectionUsesPortZero() { + DDAgentStatsDConnection connection = + new DDAgentStatsDConnection(null, null, "dogstatsd", false); + + connection.discoverConnectionSettings(); + + assertEquals(Integer.valueOf(0), connection.port); + } + @Test void writeErrorsAboutFifteenSecondsApartRecreateAfterThirtySeconds() { DDAgentStatsDConnection.WriteFailureStreak streak =