Skip to content

Conversation

@jaikiran
Copy link
Member

@jaikiran jaikiran commented May 26, 2025

Can I please get a review of this change which proposes to address the issue noted in https://bugs.openjdk.org/browse/JDK-8357708?

As noted in the issue, the current code in com.sun.jndi.ldap.Connection.readReply() is susceptible to throwing a ServiceUnavailableException even when the LDAP replies have already been received and queued for processing. The JBS issue has additional details about how that can happen.

The commit in this PR simplifies the code in com.sun.jndi.ldap.LdapRequest to make sure it always gives out the replies that have been queued when the LdapRequest.getReplyBer() gets invoked. One of those queued values could be markers for a cancelled or closed request. In that case, the getReplyBer(), like previously, continues to throw the right exception. With this change, the call to replies.take() or replies.poll() (with an infinite timeout) is no longer expected to hang forever, if the Connection is closed (or the request cancelled). This then allows us to remove the connection closure (sock == null) check in Connection.readReply().

A new jtreg test has been introduced to reproduce this issue and verify the fix. The test reproduces this issue consistently when the source fix isn't present. With the fix present, even after several thousand runs of this test, the issue no longer reproduces.

tier1, tier2 and tier3 tests continue to pass with this change. I've marked the fix version of this issue for 26 and I don't plan to push this for 25.


Progress

  • Change must be properly reviewed (1 review required, with at least 1 Reviewer)
  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue

Issue

  • JDK-8357708: com.sun.jndi.ldap.Connection ignores queued LDAP replies if Connection is subsequently closed (Bug - P4)

Reviewers

Contributors

  • Aleksei Efimov <aefimov@openjdk.org>

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/25449/head:pull/25449
$ git checkout pull/25449

Update a local copy of the PR:
$ git checkout pull/25449
$ git pull https://git.openjdk.org/jdk.git pull/25449/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 25449

View PR using the GUI difftool:
$ git pr show -t 25449

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/25449.diff

Using Webrev

Link to Webrev Comment

@bridgekeeper
Copy link

bridgekeeper bot commented May 26, 2025

👋 Welcome back jpai! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk
Copy link

openjdk bot commented May 26, 2025

@jaikiran This change now passes all automated pre-integration checks.

ℹ️ This project also has non-automated pre-integration requirements. Please see the file CONTRIBUTING.md for details.

After integration, the commit message for the final commit will be:

8357708: com.sun.jndi.ldap.Connection ignores queued LDAP replies if Connection is subsequently closed

Co-authored-by: Aleksei Efimov <aefimov@openjdk.org>
Reviewed-by: aefimov, dfuchs

You can use pull request commands such as /summary, /contributor and /issue to adjust it as needed.

At the time when this comment was updated there had been 10 new commits pushed to the master branch:

As there are no conflicts, your changes will automatically be rebased on top of these commits when integrating. If you prefer to avoid this automatic rebasing, please check the documentation for the /integrate command for further details.

➡️ To integrate this PR with the above commit message to the master branch, type /integrate in a new comment.

@openjdk openjdk bot added the rfr Pull request is ready for review label May 26, 2025
@openjdk
Copy link

openjdk bot commented May 26, 2025

@jaikiran The following label will be automatically applied to this pull request:

  • core-libs

When this pull request is ready to be reviewed, an "RFR" email will be sent to the corresponding mailing list. If you would like to change these labels, use the /label pull request command.

@openjdk openjdk bot added the core-libs core-libs-dev@openjdk.org label May 26, 2025
@mlbridge
Copy link

mlbridge bot commented May 26, 2025

Webrevs

lock.unlock();
// Add a new reply to the queue of unprocessed replies.
try {
replies.put(ber);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So theoretically this could get into the queue after one of the two markers has already been put in the queue, since we no longer use the lock. The question is: is this a problem? I'd be tempted to say no - except that getReplyBer() will take the markers out of the queue.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello Daniel,

I'd be tempted to say no - except that getReplyBer() will take the markers out of the queue.

That's correct - the change intentionally removes the lock and also lets the close/cancel markers land into the queue so that if the getReplyBer() is already blocked in a take() or poll() call, then it will be unblocked and if it isn't yet blocked on those calls then a subsequent call will find these markers (which are distinct for close and cancel).

Adding these (distinct) markers will also allow for an ordered identifiable content in the queue - some replies followed by close/cancel marker or a close/cancel marker followed by replies.

Additionally, the addRequest(), close() and cancel() methods of this LdapRequet get called by a single thread managed by the Connection class, so there isn't expected to be concurrent calls across these methods. So, I think, removing the lock and letting the (distinct) markers end up in the queue makes this code in the LdapRequest simpler.

The getReplyBer() the implementation polls the ordered queue, so it find all replies that have arrived before the cancel/close marker is encountered. Once it encounters those markers it can then throw the relevant exception as per its current specified behaviour.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, the addRequest(), close() and cancel() methods of this LdapRequet get called by a single thread managed by the Connection class, so there isn't expected to be concurrent calls across these methods.

I am not seeing that - close() is called by Connection.cleanup() which seems to be callable asynchronously.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot to reply here, but yes Daniel is right that the close() method can be called concurrently. With the current proposed change, it should be OK to have close() be invoked concurrently. The close()/cancel() invocation will place the respective marker in the queue and will also mark the close/cancel flag. We intentionally place the close/cancel markers in the queue so that the getReplyBer() will find that marker in the right order, when it is next invoked or if it is currently blocked waiting for the next message.

Given the current expected semantics of getReplyBer(), we skip adding any new replies after the close/cancel markers have been placed in the queue. But that's on a best effort basis. Due to a race, if we do add replies to the queue after the close/cancel has been invoked, then it's OK because the getReplyBer() upon noticing the close/cancel markers first, will not process the subsequent replies in the queue. Thus it retains the current behaviour of not processing any replies after close/cancel has been noticed.

@bridgekeeper
Copy link

bridgekeeper bot commented Jul 4, 2025

@jaikiran This pull request has been inactive for more than 4 weeks and will be automatically closed if another 4 weeks passes without any activity. To avoid this, simply issue a /touch or /keepalive command to the pull request. Feel free to ask for assistance if you need help with progressing this pull request towards integration!

@AlekseiEfimov
Copy link
Member

/keepalive

@openjdk
Copy link

openjdk bot commented Jul 16, 2025

@AlekseiEfimov The pull request is being re-evaluated and the inactivity timeout has been reset.

: replies.take();
// poll from 'replies' blocking queue ended-up with timeout
if (result == null) {
throw new IOException(String.format(TIMEOUT_MSG_FMT, millis));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw new IOException(String.format(TIMEOUT_MSG_FMT, millis));
throw new IOException("LDAP response read timed out, timeout used: %d ms.".formatted(millis));

TIMEOUT_MSG_FMT is only used once here, and defining a constant would make the code less readable.

// Unexpected EOF can be caused by connection closure or cancellation
if (result == EOF) {
if (result == CANCELLED_MARKER) {
throw new CommunicationException("Request: " + msgId +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current LdapRequest class should use a unified style to construct exception information, either using string concatenation or String.format. A class should not mix the two styles.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello Shaojin, these are pre-existing messages. I have however updated the PR to use a uniform style.

final int numTasks = 10;
try (final ExecutorService executor = Executors.newCachedThreadPool()) {
for (int i = 1; i <= numTasks; i++) {
final String taskName = "task-" + i;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
final String taskName = "task-" + i;
String taskName = "task-" + i;

Variables used only in the current block do not need to be final

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This a more a personal preference and since this is a new test file, I let it stay in this form. If there's a strong preference to remove it, I'll do so.

ber.parseSeq(null);
ber.parseInt();
completed = (ber.peekByte() == LdapClient.LDAP_REP_RESULT);
isLdapResResult = (ber.peekByte() == LdapClient.LDAP_REP_RESULT);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other boolean type variables in the LdapRequest class do not have the is prefix. The local variable isLdapResResult here should also use the same style. We should not use two styles in one class.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling it ldapResResult instead of isLdapResResult isn't appropriate here, so I've let this stay in its current form.

@bridgekeeper
Copy link

bridgekeeper bot commented Sep 3, 2025

@jaikiran This pull request has been inactive for more than 4 weeks and will be automatically closed if another 4 weeks passes without any activity. To avoid this, simply issue a /touch or /keepalive command to the pull request. Feel free to ask for assistance if you need help with progressing this pull request towards integration!

* @modules java.naming/com.sun.jndi.ldap
* @library /test/lib
* @build jdk.test.lib.net.URIBuilder
* @run junit LdapClientConnTest
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this test creates a daemon thread and does not try to join the thread at the end it might be more prudent to run it in /othervm mode?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello Daniel, good catch. It took me a while to understand this - leaving around arbitrary test specific threads in an agent VM isn't wise. You are right, making it othervm would be better. I've updated the PR to do so.

Copy link
Member

@dfuch dfuch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@openjdk openjdk bot added the ready Pull request is ready to be integrated label Sep 12, 2025
@AlekseiEfimov
Copy link
Member

Hello Jaikiran,

The fix and its logic looks correct to me.

I have a couple of suggestions regarding the new test:

  1. The test can be modified to remove the dependency on the internal LdapCtx class. Something like:
@@ -43,11 +43,14 @@
 import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicInteger;
 
-import com.sun.jndi.ldap.LdapCtx;
 import jdk.test.lib.net.URIBuilder;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
+
+import javax.naming.Context;
+import javax.naming.InitialContext;
+
 import static java.nio.charset.StandardCharsets.US_ASCII;
 import static org.junit.jupiter.api.Assertions.fail;
 
@@ -56,7 +59,6 @@
  * @bug 8357708
  * @summary verify that com.sun.jndi.ldap.Connection does not ignore the LDAP replies
  *          that were received before the Connection was closed.
- * @modules java.naming/com.sun.jndi.ldap
  * @library /test/lib
  * @build jdk.test.lib.net.URIBuilder
  * @run junit/othervm LdapClientConnTest
@@ -381,17 +383,22 @@
 
         @Override
         public Void call() throws Exception {
-            LdapCtx ldapCtx = null;
+            Context ldapCtx = null;
             try {
                 final InetSocketAddress serverAddr = server.getAddress();
                 final Hashtable<String, String> envProps = new Hashtable<>();
                 // explicitly set LDAP version to 3 to prevent LDAP BIND requests
                 // during LdapCtx instantiation
                 envProps.put("java.naming.ldap.version", "3");
-                ldapCtx = new LdapCtx("",
-                        serverAddr.getAddress().getHostAddress(),
-                        serverAddr.getPort(),
-                        envProps, false);
+                envProps.put(Context.INITIAL_CONTEXT_FACTORY,
+                        "com.sun.jndi.ldap.LdapCtxFactory");
+                String providerUrl = URIBuilder.newBuilder()
+                        .scheme("ldap")
+                        .host(serverAddr.getAddress())
+                        .port(serverAddr.getPort())
+                        .build().toString();
+                envProps.put(Context.PROVIDER_URL, providerUrl);
+                ldapCtx = new InitialContext(envProps);
                 final String name = SEARCH_REQ_DN_PREFIX + taskName + SEARCH_REQ_DN_SUFFIX;
                 // trigger the LDAP SEARCH requests through the lookup call. we are not
                 // interested in the returned value and are merely interested in a normal
  1. Maybe move the new test to the test/jdk/com/sun/jndi/ldap folder where other tests for internal LDAP classes live.

  2. I like the dynamic approach on generating and parsing the LDAP messages. For future tests in JNDI/LDAP area such approach could be beneficial. Therefore, a suggestion - move private static final byte BER_TYPE_* constants to the test/jdk/com/sun/jndi/ldap/lib/LDAPTestUtils.java.

@openjdk openjdk bot removed the ready Pull request is ready to be integrated label Sep 12, 2025
@jaikiran
Copy link
Member Author

/contributor @AlekseiEfimov

@openjdk
Copy link

openjdk bot commented Sep 12, 2025

@jaikiran Syntax: /contributor (add|remove) [@user | openjdk-user | Full Name <email@address>]. For example:

  • /contributor add @openjdk-bot
  • /contributor add duke
  • /contributor add J. Duke <duke@openjdk.org>

User names can only be used for users in the census associated with this repository. For other contributors you need to supply the full name and email address.

@jaikiran
Copy link
Member Author

Hello Aleksei, I have updated the PR to implement your suggestion 1 and 2. As for the other suggestion of moving these constants to the LDAPTestUtils test library class, I think that's a good idea too. However, I gave that a try and that isn't straightforward. Once I move them to that class, jtreg then requires me to add a @build and a @library instruction to bring in the test.LDAPServer class which the LDAPTestUtils uses (but not this new test). That wouldn't be too bad and I did add those, but then LDAPTestUtils has an import of an internal class:

import com.sun.jndi.ldap.LdapURL

so that then forces me to reintroduce the:

@modules java.naming/com.sun.jndi.ldap

in this new test, which I think defeats the entire cleanup. So I decided to leave those constants this in this test class for now and reconsider that move as a future work. Would that be OK?

@jaikiran
Copy link
Member Author

/contributor add @AlekseiEfimov

@openjdk
Copy link

openjdk bot commented Sep 12, 2025

@jaikiran
Contributor Aleksei Efimov <aefimov@openjdk.org> successfully added.

@AlekseiEfimov
Copy link
Member

Hello Aleksei, I have updated the PR to implement your suggestion 1 and 2. As for the other suggestion of moving these constants to the LDAPTestUtils test library class, I think that's a good idea too. However, I gave that a try and that isn't straightforward. Once I move them to that class, jtreg then requires me to add a @build and a @library instruction to bring in the test.LDAPServer class which the LDAPTestUtils uses (but not this new test). That wouldn't be too bad and I did add those, but then LDAPTestUtils has an import of an internal class:

import com.sun.jndi.ldap.LdapURL

so that then forces me to reintroduce the:

@modules java.naming/com.sun.jndi.ldap

in this new test, which I think defeats the entire cleanup. So I decided to leave those constants this in this test class for now and reconsider that move as a future work. Would that be OK?

Oh, it is complicated, thank you for trying to move the constants.
I agree that it defeats the entire cleanup initiative and doesn't worth the effort.
It is OK to leave the constants in the test class.

Copy link
Member

@AlekseiEfimov AlekseiEfimov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!

@openjdk openjdk bot added the ready Pull request is ready to be integrated label Sep 12, 2025
@jaikiran
Copy link
Member Author

Thank you everyone for the reviews. I've triggered a CI run with these latest changes and when that completes I'll go ahead and integrate this.

@jaikiran
Copy link
Member Author

/integrate

@openjdk
Copy link

openjdk bot commented Sep 13, 2025

Going to push as commit e2eaa2e.
Since your change was applied there have been 17 commits pushed to the master branch:

Your commit was automatically rebased without conflicts.

@openjdk openjdk bot added the integrated Pull request has been integrated label Sep 13, 2025
@openjdk openjdk bot closed this Sep 13, 2025
@openjdk openjdk bot removed ready Pull request is ready to be integrated rfr Pull request is ready for review labels Sep 13, 2025
@openjdk
Copy link

openjdk bot commented Sep 13, 2025

@jaikiran Pushed as commit e2eaa2e.

💡 You may see a message that your pull request was closed with unmerged commits. This can be safely ignored.

@jaikiran jaikiran deleted the 8357708 branch September 13, 2025 02:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core-libs core-libs-dev@openjdk.org integrated Pull request has been integrated

Development

Successfully merging this pull request may close these issues.

4 participants