Skip to content

Commit ff79b72

Browse files
glasstigerclaude
andcommitted
Cover the retriable catch-up send containment
sendCatchUpChunk throws CatchUpSendException on a transient wire failure instead of calling fail(). From inside the catch-up fail() re-enters connectLoop -- desyncing the fsnAtZero/nextWireSeq wire mapping (a later ACK then trims un-acked store-and-forward frames), or overflowing the stack on a flapping connection -- turning a transient outage into a hard failure. Only the oversized-entry (non-retriable) terminal was covered; the retriable path had no test. testTransientCatchUpSendFailureIsRetriableNotTerminal drives the catch-up against a stub whose sendBinary throws, and asserts the failure surfaces as a retriable CatchUpSendException and leaves the producer-facing error latch clear. Reverting the throw to fail() fails it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0ef96af commit ff79b72

1 file changed

Lines changed: 50 additions & 1 deletion

File tree

core/src/test/java/io/questdb/client/test/cutlass/qwp/client/sf/cursor/CursorWebSocketSendLoopCatchUpAlignmentTest.java

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,13 @@
3939
import org.junit.Test;
4040

4141
import java.lang.reflect.Field;
42+
import java.lang.reflect.InvocationTargetException;
4243
import java.lang.reflect.Method;
4344
import java.nio.charset.StandardCharsets;
4445
import java.nio.file.Paths;
4546

4647
import static org.junit.Assert.assertEquals;
48+
import static org.junit.Assert.fail;
4749

4850
/**
4951
* Guards the reconnect/failover symbol-dictionary catch-up ACK alignment in
@@ -173,6 +175,42 @@ public void testSplitCatchUpFramesAcksDoNotAdvanceTrimWatermark() throws Excepti
173175
});
174176
}
175177

178+
@Test
179+
public void testTransientCatchUpSendFailureIsRetriableNotTerminal() throws Exception {
180+
// A transient wire failure WHILE shipping the catch-up (the fresh
181+
// connection drops mid-handshake) must surface as a retriable
182+
// CatchUpSendException for the reconnect loop to handle -- it must NOT
183+
// call fail(). From inside the catch-up fail() re-enters connectLoop
184+
// (corrupting the fsnAtZero/nextWireSeq mapping, or overflowing the stack
185+
// on a flapping connection) or, with no reconnect attempt reachable,
186+
// latches a terminal -- turning a transient outage into a hard failure and
187+
// breaking store-and-forward. Only the oversized-entry (non-retriable)
188+
// terminal was covered; this pins the retriable path.
189+
TestUtils.assertMemoryLeak(() -> {
190+
CatchUpCapturingClient client = new CatchUpCapturingClient(0, true); // sendBinary throws
191+
try (CursorSendEngine engine = newEngine()) {
192+
appendFrames(engine, 2);
193+
engine.acknowledge(0); // ackedFsn=0 => a real unacked frame exists behind the catch-up
194+
CursorWebSocketSendLoop loop = newLoop(engine, client);
195+
try {
196+
seedMirror(loop, "s0", "s1"); // non-empty dict => catch-up fires and hits the failing send
197+
try {
198+
invokeSetWireBaselineWithCatchUp(loop, engine.ackedFsn() + 1L);
199+
fail("a transient catch-up send failure must raise a retriable "
200+
+ "CatchUpSendException, not be swallowed into fail()/a terminal");
201+
} catch (InvocationTargetException e) {
202+
assertEquals("transient catch-up send failure must surface as CatchUpSendException",
203+
"CatchUpSendException", e.getCause().getClass().getSimpleName());
204+
}
205+
// Retriable, not terminal: the producer-facing error latch stays clear.
206+
loop.checkError();
207+
} finally {
208+
loop.close();
209+
}
210+
}
211+
});
212+
}
213+
176214
private static void appendFrames(CursorSendEngine engine, int count) {
177215
long buf = Unsafe.malloc(16, MemoryTag.NATIVE_DEFAULT);
178216
try {
@@ -288,14 +326,22 @@ private static long writeVarint(long addr, long value) {
288326
}
289327

290328
// Stub transport: completes no real I/O. getServerMaxBatchSize drives the
291-
// catch-up split; sendBinary counts the frames the catch-up emitted.
329+
// catch-up split; sendBinary counts the frames the catch-up emitted, or --
330+
// when throwOnSend is set -- raises a transient wire error to model the fresh
331+
// connection dropping mid-catch-up.
292332
private static final class CatchUpCapturingClient extends WebSocketClient {
293333
private final int cap;
334+
private final boolean throwOnSend;
294335
private int framesSent;
295336

296337
CatchUpCapturingClient(int cap) {
338+
this(cap, false);
339+
}
340+
341+
CatchUpCapturingClient(int cap, boolean throwOnSend) {
297342
super(DefaultHttpClientConfiguration.INSTANCE, PlainSocketFactory.INSTANCE);
298343
this.cap = cap;
344+
this.throwOnSend = throwOnSend;
299345
}
300346

301347
@Override
@@ -310,6 +356,9 @@ public int getServerQwpVersion() {
310356

311357
@Override
312358
public void sendBinary(long dataPtr, int length) {
359+
if (throwOnSend) {
360+
throw new RuntimeException("transient wire failure during catch-up");
361+
}
313362
framesSent++;
314363
}
315364

0 commit comments

Comments
 (0)