RFR [14] 8217606: LdapContext#reconnect always opens a new connection

Pavel Rappo pavel.rappo at oracle.com
Mon Aug 12 17:41:12 UTC 2019


Comments are inline.

> On 8 Aug 2019, at 18:59, Roger Riggs <Roger.Riggs at oracle.com> wrote:
> 
> Hi Pavel,
> 
> To your questions...
> 
> 
> On 8/8/19 11:07 AM, Pavel Rappo wrote:
> ...
>>> 109: "template method" doesn't describe the method well, the method is private and not overridable.
>>>    update the javadoc.
>>> 
>> I can see several questions here. Correct me if I'm wrong. The first one is about the use of the term "Template Method". This is the name of the behavioral pattern that has been applied here. We can try to describe what that method does a bit better, however... here goes the second question. There is no Javadoc generated for classes like this one. So it's very likely that the documentation we are talking about will be read in conjunction with the source code in an editor. Combine that with the fact that this is a test supporting infrastructure and you might see why I think it would be easier to just outline the intent of the method. People will read the source code anyway. Which is not to say that documentation can be avoided altogether, it's just that I wouldn't sweat it.
> Since its a private method, it is not a template for anything and the term doesn't apply.
>> The third question if that method should be overridable. The canonical version [1] of the Template Method pattern says: 
>> 
>> "...Primitive operations that _must_ be overridden are declared pure virtual. The template method itself should not be overridden..."
>> 
> Refering the 'Template" pattern doesn't make the documentation clearer.
>> I don't have a strong opinion about this, only a preference. I prefer that we will make it overridable only if we find out that the required degree of extensibility cannot be achieved by introducing additional extension/hook methods (the ones that the template method calls). Think about it this way. If that method were overridable, then why would that class be called `BaseLdapServer`? That method is responsible for that class' "LDAPness"!
> Private is fine.
> 
> ...
>>> 178:  logger() should be final. The subclass has no reason to override.  And it can be "public final".
>>> 
>> final? Yes. public? I'd rather leave it as it is. I can't think of a case where an *internal* logger should be sticking out of that class. Hence it's not public. It might be required when subclassing though, hence it's not private or of default (package) access.
> final is enough to avoid hacking around without giving it some thought.

I will update the documentation accordingly.

>>> 238: isRunning should be synchronized(lock) to make sure each Thread sees the same value threads that update it in start() and close()
>>> 
>> We have already discussed it with you in this thread. The `state` is `volatile` which is enough for visibility in this query-method.
> yes, but there are multiple locking/synchronization constructs that make the code harder to read
> as unnecessary complexity.

Fine. I will remove `volatile` and synchronize on `lock` in the `isRunning` method.

>>> 105:  Tests that sleep are prone to intermittent failures on slow or delayed systems.
>>> 
>> True, however, I'm not aware of any general solution of this problem.
> General is not needed, and sleeps waste time time and resources.
> In this particular test, if there is no possiblity of multiple connections then CountDownLatch is a good mechanism.

I would prefer to test for a particular number of connections.

>>> It would be more reliable to countdown *after* the Connection was handled.
>>> As is, it might report success even if handleRequest failed for some reason.
>>> 
>> Hm... Let me think. What we are interested in here is whether the connection is attempted or not. We may do what you are suggesting just to be sure the server is not failing. That is, the client is served successfully.
> The test description is not specific about that.  
> And it raises questions about the purpose of the environment setup.  
> Is it all needed. 

I will update the documentation.

> Is handling Unbind in the switch needed (as different from the default).

Chris might want to answer that.

>>> <snip>
>>> 
>>> If there as some suspecion of too many connections
>>> that could be checked after the beforeConnectionHandled called countDown.
>>> 
>> Wouldn't that make problems of its own? If I got you right, a time window for the LDAP client to create more *unwanted* connections could be really small. A hybrid approach might be even better. We wait for the first connection with a generous timeout and then give the client some extra time to create more connections.
>> 
>> It really is fine-tuning. You can't shield completely from arbitrarily slow systems.
>> 
> That's fine, a Semaphore can be used to wait for the first connection and then check with a different timeout for unexpected connections.

I've sketched some possible implementations of the hybrid check we talked about. Even though the "Semaphore option" looks the most concise, it simultaneously looks the most "off-label" and counterintuitive (to my liking). However, I'm fine with it.

-Pavel

// --------------------------------------------------

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Phaser;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;

public class TwoPhase {

    public static void main(String[] args) throws InterruptedException {
        final Option opt = new OptionA();
//        final Option opt = new OptionB();
//        final Option opt = new OptionC();
        opt.acceptConnection();
        opt.acceptConnection();
        opt.checkConnections();
    }

    private interface Option {

        void acceptConnection();

        void checkConnections() throws InterruptedException;
    }

    private static class OptionA implements Option {

        private final CountDownLatch firstConnection = new CountDownLatch(1);
        private final AtomicInteger connectionsCount = new AtomicInteger();
        private final CountDownLatch otherConnections = new CountDownLatch(1);

        @Override
        public void acceptConnection() {
            if (connectionsCount.incrementAndGet() == 1) {
                firstConnection.countDown();
            } else {
                otherConnections.countDown();
            }
        }

        @Override
        public void checkConnections() throws InterruptedException {
            if (!firstConnection.await(60L, TimeUnit.SECONDS)) {
                throw new RuntimeException("Failed");
            }
            if (otherConnections.await(5L, TimeUnit.SECONDS)) {
                throw new RuntimeException("Unexpected connections: "
                                                   + connectionsCount.get());
            }
        }
    }

    private static class OptionB implements Option {

        private final Semaphore s = new Semaphore(0);

        @Override
        public void acceptConnection() {
            s.release(1);
        }

        @Override
        public void checkConnections() throws InterruptedException {
            if (!s.tryAcquire(60L, TimeUnit.SECONDS)) {
                throw new RuntimeException("Failed");
            }
            if (s.tryAcquire(5L, TimeUnit.SECONDS)) {
                throw new RuntimeException("Unexpected connections: "
                                                   + (s.availablePermits() + 2));
            }
        }
    }

    private static class OptionC implements Option {

        private final Phaser p = new Phaser(1);

        @Override
        public void acceptConnection() {
            p.arrive();
        }

        @Override
        public void checkConnections() throws InterruptedException {
            try {
                p.awaitAdvanceInterruptibly(0, 60L, TimeUnit.SECONDS);
            } catch (TimeoutException e) {
                throw new RuntimeException("Failed");
            }
            try {
                p.awaitAdvanceInterruptibly(1, 5L, TimeUnit.SECONDS);
            } catch (TimeoutException e) {
                return; /* expected */
            }
            throw new RuntimeException("Unexpected connections: "
                                               + p.getPhase());
        }
    }
}

// --------------------------------------------------




More information about the core-libs-dev mailing list