Skip to content

Add tests reproducing stale TCP connection issue (#954) - #955

Merged
komamitsu merged 16 commits into
mainfrom
test-954-stale-connection
Jun 28, 2026
Merged

Add tests reproducing stale TCP connection issue (#954)#955
komamitsu merged 16 commits into
mainfrom
test-954-stale-connection

Conversation

@komamitsu

Copy link
Copy Markdown
Owner

Summary

  • Adds TCPSenderTest cases that document RST and FIN reconnection behavior at the sender level, explaining why graceful FIN causes silent data loss (TCP two-write rule) while ACK mode avoids it
  • Adds testAckModeGuaranteesDeliveryAfterServerDropsConnections to FluencyTestWithMockServer to demonstrate end-to-end that ACK mode (setAckResponseMode(true)) delivers all records after a simulated Fluentd restart that drops all TCP connections

Closes #954

TCPSenderTest documents the RST and FIN reconnection behavior at the
sender level. FluencyTestWithMockServer demonstrates that ACK mode
delivers all records after a simulated Fluentd restart that drops all
TCP connections.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request adds new tests to reproduce and verify connection recovery behavior under issue #954, including ACK mode delivery guarantees during connection drops in FluencyTestWithMockServer.java and TCP reconnection behavior after RST and FIN close scenarios in TCPSenderTest.java. The feedback points out a potential resource leak in the new TCPSenderTest helper method, recommending that the mock server and sender be closed within try-finally and try-with-resources blocks to ensure proper cleanup if assertions fail.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Copilot AI left a comment

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.

Pull request overview

This PR adds regression/reproducer tests around stale TCP connections after a Fluentd restart (issue #954), with both low-level TCPSender behavior documentation and an end-to-end Fluency/ACK-mode verification using a mock server.

Changes:

  • Add TCPSenderTest cases covering reconnect behavior when the server closes sockets via RST vs FIN.
  • Add an end-to-end test in FluencyTestWithMockServer showing ACK mode delivers records after the server drops existing TCP connections.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
fluency-fluentd/src/test/java/org/komamitsu/fluency/fluentd/ingester/sender/TCPSenderTest.java Adds RST/FIN reproducer tests and explanatory Javadoc around stale-connection behavior.
fluency-fluentd/src/test/java/org/komamitsu/fluency/fluentd/FluencyTestWithMockServer.java Adds end-to-end ACK-mode test that simulates a server-side connection drop and verifies delivery.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Wrap sender in try-with-resources and server.stop() in finally to
  prevent resource leaks on assertion failure
- Assert connectCount > 1 to verify an actual reconnect occurred, not
  just consecutive successes on the stale socket
- Fix Javadoc cross-reference to point to FluencyTestWithMockServer
@komamitsu
komamitsu requested a review from Copilot June 27, 2026 14:49
@komamitsu

Copy link
Copy Markdown
Owner Author

/gemini review

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request adds test coverage to reproduce and verify issue #954, introducing tests for connection drops and reconnection behavior under ACK mode, RST, and FIN scenarios. The reviewer suggested a minor improvement in TCPSenderTest.java to extract firstAcceptedSocket.get() into a local variable and assert that it is not null before performing operations on it, avoiding multiple dereferences.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +381 to +385
if (abruptClose) {
// RST path: SO_LINGER with timeout=0 makes close() send RST instead of FIN
firstAcceptedSocket.get().setSoLinger(true, 0);
}
firstAcceptedSocket.get().close();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

To adhere to defensive programming practices and avoid multiple dereferences of the AtomicReference, it is recommended to retrieve the socket into a local variable, assert that it is not null, and then perform the operations on it.

Suggested change
if (abruptClose) {
// RST path: SO_LINGER with timeout=0 makes close() send RST instead of FIN
firstAcceptedSocket.get().setSoLinger(true, 0);
}
firstAcceptedSocket.get().close();
Socket socket = firstAcceptedSocket.get();
assertThat(socket).isNotNull();
if (abruptClose) {
// RST path: SO_LINGER with timeout=0 makes close() send RST instead of FIN
socket.setSoLinger(true, 0);
}
socket.close();

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment on lines +395 to +409
int consecutiveSuccesses = 0;
for (int attempt = 0; attempt < 10 && consecutiveSuccesses < 3; attempt++) {
try {
sender.send(ByteBuffer.wrap(data));
LOG.debug("Send succeeded on attempt {}", attempt);
if (connectCount.get() > 1) {
consecutiveSuccesses++;
} else {
consecutiveSuccesses = 0;
}
} catch (IOException e) {
LOG.debug("Attempt {} failed (expected on stale socket): {}", attempt, e.getMessage());
consecutiveSuccesses = 0;
}
}
for (int i = 0; i < recordsBeforeDrop; i++) {
fluency.emit("tag", data);
}
fluency.waitUntilAllBufferFlushed(10);
for (int i = 0; i < recordsAfterDrop; i++) {
fluency.emit("tag", data);
}
fluency.waitUntilAllBufferFlushed(30);
- Replace tight attempt loop with a CountDownLatch on the second
  onConnect event so the reconnect assertion is timing-independent
- Add 100ms sleep between send attempts to give the OS time to deliver
  FIN/RST before the loop exhausts its retries
- Extract firstAcceptedSocket.get() to a local variable with null check
- Assert waitUntilAllBufferFlushed() return value so flush timeouts
  surface as clear failures rather than record-count mismatches
@komamitsu
komamitsu requested a review from Copilot June 28, 2026 00:39
@komamitsu

Copy link
Copy Markdown
Owner Author

/gemini review

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces integration and unit tests to reproduce and verify behavior related to connection drops (issue #954). Specifically, it adds a test in FluencyTestWithMockServer to ensure that ACK mode guarantees message delivery when the server drops connections. Additionally, it adds tests in TCPSenderTest to verify that TCPSender successfully reconnects after both abrupt (RST) and graceful (FIN) connection closures by the server. As there are no review comments, I have no feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment on lines +319 to +323
* <p>Scenario B (graceful / FIN): The server closes normally, sending FIN. Due to TCP half-close
* the first write may silently "succeed" — the OS places data in the send buffer before the RST
* triggered by the peer's close arrives. That data is silently lost at the TCPSender level. The
* second write fails ("Broken pipe"), closeSocket() nulls the channel, and the third send
* reconnects successfully.
Comment on lines +369 to +372
assertTrue(
fluency.waitUntilAllBufferFlushed(10), "Buffer should flush before dropping connections");

LOG.info("Dropping {} connections to simulate Fluentd restart", acceptedSockets.size());
- Clarify Javadoc: silent data loss in the FIN case is a potential
  concern; this test only verifies reconnection, not data loss
- Assert acceptedSockets is non-empty before dropping connections to
  prevent the drop step from being a silent no-op
@komamitsu
komamitsu requested a review from Copilot June 28, 2026 00:52
@komamitsu

Copy link
Copy Markdown
Owner Author

/gemini review

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces new integration and unit tests to verify reconnection behavior and message delivery guarantees under connection drop scenarios (both graceful FIN and abrupt RST) for ACK mode and TCPSender. The reviewer feedback highlights critical resource leak risks in both test files where server.start() is called outside the try-finally block, meaning a startup failure would leak background threads and sockets. Additionally, the reviewer suggests increasing the retry limit in the TCPSender test to avoid flakiness in slower CI environments.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +353 to +390
server.start();

int recordsBeforeDrop = 200;
int recordsAfterDrop = 200;

FluencyBuilderForFluentd builder = new FluencyBuilderForFluentd();
builder.setAckResponseMode(true);
builder.setFlushAttemptIntervalMillis(200);

try (Fluency fluency = builder.build(server.getLocalPort())) {
Map<String, Object> data = new HashMap<>();
data.put("key", "value");

for (int i = 0; i < recordsBeforeDrop; i++) {
fluency.emit("tag", data);
}
assertTrue(
fluency.waitUntilAllBufferFlushed(10), "Buffer should flush before dropping connections");

assertThat(acceptedSockets)
.as("At least one connection must have been established")
.isNotEmpty();
LOG.info("Dropping {} connections to simulate Fluentd restart", acceptedSockets.size());
for (Socket socket : acceptedSockets) {
try {
socket.close();
} catch (IOException e) {
LOG.warn("Failed to close socket", e);
}
}

for (int i = 0; i < recordsAfterDrop; i++) {
fluency.emit("tag", data);
}
assertTrue(fluency.waitUntilAllBufferFlushed(30), "Buffer should flush after reconnection");
} finally {
server.stop();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If server.start() throws an exception (e.g., due to port binding issues or timeout) or if FluencyBuilderForFluentd initialization fails, the finally block is never reached. This will leak the background thread pool and server socket of AbstractFluentdServer, potentially causing subsequent tests to fail due to port conflicts or resource exhaustion.

Wrapping the server startup and builder initialization in the outer try-finally block ensures that resources are always cleaned up properly.

    try {
      server.start();

      int recordsBeforeDrop = 200;
      int recordsAfterDrop = 200;

      FluencyBuilderForFluentd builder = new FluencyBuilderForFluentd();
      builder.setAckResponseMode(true);
      builder.setFlushAttemptIntervalMillis(200);

      try (Fluency fluency = builder.build(server.getLocalPort())) {
        Map<String, Object> data = new HashMap<>();
        data.put("key", "value");

        for (int i = 0; i < recordsBeforeDrop; i++) {
          fluency.emit("tag", data);
        }
        assertTrue(
            fluency.waitUntilAllBufferFlushed(10), "Buffer should flush before dropping connections");

        assertThat(acceptedSockets)
            .as("At least one connection must have been established")
            .isNotEmpty();
        LOG.info("Dropping {} connections to simulate Fluentd restart", acceptedSockets.size());
        for (Socket socket : acceptedSockets) {
          try {
            socket.close();
          } catch (IOException e) {
            LOG.warn("Failed to close socket", e);
          }
        }

        for (int i = 0; i < recordsAfterDrop; i++) {
          fluency.emit("tag", data);
        }
        assertTrue(fluency.waitUntilAllBufferFlushed(30), "Buffer should flush after reconnection");
      }
    } finally {
      server.stop();
    }

Comment on lines +374 to +427
server.start();

TCPSender.Config config = new TCPSender.Config();
config.setPort(server.getLocalPort());

try (TCPSender sender = new TCPSender(config)) {
byte[] data = "hello".getBytes(StandardCharsets.UTF_8);

// Establish the connection by sending initial data
sender.send(ByteBuffer.wrap(data));
assertTrue(firstDataReceivedLatch.await(5, TimeUnit.SECONDS));

Socket socket = firstAcceptedSocket.get();
assertThat(socket).isNotNull();
if (abruptClose) {
// RST path: SO_LINGER with timeout=0 makes close() send RST instead of FIN
socket.setSoLinger(true, 0);
}
socket.close();

// Keep sending until the server observes the reconnect (second onConnect).
// - RST path: the first send fails ("Connection reset"), the next send reconnects.
// - FIN path: sends may silently "succeed" (TCP half-close) before the RST triggers
// closeSocket(); after that a send fails ("Broken pipe") and the next reconnects.
for (int attempt = 0; attempt < 20 && reconnectedLatch.getCount() > 0; attempt++) {
try {
sender.send(ByteBuffer.wrap(data));
LOG.debug("Send succeeded on attempt {}", attempt);
} catch (IOException e) {
LOG.debug("Attempt {} failed (expected on stale socket): {}", attempt, e.getMessage());
}
TimeUnit.MILLISECONDS.sleep(100);
}

assertTrue(
reconnectedLatch.await(10, TimeUnit.SECONDS),
"TCPSender should reconnect after the server closed the connection");

// Verify stable operation on the new connection
int consecutiveSuccesses = 0;
for (int attempt = 0; attempt < 10 && consecutiveSuccesses < 3; attempt++) {
try {
sender.send(ByteBuffer.wrap(data));
consecutiveSuccesses++;
} catch (IOException e) {
consecutiveSuccesses = 0;
}
}
assertTrue(
consecutiveSuccesses >= 3,
"TCPSender should reach stable operation on the new connection");
} finally {
server.stop();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This addresses two issues:

  1. Resource Leak: If server.start() or configuration throws an exception, the finally block is never reached, leaking the background thread pool and socket of MockTCPServer.
  2. Test Flakiness: In slow or resource-constrained CI environments, the reconnection might take longer than 2 seconds. Since TCPSender only attempts reconnection lazily when send() is called, stopping the send() loop after 20 attempts (2 seconds) means no further reconnection attempts will be made, causing the subsequent reconnectedLatch.await(10, TimeUnit.SECONDS) to time out and fail. Increasing the maximum attempts to 100 (10 seconds) matches the latch timeout and prevents flakiness without slowing down successful runs.
    try {
      server.start();

      TCPSender.Config config = new TCPSender.Config();
      config.setPort(server.getLocalPort());

      try (TCPSender sender = new TCPSender(config)) {
        byte[] data = "hello".getBytes(StandardCharsets.UTF_8);

        // Establish the connection by sending initial data
        sender.send(ByteBuffer.wrap(data));
        assertTrue(firstDataReceivedLatch.await(5, TimeUnit.SECONDS));

        Socket socket = firstAcceptedSocket.get();
        assertThat(socket).isNotNull();
        if (abruptClose) {
          // RST path: SO_LINGER with timeout=0 makes close() send RST instead of FIN
          socket.setSoLinger(true, 0);
        }
        socket.close();

        // Keep sending until the server observes the reconnect (second onConnect).
        // - RST path: the first send fails ("Connection reset"), the next send reconnects.
        // - FIN path: sends may silently "succeed" (TCP half-close) before the RST triggers
        //   closeSocket(); after that a send fails ("Broken pipe") and the next reconnects.
        for (int attempt = 0; attempt < 100 && reconnectedLatch.getCount() > 0; attempt++) {
          try {
            sender.send(ByteBuffer.wrap(data));
            LOG.debug("Send succeeded on attempt {}", attempt);
          } catch (IOException e) {
            LOG.debug("Attempt {} failed (expected on stale socket): {}", attempt, e.getMessage());
          }
          TimeUnit.MILLISECONDS.sleep(100);
        }

        assertTrue(
            reconnectedLatch.await(10, TimeUnit.SECONDS),
            "TCPSender should reconnect after the server closed the connection");

        // Verify stable operation on the new connection
        int consecutiveSuccesses = 0;
        for (int attempt = 0; attempt < 10 && consecutiveSuccesses < 3; attempt++) {
          try {
            sender.send(ByteBuffer.wrap(data));
            consecutiveSuccesses++;
          } catch (IOException e) {
            consecutiveSuccesses = 0;
          }
        }
        assertTrue(
            consecutiveSuccesses >= 3,
            "TCPSender should reach stable operation on the new connection");
      }
    } finally {
      server.stop();
    }

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines +394 to +410
// Keep sending until the server observes the reconnect (second onConnect).
// - RST path: the first send fails ("Connection reset"), the next send reconnects.
// - FIN path: sends may silently "succeed" (TCP half-close) before the RST triggers
// closeSocket(); after that a send fails ("Broken pipe") and the next reconnects.
for (int attempt = 0; attempt < 20 && reconnectedLatch.getCount() > 0; attempt++) {
try {
sender.send(ByteBuffer.wrap(data));
LOG.debug("Send succeeded on attempt {}", attempt);
} catch (IOException e) {
LOG.debug("Attempt {} failed (expected on stale socket): {}", attempt, e.getMessage());
}
TimeUnit.MILLISECONDS.sleep(100);
}

assertTrue(
reconnectedLatch.await(10, TimeUnit.SECONDS),
"TCPSender should reconnect after the server closed the connection");
Wrap the body after server.start() in try/finally in both
TCPSenderTest and FluencyTestWithMockServer so the server is always
stopped if builder.build() or TCPSender construction throws.
@komamitsu
komamitsu requested a review from Copilot June 28, 2026 01:02
@komamitsu

Copy link
Copy Markdown
Owner Author

/gemini review

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request adds tests to verify reconnection behavior and guarantee message delivery in ACK mode when the server drops connections, addressing issue #954. The review feedback suggests increasing the retry attempts in the TCPSenderTest reconnection loop from 20 to 100 to prevent potential test flakiness in slow CI environments.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

// - RST path: the first send fails ("Connection reset"), the next send reconnects.
// - FIN path: sends may silently "succeed" (TCP half-close) before the RST triggers
// closeSocket(); after that a send fails ("Broken pipe") and the next reconnects.
for (int attempt = 0; attempt < 20 && reconnectedLatch.getCount() > 0; attempt++) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In slow or resource-constrained CI environments (such as GitHub Actions), 20 attempts with a 100ms sleep (totaling 2 seconds) might not be sufficient for the TCP handshake and reconnection to complete. Since TCPSender only reconnects when a send operation is actively attempted, if the loop finishes before the reconnection is established, the test will fail.\n\nIncreasing the maximum number of attempts to 100 (allowing up to 10 seconds) to match the subsequent reconnectedLatch.await(10, TimeUnit.SECONDS) timeout will make the test much more resilient and prevent flakiness.

Suggested change
for (int attempt = 0; attempt < 20 && reconnectedLatch.getCount() > 0; attempt++) {
for (int attempt = 0; attempt < 100 && reconnectedLatch.getCount() > 0; attempt++) {

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces new tests to reproduce and verify reconnection behavior and delivery guarantees after server connection drops (addressing issue #954). Specifically, it adds an ACK mode delivery test in FluencyTestWithMockServer and reconnection tests simulating both abrupt (RST) and graceful (FIN) connection closures in TCPSenderTest. The review feedback suggests moving the server.start() calls inside the try blocks in both test files to guarantee proper resource cleanup and server shutdown in case start() fails.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +364 to +365
server.start();
try {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

To ensure that the mock server is properly stopped and resources (like threads and ports) are cleaned up even if server.start() fails or throws an exception during test execution, it is recommended to move server.start() inside the try block.

Suggested change
server.start();
try {
try {
server.start();

Comment on lines +374 to +375
server.start();
try {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

To ensure that the mock server is properly stopped and resources are cleaned up even if server.start() fails or throws an exception during test execution, it is recommended to move server.start() inside the try block.

Suggested change
server.start();
try {
try {
server.start();

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines +319 to +323
* <p>Scenario B (graceful / FIN): The server closes normally, sending FIN. Due to TCP half-close
* the first write may silently "succeed" — the OS places data in the send buffer before the RST
* triggered by the peer's close arrives. That data may be silently lost at the TCPSender level
* (this test only verifies reconnection, not data loss). The second write fails ("Broken pipe"),
* closeSocket() nulls the channel, and the third send reconnects successfully.
- Move server.start() inside the try block in both test files so the
  finally clause runs server.stop() even if start() throws
- Soften the FIN-path Javadoc: "one or more writes may silently succeed"
  rather than claiming exactly second/third writes fail/reconnect
Ensures the assertion runs (and reports the correct failure) even if
fluency.close() or other cleanup throws an exception.
@komamitsu
komamitsu requested a review from Copilot June 28, 2026 05:59
@komamitsu

Copy link
Copy Markdown
Owner Author

/gemini review

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces new tests to verify connection recovery and delivery guarantees when the Fluentd server drops TCP connections, addressing issue #954. Specifically, it adds tests to ensure ACK mode guarantees delivery after connection drops, and that TCPSender successfully reconnects after both abrupt (RST) and graceful (FIN) server-side closures. The reviewer suggested adding a small backoff delay during the stability check's failure path in TCPSenderTest to prevent rapid exhaustion of the retry budget on transient failures, making the test more robust in CI environments.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +424 to +429
} catch (IOException e) {
LOG.debug(
"Send failed in stability check (failure {}): {}", failureCount, e.getMessage());
consecutiveSuccesses = 0;
failureCount++;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

During the stability check, if a send attempt fails due to a transient socket or network state, retrying immediately without any delay can rapidly exhaust the failureCount budget (up to 10 attempts) in a few microseconds. Adding a small backoff delay (e.g., 100 milliseconds) inside the catch block will make the test significantly more robust and less prone to flakiness in slow or resource-constrained CI environments.

          } catch (IOException e) {
            LOG.debug(
                "Send failed in stability check (failure {}): {}", failureCount, e.getMessage());
            consecutiveSuccesses = 0;
            failureCount++;
            try {
              TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException ie) {
              Thread.currentThread().interrupt();
              throw new IOException("Interrupted during stability check backoff", ie);
            }
          }

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Reconnection is already confirmed by the latch before this point,
so failures here indicate a real problem. No tolerance needed.
ACK mode guarantees at-least-once, but in this test all pre-drop records
are fully ACKed before connections are dropped, so duplicates should never
occur. Assert that invariant explicitly.
@komamitsu
komamitsu requested a review from Copilot June 28, 2026 07:16
@komamitsu

Copy link
Copy Markdown
Owner Author

/gemini review

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces reproducer tests for issue #954. Specifically, it adds a test to verify that ACK mode guarantees delivery after connection drops, and adds reconnection tests to TCPSenderTest for both abrupt (RST) and graceful (FIN) connection closures. The review feedback recommends capturing any exceptions thrown in the mock server's background thread using an AtomicReference and asserting that no errors occurred at the end of the test, ensuring that background thread failures are properly propagated to the main test thread.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +333 to +363
void testAckModeGuaranteesDeliveryAfterServerDropsConnections() throws Exception {
Set<Integer> receivedIds = ConcurrentHashMap.newKeySet();
List<Socket> acceptedSockets = new CopyOnWriteArrayList<>();
Value idKey = ValueFactory.newString("id");

AbstractFluentdServer server =
new AbstractFluentdServer(false) {
@Override
protected EventHandler getFluentdEventHandler() {
return new EventHandler() {
@Override
public void onConnect(Socket socket) {
acceptedSockets.add(socket);
}

@Override
public void onReceive(String tag, long timestampMillis, MapValue data) {
Value idValue = data.map().get(idKey);
if (idValue != null) {
int id = idValue.asIntegerValue().asInt();
if (!receivedIds.add(id)) {
throw new AssertionError("Duplicate record received: id=" + id);
}
}
}

@Override
public void onClose(Socket socket) {}
};
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The AssertionError thrown inside the mock server's onReceive method (line 354) runs in a background thread pool (serverExecutorService). In JUnit, unhandled exceptions or errors in background threads do not directly fail the main test thread; instead, they are caught and logged by the server task runner, and the test will only fail later due to a timeout or size mismatch. This makes debugging duplicate record issues extremely difficult.

To fix this, we can capture any exception thrown in the background thread using an AtomicReference<Throwable> and assert that it is null at the end of the test (before the size assertion on line 406).

  void testAckModeGuaranteesDeliveryAfterServerDropsConnections() throws Exception {
    Set<Integer> receivedIds = ConcurrentHashMap.newKeySet();
    List<Socket> acceptedSockets = new CopyOnWriteArrayList<>();
    Value idKey = ValueFactory.newString("id");
    AtomicReference<Throwable> serverError = new AtomicReference<>();

    AbstractFluentdServer server =
        new AbstractFluentdServer(false) {
          @Override
          protected EventHandler getFluentdEventHandler() {
            return new EventHandler() {
              @Override
              public void onConnect(Socket socket) {
                acceptedSockets.add(socket);
              }

              @Override
              public void onReceive(String tag, long timestampMillis, MapValue data) {
                try {
                  Value idValue = data.map().get(idKey);
                  if (idValue != null) {
                    int id = idValue.asIntegerValue().asInt();
                    if (!receivedIds.add(id)) {
                      throw new AssertionError("Duplicate record received: id=" + id);
                    } 
                  }
                } catch (Throwable t) {
                  serverError.compareAndSet(null, t);
                  throw t;
                }
              }

              @Override
              public void onClose(Socket socket) {}
            };
          }
        };

Comment on lines +406 to +410
assertThat(receivedIds)
.as(
"ACK mode must deliver all %d distinct records despite connection drops",
recordsBeforeDrop + recordsAfterDrop)
.hasSize(recordsBeforeDrop + recordsAfterDrop);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Assert that no background thread errors occurred in the mock server before asserting the received IDs size. This ensures that any duplicate record assertion failures are immediately propagated to the main test thread with the actual error message.

      assertThat(serverError.get()).isNull();

      assertThat(receivedIds)
          .as(
              "ACK mode must deliver all %d distinct records despite connection drops",
              recordsBeforeDrop + recordsAfterDrop)
          .hasSize(recordsBeforeDrop + recordsAfterDrop);

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines +350 to +356
Value idValue = data.map().get(idKey);
if (idValue != null) {
int id = idValue.asIntegerValue().asInt();
if (!receivedIds.add(id)) {
throw new AssertionError("Duplicate record received: id=" + id);
}
}
AbstractFluentdServer's FluentdAcceptTask catches all Throwables, so
throwing AssertionError from onReceive was silently swallowed and never
failed the test. Capture it in an AtomicReference and rethrow in the
main thread after flushing.
@komamitsu

Copy link
Copy Markdown
Owner Author

/gemini review

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request adds new tests to verify reconnection behavior and delivery guarantees when connections are dropped, addressing issue #954. Specifically, it introduces a test for ACK mode delivery guarantees after server connection drops in FluencyTestWithMockServer, and tests for TCPSender reconnection after abrupt (RST) and graceful (FIN) socket closures in TCPSenderTest. The review feedback suggests wrapping the background event handler's message processing in a try-catch block to capture unexpected exceptions and prevent silent test failures.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +350 to +359
public void onReceive(String tag, long timestampMillis, MapValue data) {
Value idValue = data.map().get(idKey);
if (idValue != null) {
int id = idValue.asIntegerValue().asInt();
if (!receivedIds.add(id)) {
backgroundError.compareAndSet(
null, new AssertionError("Duplicate record received: id=" + id));
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In the background event handler, any unexpected exceptions (such as NullPointerException or ClassCastException) during onReceive will be caught by the server task and logged, but they won't fail the test directly. This can lead to silent failures or generic size mismatch assertions that are hard to debug. Wrapping the processing in a try-catch block and capturing any Throwable in backgroundError ensures that the actual root cause is propagated and fails the test immediately.

              public void onReceive(String tag, long timestampMillis, MapValue data) {
                try {
                  Value idValue = data.map().get(idKey);
                  if (idValue != null) {
                    int id = idValue.asIntegerValue().asInt();
                    if (!receivedIds.add(id)) {
                      backgroundError.compareAndSet(
                          null, new AssertionError("Duplicate record received: id=" + id));
                    }
                  }
                } catch (Throwable t) {
                  backgroundError.compareAndSet(
                      null, new AssertionError("Unexpected error in background event handler", t));
                }
              }

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines +392 to +398
for (Socket socket : acceptedSockets) {
try {
socket.close();
} catch (IOException e) {
LOG.warn("Failed to close socket", e);
}
}
A single TCPSender to a healthy server always establishes exactly one
connection before the flush completes. hasSize(1) makes the precondition
explicit and catches unexpected reconnections.
@komamitsu

Copy link
Copy Markdown
Owner Author

/gemini review

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces new test cases to reproduce and verify connection recovery behavior under issue #954. Specifically, it adds tests in FluencyTestWithMockServer to ensure ACK mode guarantees delivery after server connection drops, and in TCPSenderTest to verify reconnection behavior after graceful (FIN) and abrupt (RST) server closures. The review feedback suggests improving test robustness by using server.stop(true) to prevent socket leaks and increasing a latch timeout from 5 to 10 seconds to avoid flaky CI runs.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

recordsBeforeDrop + recordsAfterDrop)
.hasSize(recordsBeforeDrop + recordsAfterDrop);
} finally {
server.stop();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Calling server.stop() (which defaults to server.stop(false)) does not immediately close the accepted sockets or the server socket if the server thread is blocked on accept() or read(). This can lead to thread and socket leaks during test execution. Using server.stop(true) ensures all sockets are closed immediately, releasing resources and preventing potential port-binding conflicts in subsequent tests.

Suggested change
server.stop();
server.stop(true);


// Establish the connection by sending initial data
sender.send(ByteBuffer.wrap(data));
assertTrue(firstDataReceivedLatch.await(5, TimeUnit.SECONDS));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using a 5-second timeout for network-based latch synchronization can lead to flaky test failures on slow or heavily loaded CI environments. Increasing this timeout to 10 seconds provides a safer margin without affecting the test duration under normal conditions.

Suggested change
assertTrue(firstDataReceivedLatch.await(5, TimeUnit.SECONDS));
assertTrue(firstDataReceivedLatch.await(10, TimeUnit.SECONDS));

}
}
} finally {
server.stop();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Calling server.stop() (which defaults to server.stop(false)) does not immediately close the accepted sockets or the server socket if the server thread is blocked on accept() or read(). This can lead to thread and socket leaks during test execution. Using server.stop(true) ensures all sockets are closed immediately, releasing resources and preventing potential port-binding conflicts in subsequent tests.

Suggested change
server.stop();
server.stop(true);

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines +353 to +357
int id = idValue.asIntegerValue().asInt();
if (!receivedIds.add(id)) {
backgroundError.compareAndSet(
null, new AssertionError("Duplicate record received: id=" + id));
}
@komamitsu
komamitsu merged commit 0363476 into main Jun 28, 2026
11 checks passed
@komamitsu
komamitsu deleted the test-954-stale-connection branch June 28, 2026 11:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TCPSender doesn't detect stale connections after Fluentd restart

2 participants