From aa7960b27a1764862e6bc4f53f972168e9d6b9d3 Mon Sep 17 00:00:00 2001 From: Daniel McCoy Stephenson Date: Sat, 24 Oct 2026 01:55:29 -0600 Subject: [PATCH] Cover the sender thread and an interrupted close() The class Javadoc promises a single daemon thread, and close() cuts the drain short and restores the interrupt when the caller is interrupted. Neither was asserted. One test holds a request on a slow server and checks that exactly one thread named trace-client/ exists and that it is a daemon. The other interrupts the caller before close() and checks that close() returns well inside the drain timeout with the interrupt flag still set. Co-Authored-By: Claude Opus 5.5 --- .../stephenson/trace/TraceClientTest.java | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/test/java/software/stephenson/trace/TraceClientTest.java b/src/test/java/software/stephenson/trace/TraceClientTest.java index d80b902..536d510 100644 --- a/src/test/java/software/stephenson/trace/TraceClientTest.java +++ b/src/test/java/software/stephenson/trace/TraceClientTest.java @@ -594,6 +594,75 @@ void close_stillReturnsWithinTheTimeoutWhenTheServerHangs() throws Exception { slow.stop(0); } + @Test + void close_whenInterruptedReturnsAtOnceAndKeepsTheInterruptFlag() throws Exception { + // Arrange + // A server holding a request, so close() would otherwise wait out + // the whole drain timeout. A host shutting down on an interrupt must + // not wait that long, and must still see that it was interrupted. + CountDownLatch inFlight = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + HttpServer slow = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + slow.createContext("/", exchange -> { + inFlight.countDown(); + try { release.await(15, TimeUnit.SECONDS); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); } + exchange.sendResponseHeaders(201, -1); + exchange.close(); + }); + slow.start(); + TraceClient client = TraceClient.builder("http://127.0.0.1:" + slow.getAddress().getPort(), "MyCli", "1.2.3").key("k").build(); + client.report("startup"); + assertTrue(inFlight.await(5, TimeUnit.SECONDS), "the report should reach the server"); + + // Act + Thread.currentThread().interrupt(); + long before = System.nanoTime(); + client.close(); + long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - before); + boolean stillInterrupted = Thread.interrupted(); // clears it, so later tests run uninterrupted + + // Assert + assertTrue(stillInterrupted, "close() must restore the interrupt it swallowed"); + assertTrue(elapsedMs < 2_000, "close() took " + elapsedMs + " ms; an interrupt must cut the drain short"); + release.countDown(); + slow.stop(0); + } + + @Test + void report_sendsFromOneDaemonThreadNamedForTheApplication() throws Exception { + // Arrange + // A server holding the first request, so the sender thread is alive + // and busy while the rest queue behind it. + CountDownLatch inFlight = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + HttpServer slow = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + slow.createContext("/", exchange -> { + inFlight.countDown(); + try { release.await(15, TimeUnit.SECONDS); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); } + exchange.sendResponseHeaders(201, -1); + exchange.close(); + }); + slow.start(); + TraceClient client = TraceClient.builder("http://127.0.0.1:" + slow.getAddress().getPort(), "ThreadProbe", "1.2.3") + .key("k").build(); + + // Act + for (int i = 0; i < 5; i++) { + client.report("startup"); + } + assertTrue(inFlight.await(5, TimeUnit.SECONDS), "the first report should reach the server"); + List senders = Thread.getAllStackTraces().keySet().stream() + .filter(thread -> thread.getName().equals("trace-client/ThreadProbe")) + .collect(Collectors.toList()); + + // Assert + assertEquals(1, senders.size(), "one sender thread, however many reports wait"); + assertTrue(senders.get(0).isDaemon(), "a daemon thread never keeps the host's JVM from exiting"); + release.countDown(); + client.close(); + slow.stop(0); + } + @Test void close_isSafeToCallTwice() throws Exception { // Arrange