From 734cefacb9001abc9816d60c4cb78dfd3bc9a10e Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Mon, 20 Jul 2026 18:11:31 +0300 Subject: [PATCH] Issue #755: wait for the LDAP listener after in-process server (re)start BindOperationTestCase.testSubtreeModifyUpdatesAuthInfo failed once on CI with "Connection refused" on the first RemoteConnection right after TestCaseUtils.restartServer(): the wait-for-listen handshake in LDAPConnectionHandler.start() used a bare waitListen.wait() with no condition predicate, so an early wakeup let startup return before the listen channel was bound. - LDAPConnectionHandler: guard the wait with a listenAttempted predicate set at both notify sites, and restore the interrupt flag on interrupt - TestCaseUtils: poll the LDAP port until it actually accepts TCP connections after server.start() in startServer()/restartServer(), covering all tests that connect right after a restart --- .../protocols/ldap/LDAPConnectionHandler.java | 17 ++++++++- .../java/org/opends/server/TestCaseUtils.java | 38 ++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/ldap/LDAPConnectionHandler.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/ldap/LDAPConnectionHandler.java index c584f27c0c..37a3256532 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/ldap/LDAPConnectionHandler.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/protocols/ldap/LDAPConnectionHandler.java @@ -13,6 +13,7 @@ * * Copyright 2006-2010 Sun Microsystems, Inc. * Portions Copyright 2011-2016 ForgeRock AS. + * Portions Copyrighted 2026 3A Systems, LLC. */ package org.opends.server.protocols.ldap; @@ -183,6 +184,14 @@ public void run() */ private final Object waitListen = new Object(); + /** + * Condition predicate for {@link #waitListen}: set once the selector thread + * has attempted to open the listen channels (successfully or not). Guarded + * by the {@link #waitListen} monitor; protects the start method against + * spurious wakeups. + */ + private boolean listenAttempted; + /** The friendly name of this connection handler. */ private String friendlyName; @@ -814,12 +823,16 @@ public void start() try { - waitListen.wait(); + while (!listenAttempted) + { + waitListen.wait(); + } } catch (InterruptedException e) { // If something interrupted the start its probably better // to return ASAP. + Thread.currentThread().interrupt(); } } } @@ -859,6 +872,7 @@ public void run() synchronized (waitListen) { starting = false; + listenAttempted = true; waitListen.notify(); } } @@ -882,6 +896,7 @@ public void run() // should be notified and resume its work in any cases. synchronized (waitListen) { + listenAttempted = true; waitListen.notify(); } diff --git a/opendj-server-legacy/src/test/java/org/opends/server/TestCaseUtils.java b/opendj-server-legacy/src/test/java/org/opends/server/TestCaseUtils.java index c7a835bf32..9f220fa7e8 100644 --- a/opendj-server-legacy/src/test/java/org/opends/server/TestCaseUtils.java +++ b/opendj-server-legacy/src/test/java/org/opends/server/TestCaseUtils.java @@ -14,7 +14,7 @@ * Copyright 2006-2010 Sun Microsystems, Inc. * Portions Copyright 2011-2016 ForgeRock AS. * Portions Copyright 2013 Manuel Gaupp - * Portions Copyright 2018-2025 3A Systems, LLC + * Portions Copyright 2018-2026 3A Systems, LLC */ package org.opends.server; @@ -68,6 +68,7 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; +import java.util.concurrent.TimeUnit; import java.util.logging.ConsoleHandler; import java.util.logging.Handler; import java.util.logging.LogManager; @@ -119,6 +120,7 @@ import org.opends.server.util.BuildVersion; import org.opends.server.util.DynamicConstants; import org.opends.server.util.LDIFReader; +import org.opends.server.util.TestTimer; import com.forgerock.opendj.util.OperatingSystem; @@ -327,6 +329,7 @@ public static void startServer() throws Exception setupLoggers(); writeBuildInfoFile(); server.start(); + waitForLdapListener(); assertTrue(InvocationCounterPlugin.startupCalled()); // Save config.ldif for when we restart the server backupServerConfigLdif(); @@ -607,6 +610,7 @@ public static synchronized void restartServer() restoreServerConfigLdif(); server.start(); + waitForLdapListener(); clearJEBackends(); initializeTestBackend(true); @@ -625,6 +629,38 @@ public static synchronized void restartServer() } } + /** + * Waits until the LDAP connection handler actually accepts TCP connections. + * {@code LDAPConnectionHandler.start()} may return before the listen + * channel is bound (e.g. after an early wakeup of its wait-for-listen + * handshake), so the very first connection made right after a server + * (re)start could otherwise be refused. + */ + private static void waitForLdapListener() throws Exception + { + new TestTimer.Builder() + .maxSleep(10, TimeUnit.SECONDS) + .sleepTimes(100, TimeUnit.MILLISECONDS) + .toTimer() + .repeatUntilSuccess(new TestTimer.CallableVoid() + { + @Override + public void call() throws Exception + { + try (Socket s = new Socket()) + { + s.connect(new InetSocketAddress("127.0.0.1", getServerLdapPort()), 500); + } + catch (IOException e) + { + // repeatUntilSuccess() only retries on assertion errors. + throw new AssertionError("LDAP port " + getServerLdapPort() + + " is not accepting connections yet", e); + } + } + }); + } + /** * Returns the embedded server used for tests. *