|
27 | 27 | import io.questdb.client.cutlass.line.LineSenderException; |
28 | 28 | import io.questdb.client.cutlass.line.array.DoubleArray; |
29 | 29 | import io.questdb.client.cutlass.line.array.LongArray; |
| 30 | +import io.questdb.client.cutlass.qwp.client.MicrobatchBuffer; |
30 | 31 | import io.questdb.client.cutlass.qwp.client.QwpWebSocketSender; |
| 32 | +import io.questdb.client.cutlass.qwp.client.sf.cursor.CursorSendEngine; |
31 | 33 | import io.questdb.client.cutlass.qwp.protocol.QwpTableBuffer; |
32 | 34 | import io.questdb.client.std.Decimal128; |
33 | 35 | import io.questdb.client.std.Decimal256; |
34 | 36 | import io.questdb.client.std.Decimal64; |
35 | 37 | import io.questdb.client.std.bytes.DirectByteSlice; |
| 38 | +import io.questdb.client.test.cutlass.qwp.websocket.TestWebSocketServer; |
36 | 39 | import org.junit.Assert; |
37 | 40 | import org.junit.Test; |
38 | 41 |
|
| 42 | +import java.lang.reflect.Field; |
39 | 43 | import java.time.Instant; |
40 | 44 | import java.time.temporal.ChronoUnit; |
| 45 | +import java.util.concurrent.TimeUnit; |
41 | 46 |
|
42 | 47 | import static io.questdb.client.test.tools.TestUtils.assertMemoryLeak; |
43 | 48 |
|
@@ -326,6 +331,54 @@ public void testDoubleColumnAfterCloseThrows() throws Exception { |
326 | 331 | }); |
327 | 332 | } |
328 | 333 |
|
| 334 | + @Test |
| 335 | + public void testFlushAppendFailureDoesNotLeaveMicrobatchBufferInUse() throws Exception { |
| 336 | + assertMemoryLeak(() -> { |
| 337 | + int port = TestPorts.findUnusedPort(); |
| 338 | + try (TestWebSocketServer server = new TestWebSocketServer(port, new TestWebSocketServer.WebSocketServerHandler() { |
| 339 | + })) { |
| 340 | + server.start(); |
| 341 | + Assert.assertTrue(server.awaitStart(5, TimeUnit.SECONDS)); |
| 342 | + |
| 343 | + // Memory-only engine with a 33-byte budget and a 1 ns append |
| 344 | + // deadline guarantees every appendBlocking() call trips the |
| 345 | + // backpressure deadline and throws. |
| 346 | + CursorSendEngine engine = new CursorSendEngine(null, 33, 33, 1L); |
| 347 | + QwpWebSocketSender sender = QwpWebSocketSender.connect( |
| 348 | + "localhost", port, null, Integer.MAX_VALUE, 0, 0L, null, |
| 349 | + QwpWebSocketSender.DEFAULT_MAX_SCHEMAS_PER_CONNECTION, false, engine, 0L); |
| 350 | + try { |
| 351 | + sender.table("t").longColumn("v", 1L).atNow(); |
| 352 | + |
| 353 | + try { |
| 354 | + sender.flushAndGetSequence(); |
| 355 | + Assert.fail("Expected LineSenderException"); |
| 356 | + } catch (LineSenderException e) { |
| 357 | + Assert.assertTrue(e.getMessage().contains("cursor SF append failed")); |
| 358 | + } |
| 359 | + |
| 360 | + MicrobatchBuffer buffer0 = getMicrobatchBuffer(sender, "buffer0"); |
| 361 | + MicrobatchBuffer buffer1 = getMicrobatchBuffer(sender, "buffer1"); |
| 362 | + Assert.assertFalse( |
| 363 | + "failed append must not leave any buffer in use [buffer0=" |
| 364 | + + MicrobatchBuffer.stateName(buffer0.getState()) |
| 365 | + + ", buffer1=" + MicrobatchBuffer.stateName(buffer1.getState()) + "]", |
| 366 | + buffer0.isInUse() || buffer1.isInUse()); |
| 367 | + } finally { |
| 368 | + // close() drains pending rows, which appendBlocking still |
| 369 | + // rejects because the engine is permanently wedged in this |
| 370 | + // test. The bug under test is about microbatch buffer |
| 371 | + // state, not about close() being lenient toward residual |
| 372 | + // unflushed rows — swallow the predictable rethrow here. |
| 373 | + try { |
| 374 | + sender.close(); |
| 375 | + } catch (LineSenderException ignored) { |
| 376 | + } |
| 377 | + } |
| 378 | + } |
| 379 | + }); |
| 380 | + } |
| 381 | + |
329 | 382 | @Test |
330 | 383 | public void testGeoHashColumnLongAfterCloseThrows() throws Exception { |
331 | 384 | assertMemoryLeak(() -> { |
@@ -705,6 +758,12 @@ private static void assertClosed(Runnable r) { |
705 | 758 | } |
706 | 759 | } |
707 | 760 |
|
| 761 | + private static MicrobatchBuffer getMicrobatchBuffer(QwpWebSocketSender sender, String fieldName) throws Exception { |
| 762 | + Field field = QwpWebSocketSender.class.getDeclaredField(fieldName); |
| 763 | + field.setAccessible(true); |
| 764 | + return (MicrobatchBuffer) field.get(sender); |
| 765 | + } |
| 766 | + |
708 | 767 | /** |
709 | 768 | * Creates a sender without connecting. |
710 | 769 | * For unit tests that don't need actual connectivity. |
|
0 commit comments