From d673a08c58b56d22eca641d0601fc9f195021b76 Mon Sep 17 00:00:00 2001 From: Goutam Adwant Date: Sat, 26 Sep 2026 23:11:05 -0700 Subject: [PATCH 1/2] Cancel pool connection timeout timer when acquisition fails See #1706 When a pooled query fails to acquire a connection, the connection timeout timer is not cancelled, so it fails the query promise again when it fires and reports "IllegalStateException: Result is already complete". When the timer fires first, the acquisition failure fails the promise again with the same exception. Handle the acquisition failure like a successful acquisition: cancel the timer, and if it already fired, do not fail the promise again. Signed-off-by: Goutam Adwant --- .../impl/pool/SqlConnectionPool.java | 4 + .../impl/pool/SqlConnectionPoolTest.java | 87 +++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/pool/SqlConnectionPool.java b/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/pool/SqlConnectionPool.java index 7eaf11b7a..58771a043 100644 --- a/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/pool/SqlConnectionPool.java +++ b/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/pool/SqlConnectionPool.java @@ -248,6 +248,10 @@ public Future execute(ContextInternal context, CommandBase cmd, long t }); }, t -> { dequeueAndReject(queueMetric); + if (timerId != -1 && !vertx.cancelTimer(timerId)) { + // The timer already failed the result + return Future.failedFuture(POOL_QUERY_TIMEOUT_EXCEPTION); + } return Future.failedFuture(t); }).onComplete(ar -> { if (ar.succeeded()) { diff --git a/vertx-sql-client/src/test/java/io/vertx/sqlclient/impl/pool/SqlConnectionPoolTest.java b/vertx-sql-client/src/test/java/io/vertx/sqlclient/impl/pool/SqlConnectionPoolTest.java index adf59f14a..1b9eec1b0 100644 --- a/vertx-sql-client/src/test/java/io/vertx/sqlclient/impl/pool/SqlConnectionPoolTest.java +++ b/vertx-sql-client/src/test/java/io/vertx/sqlclient/impl/pool/SqlConnectionPoolTest.java @@ -30,6 +30,9 @@ import io.vertx.sqlclient.spi.Driver; import org.junit.Test; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -214,4 +217,88 @@ public void testAfterAcquireFailureReleasesConnection() throws Exception { vertx.close(); } } + + @Test + public void testConnectFailureCancelsConnectionTimeout() throws Exception { + RuntimeException connectError = new RuntimeException("connect failed"); + + VertxInternal vertx = (VertxInternal) Vertx.vertx(); + List uncaught = new CopyOnWriteArrayList<>(); + vertx.exceptionHandler(uncaught::add); + + try { + SqlConnectionPool pool = createPool(vertx, ctx -> Future.failedFuture(connectError)); + + ContextInternal ctx = vertx.getOrCreateContext(); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference failure = new AtomicReference<>(); + ctx.runOnContext(v -> { + pool.execute(ctx, new CommandBase() {}, 1000).onComplete(ar -> { + failure.set(ar.cause()); + latch.countDown(); + }); + }); + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertEquals(connectError, failure.get()); + + // Let the connection timeout elapse + Thread.sleep(1500); + assertEquals(Collections.emptyList(), uncaught); + + pool.close(); + } finally { + vertx.close(); + } + } + + @Test + public void testConnectFailureAfterConnectionTimeout() throws Exception { + RuntimeException connectError = new RuntimeException("connect failed"); + Promise connectPromise = Promise.promise(); + + VertxInternal vertx = (VertxInternal) Vertx.vertx(); + List uncaught = new CopyOnWriteArrayList<>(); + vertx.exceptionHandler(uncaught::add); + + try { + SqlConnectionPool pool = createPool(vertx, ctx -> connectPromise.future()); + + ContextInternal ctx = vertx.getOrCreateContext(); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference failure = new AtomicReference<>(); + ctx.runOnContext(v -> { + pool.execute(ctx, new CommandBase() {}, 100).onComplete(ar -> { + failure.set(ar.cause()); + latch.countDown(); + }); + }); + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertEquals("Timeout waiting for connection", failure.get().getMessage()); + + ctx.runOnContext(v -> connectPromise.fail(connectError)); + Thread.sleep(300); + assertEquals(Collections.emptyList(), uncaught); + + pool.close(); + } finally { + vertx.close(); + } + } + + private static SqlConnectionPool createPool(VertxInternal vertx, java.util.function.Function> connectionProvider) { + return new SqlConnectionPool( + connectionProvider, + () -> null, + null, + null, + null, + vertx, + 0, + 0, + 1, + false, + -1, + 0 + ); + } } From 313ed295c8789ad7cb672622dfd7de66351b9962 Mon Sep 17 00:00:00 2001 From: Goutam Adwant Date: Mon, 28 Sep 2026 00:03:18 -0700 Subject: [PATCH 2/2] Use if/else in the acquisition failure branch Signed-off-by: Goutam Adwant --- .../java/io/vertx/sqlclient/impl/pool/SqlConnectionPool.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/pool/SqlConnectionPool.java b/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/pool/SqlConnectionPool.java index 58771a043..bfbe5cc81 100644 --- a/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/pool/SqlConnectionPool.java +++ b/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/pool/SqlConnectionPool.java @@ -251,8 +251,9 @@ public Future execute(ContextInternal context, CommandBase cmd, long t if (timerId != -1 && !vertx.cancelTimer(timerId)) { // The timer already failed the result return Future.failedFuture(POOL_QUERY_TIMEOUT_EXCEPTION); + } else { + return Future.failedFuture(t); } - return Future.failedFuture(t); }).onComplete(ar -> { if (ar.succeeded()) { res.complete(ar.result());