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 955486e0fb fix: Make the lease-time decision deterministic (purely
idle-time based, plus the force flag)
955486e0fb is described below
commit 955486e0fb42c858fc14aa9832e6533233fff300
Author: Christofer Dutz <[email protected]>
AuthorDate: Tue Jul 14 13:40:19 2026 +0200
fix: Make the lease-time decision deterministic (purely idle-time based,
plus the force flag)
---
.../java/utils/cache/ConnectionContainer.java | 71 +++++++---------------
.../cache/CachedPlcConnectionManagerTest.java | 11 ++--
2 files changed, 26 insertions(+), 56 deletions(-)
diff --git
a/plc4j/tools/connection-cache/src/main/java/org/apache/plc4x/java/utils/cache/ConnectionContainer.java
b/plc4j/tools/connection-cache/src/main/java/org/apache/plc4x/java/utils/cache/ConnectionContainer.java
index aff47dbb49..ff8c600ef9 100644
---
a/plc4j/tools/connection-cache/src/main/java/org/apache/plc4x/java/utils/cache/ConnectionContainer.java
+++
b/plc4j/tools/connection-cache/src/main/java/org/apache/plc4x/java/utils/cache/ConnectionContainer.java
@@ -74,7 +74,11 @@ class ConnectionContainer {
private final ConnectionStateTracker stateTracker;
private PlcConnection connection;
- private boolean requiresValidation;
+ // When true, the next lease MUST validate (ping) the connection
regardless of how recently it
+ // was used. Set when a lease is force-returned (e.g. by a max-lease
timeout) because the
+ // connection may be dead. In all other cases validation is decided purely
from idle time at
+ // lease() time, so it is deterministic and needs no scheduled task.
+ private boolean forceValidationOnNextLease;
// Tracks the last time a lease was successfully returned (i.e., the
connection was used).
// Used to determine if a ping validation is actually needed: if a
successful operation
// completed recently, the connection is known to be alive and pinging is
unnecessary.
@@ -91,7 +95,6 @@ class ConnectionContainer {
// Scheduled timeout tasks (must be canceled to prevent leaks)
private volatile ScheduledFuture<?> idleTimeoutTask;
- private volatile ScheduledFuture<?> requireValidationTimeoutTask;
private volatile ScheduledFuture<?> maxLeaseTimeoutTask;
/**
@@ -120,7 +123,7 @@ class ConnectionContainer {
this.idlePingThresholdMs = idlePingThresholdMs;
this.closeTimeoutMs = closeTimeoutMs;
this.queue = new LinkedList<>();
- this.requiresValidation = false;
+ this.forceValidationOnNextLease = false;
this.lastSuccessfulReturnTimeMs = 0;
this.currentLeaseId = 0;
this.lock = new ReentrantLock(true); // Fair lock to prevent thread
starvation
@@ -164,9 +167,14 @@ class ConnectionContainer {
// If validation fails, we need to close the existing
connection and get a new one.
// Skip validation if a successful operation completed
recently — the connection
// is known to be alive and pinging would just add unnecessary
blocking latency.
- else if (requiresValidation
- && (lastSuccessfulReturnTimeMs == 0
- || (System.currentTimeMillis() -
lastSuccessfulReturnTimeMs) >= idlePingThresholdMs)) {
+ //
+ // This decision is computed entirely here, at lease() time,
from the idle duration
+ // (plus the force flag). It does NOT depend on any scheduled
task having fired, so
+ // it is fully deterministic. A zero idlePingThreshold means
"always validate", which
+ // the (now - lastReturn) >= 0 comparison already yields.
+ else if (forceValidationOnNextLease
+ || lastSuccessfulReturnTimeMs == 0
+ || (System.currentTimeMillis() -
lastSuccessfulReturnTimeMs) >= idlePingThresholdMs) {
if (!validate()) {
LOGGER.debug("Connection failed validation, closing
and getting new connection");
closeBounded(connection);
@@ -266,13 +274,10 @@ class ConnectionContainer {
if(queue.isEmpty()) {
leasedConnection = null;
scheduleIdleTimeout();
- // Force validation if requested (e.g., from max lease
timeout) or if idlePingThresholdMs is 0
- if (forceValidation || idlePingThresholdMs == 0) {
- requiresValidation = true;
- } else {
- requiresValidation = false;
- scheduleRequireValidationTimeout();
- }
+ // A force-return means the connection may be dead: validate
on the next lease no
+ // matter how recently it was used. Otherwise leave it to the
idle-time check in
+ // lease() — no scheduled task needed.
+ forceValidationOnNextLease = forceValidation;
LOGGER.trace("Connection idle {}", connectionString);
return;
}
@@ -317,12 +322,9 @@ class ConnectionContainer {
// All queued entries had already timed out — go idle
leasedConnection = null;
scheduleIdleTimeout();
- if (forceValidation || idlePingThresholdMs == 0) {
- requiresValidation = true;
- } else {
- requiresValidation = false;
- scheduleRequireValidationTimeout();
- }
+ // See the queue-empty branch above: force validation only on a
force-return; otherwise
+ // the idle-time check in lease() decides deterministically.
+ forceValidationOnNextLease = forceValidation;
LOGGER.trace("Connection idle after all waiters timed out {}",
connectionString);
} finally {
lock.unlock();
@@ -469,27 +471,6 @@ class ConnectionContainer {
}
}
- private void scheduleRequireValidationTimeout() {
- // Cancel any existing max lease timeout
- cancelRequireValidationTimeout();
-
- try {
- requireValidationTimeoutTask = scheduler.schedule(() -> {
- // Mutate requiresValidation under the lock so the next
lease() reliably observes it.
- lock.lock();
- try {
- requiresValidation = true;
- } finally {
- lock.unlock();
- }
- }, idlePingThresholdMs, TimeUnit.MILLISECONDS);
- LOGGER.trace("Scheduled require validation timeout for {} in
{}ms", connectionString, idlePingThresholdMs);
- } catch (RejectedExecutionException e) {
- // Scheduler was already shut down (manager shutting down)
- LOGGER.debug("Cannot schedule require validation timeout,
scheduler shut down: {}", connectionString);
- }
- }
-
/**
* Cancel the idle timeout task if running.
*/
@@ -512,16 +493,6 @@ class ConnectionContainer {
}
}
- /**
- * Cancel the require-validation timeout task if running.
- */
- private void cancelRequireValidationTimeout() {
- if (requireValidationTimeoutTask != null) {
- requireValidationTimeoutTask.cancel(false);
- requireValidationTimeoutTask = null;
- LOGGER.trace("Cancelled require validation timeout for {}",
connectionString);
- }
- }
/**
* Close this connection container and the underlying connection.
diff --git
a/plc4j/tools/connection-cache/src/test/java/org/apache/plc4x/java/utils/cache/CachedPlcConnectionManagerTest.java
b/plc4j/tools/connection-cache/src/test/java/org/apache/plc4x/java/utils/cache/CachedPlcConnectionManagerTest.java
index 25827d83bc..9efb7e31a9 100644
---
a/plc4j/tools/connection-cache/src/test/java/org/apache/plc4x/java/utils/cache/CachedPlcConnectionManagerTest.java
+++
b/plc4j/tools/connection-cache/src/test/java/org/apache/plc4x/java/utils/cache/CachedPlcConnectionManagerTest.java
@@ -593,12 +593,11 @@ class CachedPlcConnectionManagerTest {
PlcConnection connection1 =
manager.getConnection("test:tcp://localhost");
connection1.close();
- // Wait for connection to become idle beyond threshold.
- // Use a generous margin over the 500ms threshold: the connection is
only marked
- // "requires validation" by a task on the (single-thread) scheduler
that fires at the
- // threshold, and on a loaded CI runner that task can be delayed. A
small margin
- // (e.g. 100ms) makes this test flaky; 1500ms gives the scheduler
ample room.
- Thread.sleep(1500);
+ // Wait for the connection to become idle beyond the 500ms threshold.
The ping decision is
+ // made deterministically from the idle duration at lease() time (no
scheduled task
+ // involved), so any wall-clock margin over the threshold suffices;
800ms is comfortably
+ // clear of scheduling jitter on a loaded CI runner.
+ Thread.sleep(800);
// Reset ping mock to track subsequent calls
reset(mockConnection);