This is an automated email from the ASF dual-hosted git repository.

chrisdutz pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x.git


The following commit(s) were added to refs/heads/develop by this push:
     new be5dc80c91 fix: Fixed a potential race condition in the 
connection-cache.
be5dc80c91 is described below

commit be5dc80c9182244c81990ffcd221cc2a12d9f82a
Author: Christofer Dutz <[email protected]>
AuthorDate: Sun Jun 28 13:35:52 2026 +0200

    fix: Fixed a potential race condition in the connection-cache.
---
 .../utils/cache/CachedPlcConnectionManager.java    | 18 +++++++++-
 .../java/utils/cache/ConnectionContainerTest.java  | 40 ++++++++++------------
 2 files changed, 36 insertions(+), 22 deletions(-)

diff --git 
a/plc4j/tools/connection-cache/src/main/java/org/apache/plc4x/java/utils/cache/CachedPlcConnectionManager.java
 
b/plc4j/tools/connection-cache/src/main/java/org/apache/plc4x/java/utils/cache/CachedPlcConnectionManager.java
index 535d93d048..b3f9ba5575 100644
--- 
a/plc4j/tools/connection-cache/src/main/java/org/apache/plc4x/java/utils/cache/CachedPlcConnectionManager.java
+++ 
b/plc4j/tools/connection-cache/src/main/java/org/apache/plc4x/java/utils/cache/CachedPlcConnectionManager.java
@@ -85,6 +85,15 @@ public class CachedPlcConnectionManager implements 
PlcConnectionManager, AutoClo
     private static final long DEFAULT_IDLE_PING_THRESHOLD_MS = 
TimeUnit.SECONDS.toMillis(30);
     private static final long DEFAULT_CLOSE_TIMEOUT_MS = 
TimeUnit.SECONDS.toMillis(5);
 
+    /**
+     * Grace margin added to the caller-facing lease {@code get()} timeout, on 
top of the per-connection
+     * max-wait. It ensures the container's own wait timeout (scheduled at 
exactly max-wait) fires and
+     * marks a queued waiter "done" before this manager gives up — preventing 
a returned connection from
+     * being handed to a waiter whose caller has already timed out. Purely an 
upper safety bound; the
+     * {@code get()} returns as soon as the future completes.
+     */
+    private static final long LEASE_WAIT_GRACE_MS = 
TimeUnit.SECONDS.toMillis(1);
+
     private final PlcConnectionManager connectionManager;
     private final ScheduledExecutorService scheduler;
     private final long maxIdleTimeMs;
@@ -155,7 +164,14 @@ public class CachedPlcConnectionManager implements 
PlcConnectionManager, AutoClo
         // Lease the connection - THIS WILL BLOCK IF ALREADY LEASED
         Future<PlcConnection> leaseFuture = container.lease();
         try {
-            return leaseFuture.get(maxWaitTimeMs, TimeUnit.MILLISECONDS);
+            // Wait a grace margin BEYOND the container's own max-wait 
timeout. The container already
+            // guarantees the lease future completes within maxWaitTimeMs (it 
schedules its own wait
+            // timeout), so letting our get() time out at exactly 
maxWaitTimeMs would race that: if our
+            // get() won, we'd abandon a queue entry that is still "not done", 
and a concurrent return
+            // could then hand the connection to that phantom waiter — wedging 
the connection. The grace
+            // lets the container's timeout win and mark the entry done first; 
get() still returns the
+            // instant the future completes, so a normal wait-timeout is not 
slowed down.
+            return leaseFuture.get(maxWaitTimeMs + LEASE_WAIT_GRACE_MS, 
TimeUnit.MILLISECONDS);
         } catch (ExecutionException | InterruptedException | TimeoutException 
e) {
             throw new PlcConnectionException("Error acquiring lease for 
connection", e);
         }
diff --git 
a/plc4j/tools/connection-cache/src/test/java/org/apache/plc4x/java/utils/cache/ConnectionContainerTest.java
 
b/plc4j/tools/connection-cache/src/test/java/org/apache/plc4x/java/utils/cache/ConnectionContainerTest.java
index 23951adbc1..6752128531 100644
--- 
a/plc4j/tools/connection-cache/src/test/java/org/apache/plc4x/java/utils/cache/ConnectionContainerTest.java
+++ 
b/plc4j/tools/connection-cache/src/test/java/org/apache/plc4x/java/utils/cache/ConnectionContainerTest.java
@@ -26,6 +26,7 @@ import org.junit.jupiter.api.Test;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.time.Duration;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executors;
@@ -34,6 +35,7 @@ import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
+import static org.awaitility.Awaitility.await;
 import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.Mockito.*;
 
@@ -220,20 +222,17 @@ class ConnectionContainerTest {
             100 // Very short idle ping threshold
         );
 
-        // Act - Lease, return, wait for validation threshold
+        // Act - Lease, return, then re-lease once the connection is due for 
validation.
         PlcConnection leased1 = container.lease().get();
         leased1.close();
 
-        Thread.sleep(200); // Wait past idle ping threshold
-
-        // Second lease should trigger validation
-        PlcConnection leased2 = container.lease().get();
-
-        // Assert
-        verify(mockConnection, atLeastOnce()).ping();
-
-        // Cleanup
-        leased2.close();
+        // The container arms validation via a scheduled task 
~idlePingThreshold after the return.
+        // That scheduler tick can lag under load, so poll the lease/validate 
path until the idle
+        // connection is actually pinged instead of assuming a fixed 
wall-clock delay.
+        await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+            container.lease().get().close();
+            verify(mockConnection, atLeastOnce()).ping();
+        });
     }
 
     @Test
@@ -261,22 +260,21 @@ class ConnectionContainerTest {
         // Make first connection's ping fail
         
when(mockConnection.ping()).thenReturn(CompletableFuture.failedFuture(new 
RuntimeException("Ping failed")));
 
-        // Act - Lease, return, wait for validation threshold
+        // Act - Lease, return, then re-lease once the connection is due for 
validation.
         PlcConnection leased1 = container.lease().get();
         leased1.close();
 
-        Thread.sleep(200); // Wait past idle ping threshold
-
-        // Second lease should fail validation and create new connection
-        PlcConnection leased2 = container.lease().get();
+        // Validation is armed by a scheduled task ~idlePingThreshold after 
the return, which can
+        // lag under load. Poll the lease/validate path until the (failing) 
ping has run; once the
+        // first connection's ping fails the container discards it and builds 
a replacement.
+        await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+            container.lease().get().close();
+            verify(mockConnection, atLeastOnce()).ping();
+        });
 
-        // Assert - Should have created connection twice
+        // Assert - the failed ping replaced the first connection exactly once.
         assertEquals(2, callCount[0]);
-        verify(mockConnection, atLeastOnce()).ping();
         verify(mockConnection, times(1)).close(); // Old connection closed
-
-        // Cleanup
-        leased2.close();
     }
 
     @Test

Reply via email to