--- old/src/java.naming/share/classes/com/sun/jndi/ldap/DefaultLdapDnsProvider.java 2019-08-30 13:49:18.000000000 +0100 +++ new/src/java.naming/share/classes/com/sun/jndi/ldap/DefaultLdapDnsProvider.java 2019-08-30 13:49:18.000000000 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -76,7 +76,7 @@ } LdapDnsProviderResult res = new LdapDnsProviderResult(domainName, endpoints); - if (res.getEndpoints().size() == 0 && res.getDomainName().isEmpty()) { + if (res.getEndpoints().isEmpty() && res.getDomainName().isEmpty()) { return Optional.empty(); } else { return Optional.of(res); --- old/src/java.naming/share/classes/com/sun/jndi/ldap/LdapDnsProviderService.java 2019-08-30 13:49:19.000000000 +0100 +++ new/src/java.naming/share/classes/com/sun/jndi/ldap/LdapDnsProviderService.java 2019-08-30 13:49:19.000000000 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -37,6 +37,8 @@ * The {@code LdapDnsProviderService} is responsible for creating and providing * access to the registered {@code LdapDnsProvider}s. The {@link ServiceLoader} * is used to find and register any implementations of {@link LdapDnsProvider}. + * + *

Instances of this class are safe for use by multiple threads. */ final class LdapDnsProviderService { @@ -68,11 +70,11 @@ } /** - * Retrieve the singleton static instance of LdapDnsProviderService. + * Retrieves the singleton instance of LdapDnsProviderService. */ static LdapDnsProviderService getInstance() { if (service != null) return service; - synchronized(LOCK) { + synchronized (LOCK) { if (service != null) return service; service = new LdapDnsProviderService(); } @@ -80,7 +82,7 @@ } /** - * Retrieve result from the first provider that successfully resolves + * Retrieves result from the first provider that successfully resolves * the endpoints. If no results are found when calling installed * subclasses of {@code LdapDnsProvider} then this method will fall back * to the {@code DefaultLdapDnsProvider}. @@ -91,14 +93,15 @@ LdapDnsProviderResult lookupEndpoints(String url, Hashtable env) throws NamingException { - Iterator iterator = providers.iterator(); - Hashtable envCopy = new Hashtable<>(env); LdapDnsProviderResult result = null; - - while (result == null && iterator.hasNext()) { - result = iterator.next().lookupEndpoints(url, envCopy) - .filter(r -> r.getEndpoints().size() > 0) - .orElse(null); + Hashtable envCopy = new Hashtable<>(env); + synchronized (LOCK) { + Iterator iterator = providers.iterator(); + while (result == null && iterator.hasNext()) { + result = iterator.next().lookupEndpoints(url, envCopy) + .filter(r -> !r.getEndpoints().isEmpty()) + .orElse(null); + } } if (result == null) { --- old/test/jdk/ProblemList.txt 2019-08-30 13:49:20.000000000 +0100 +++ new/test/jdk/ProblemList.txt 2019-08-30 13:49:20.000000000 +0100 @@ -873,8 +873,6 @@ com/sun/jndi/ldap/DeadSSLLdapTimeoutTest.java 8169942 linux-i586,macosx-all,windows-x64 -com/sun/jndi/ldap/LdapTimeoutTest.java 8151678 generic-all - com/sun/jndi/dns/ConfigTests/PortUnreachable.java 7164518 macosx-all javax/rmi/ssl/SSLSocketParametersTest.sh 8162906 generic-all --- old/test/jdk/com/sun/jndi/ldap/LdapTimeoutTest.java 2019-08-30 13:49:21.000000000 +0100 +++ new/test/jdk/com/sun/jndi/ldap/LdapTimeoutTest.java 2019-08-30 13:49:21.000000000 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2016, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2019, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -21,424 +21,496 @@ * questions. */ -/** +/* * @test - * @run main/othervm LdapTimeoutTest - * @bug 7094377 8000487 6176036 7056489 + * @library lib/ + * @run testng/othervm LdapTimeoutTest + * @bug 7094377 8000487 6176036 7056489 8151678 * @summary Timeout tests for ldap */ +import org.testng.Assert; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.directory.InitialDirContext; +import javax.naming.directory.SearchControls; +import java.io.IOException; +import java.io.OutputStream; import java.net.Socket; -import java.net.ServerSocket; -import java.net.SocketTimeoutException; -import java.io.*; -import javax.naming.*; -import javax.naming.directory.*; -import java.util.List; -import java.util.Hashtable; import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.Objects; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeoutException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; -import javax.net.ssl.SSLHandshakeException; +import java.util.concurrent.TimeoutException; +import static java.lang.String.format; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; +public class LdapTimeoutTest { -abstract class LdapTest implements Callable { - - Hashtable env; - TestServer server; - ScheduledExecutorService killSwitchPool; - boolean passed = false; - private int HANGING_TEST_TIMEOUT = 20_000; - - public LdapTest (TestServer server, Hashtable env) { - this.server = server; - this.env = env; - } - - public LdapTest(TestServer server, Hashtable env, - ScheduledExecutorService killSwitchPool) - { - this(server, env); - this.killSwitchPool = killSwitchPool; - } - - public abstract void performOp(InitialContext ctx) throws NamingException; - public abstract void handleNamingException( - NamingException e, long start, long end); - - public void pass() { - this.passed = true; - } - - public void fail() { - throw new RuntimeException("Test failed"); - } - - public void fail(Exception e) { - throw new RuntimeException("Test failed", e); - } + // ------ configure test timeouts here ------ - boolean shutItDown(InitialContext ctx) { + /* + * Practical representation of "indefinite" timeout. + */ + private static final long INFINITY_MILLIS = 20_000; + /* + * The lag. + * + * In other words, how long it takes to observe a timeout after it has + * occurred. This time is provisioned for things like stack unwinding and + * threads communication. + */ + private static final long RIGHT_MARGIN = 3_000; + /* + * The accuracy. + * + * Consider an activity that is supposed to take 10 seconds starts a little + * bit before we start measuring those 10 seconds. So from our point of view + * that 10-second activity may finish within, say, 9.7 seconds. + * + * Another example would be using a timed operation that returns prematurely. + */ + private static final long LEFT_MARGIN = 200; + + private static final long CONNECT_MILLIS = 3_000; + private static final long READ_MILLIS = 10_000; + + static { + // quick sanity check to make sure this timeouts configuration is + // consistent and the timeouts do not overlap + assert (LEFT_MARGIN >= 0 && RIGHT_MARGIN >= 0); + assert (2 * CONNECT_MILLIS + RIGHT_MARGIN < READ_MILLIS - LEFT_MARGIN); + assert (2 * CONNECT_MILLIS + READ_MILLIS + RIGHT_MARGIN < INFINITY_MILLIS - LEFT_MARGIN); + } + + @BeforeTest + public void beforeTest() { + startAuxiliaryDiagnosticOutput(); + } + + /* + * These are timeout tests and they are run in parallel to reduce the total + * amount of run time. + * + * Currently it doesn't seem possible to instruct JTREG to run TestNG test + * methods in parallel. That said, this JTREG test is still + * a "TestNG-flavored" test for the sake of having org.testng.Assert + * capability. + */ + @Test + public void test() throws Exception { + List> futures = new ArrayList<>(); + ExecutorService executorService = Executors.newCachedThreadPool(); try { - if (ctx != null) ctx.close(); - return true; - } catch (NamingException ex) { - return false; + futures.add(executorService.submit(() -> { test1(); return null; })); + futures.add(executorService.submit(() -> { test2(); return null; })); + futures.add(executorService.submit(() -> { test3(); return null; })); + futures.add(executorService.submit(() -> { test4(); return null; })); + futures.add(executorService.submit(() -> { test5(); return null; })); + futures.add(executorService.submit(() -> { test6(); return null; })); + futures.add(executorService.submit(() -> { test7(); return null; })); + } finally { + executorService.shutdown(); } - } - - public Boolean call() { - InitialContext ctx = null; - ScheduledFuture killer = null; - long start = System.nanoTime(); - - try { - while(!server.accepting()) - Thread.sleep(200); // allow the server to start up - Thread.sleep(200); // to be sure - - // if this is a hanging test, scheduled a thread to - // interrupt after a certain time - if (killSwitchPool != null) { - final Thread current = Thread.currentThread(); - killer = killSwitchPool.schedule( - new Callable() { - public Void call() throws Exception { - current.interrupt(); - return null; - } - }, HANGING_TEST_TIMEOUT, MILLISECONDS); - } - - env.put(Context.PROVIDER_URL, "ldap://localhost:" + - server.getLocalPort()); - + int failedCount = 0; + for (var f : futures) { try { - ctx = new InitialDirContext(env); - performOp(ctx); - fail(); - } catch (NamingException e) { - long end = System.nanoTime(); - System.out.println(this.getClass().toString() + " - elapsed: " - + NANOSECONDS.toMillis(end - start)); - handleNamingException(e, start, end); - } finally { - if (killer != null && !killer.isDone()) - killer.cancel(true); - shutItDown(ctx); - server.close(); + f.get(); + } catch (ExecutionException e) { + failedCount++; + e.getCause().printStackTrace(System.out); } - return passed; - } catch (IOException|InterruptedException e) { - throw new RuntimeException(e); } + if (failedCount > 0) + throw new RuntimeException(failedCount + " (sub)tests failed"); } -} -abstract class ReadServerTest extends LdapTest { + static void test1() throws Exception { + Hashtable env = new Hashtable<>(); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + // Here and in the other tests it's important to close the server as + // calling `thread.interrupt` from assertion may not be enough + // (depending on where the blocking call has stuck) + try (TestServer server = new NotBindableServer()) { + env.put(Context.PROVIDER_URL, urlTo(server)); + server.start(); + // Here and in the other tests joining done purely to reduce timing + // jitter. Commenting out or removing that should not make the test + // incorrect. (ServerSocket can accept connection as soon as it is + // bound, not need to call `accept` before that.) + server.starting().join(); + assertIncompletion(INFINITY_MILLIS, () -> new InitialDirContext(env)); + } + } - public ReadServerTest(Hashtable env) throws IOException { - super(new BindableServer(), env); + static void test2() throws Exception { + Hashtable env = new Hashtable<>(); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + env.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(CONNECT_MILLIS)); + try (TestServer server = new BindableButNotReadableServer()) { + env.put(Context.PROVIDER_URL, urlTo(server)); + server.start(); + server.starting().join(); + InitialDirContext ctx = new InitialDirContext(env); + SearchControls scl = new SearchControls(); + scl.setSearchScope(SearchControls.SUBTREE_SCOPE); + assertIncompletion(INFINITY_MILLIS, + () -> ctx.search("ou=People,o=JNDITutorial", "(objectClass=*)", scl)); + } } - public ReadServerTest(Hashtable env, - ScheduledExecutorService killSwitchPool) - throws IOException - { - super(new BindableServer(), env, killSwitchPool); + static void test3() throws Exception { + Hashtable env = new Hashtable<>(); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + try (TestServer server = new BindableButNotReadableServer()) { + env.put(Context.PROVIDER_URL, urlTo(server)); + server.start(); + server.starting().join(); + InitialDirContext ctx = new InitialDirContext(env); + SearchControls scl = new SearchControls(); + scl.setSearchScope(SearchControls.SUBTREE_SCOPE); + assertIncompletion(INFINITY_MILLIS, + () -> ctx.search("ou=People,o=JNDITutorial", "(objectClass=*)", scl)); + } } - public void performOp(InitialContext ctx) throws NamingException { - SearchControls scl = new SearchControls(); - scl.setSearchScope(SearchControls.SUBTREE_SCOPE); - NamingEnumeration answer = ((InitialDirContext)ctx) - .search("ou=People,o=JNDITutorial", "(objectClass=*)", scl); + static void test4() throws Exception { + Hashtable env = new Hashtable<>(); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + env.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(CONNECT_MILLIS)); + env.put("com.sun.jndi.ldap.read.timeout", String.valueOf(READ_MILLIS)); + try (TestServer server = new NotBindableServer()) { + env.put(Context.PROVIDER_URL, urlTo(server)); + server.start(); + server.starting().join(); + Assert.ThrowingRunnable completion = + () -> assertCompletion(CONNECT_MILLIS - LEFT_MARGIN, + 2 * CONNECT_MILLIS + RIGHT_MARGIN, + () -> new InitialDirContext(env)); + NamingException e = expectThrows(NamingException.class, completion); + String msg = e.getMessage(); + assertTrue(msg != null && msg.contains("timeout") + && msg.contains(String.valueOf(CONNECT_MILLIS)), + msg); + } } -} -abstract class DeadServerTest extends LdapTest { + static void test5() throws Exception { + Hashtable env = new Hashtable<>(); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + env.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(CONNECT_MILLIS)); + env.put("com.sun.jndi.ldap.read.timeout", String.valueOf(READ_MILLIS)); + try (TestServer server = new BindableButNotReadableServer()) { + env.put(Context.PROVIDER_URL, urlTo(server)); + server.start(); + server.starting().join(); + InitialDirContext ctx = new InitialDirContext(env); + SearchControls scl = new SearchControls(); + scl.setSearchScope(SearchControls.SUBTREE_SCOPE); + Assert.ThrowingRunnable completion = + () -> assertCompletion(READ_MILLIS - LEFT_MARGIN, + READ_MILLIS + RIGHT_MARGIN, + () -> ctx.search("ou=People,o=JNDITutorial", "(objectClass=*)", scl)); + NamingException e = expectThrows(NamingException.class, completion); + String msg = e.getMessage(); + assertTrue(msg != null && msg.contains("timeout") + && msg.contains(String.valueOf(READ_MILLIS)), + msg); + } + } - public DeadServerTest(Hashtable env) throws IOException { - super(new DeadServer(), env); + static void test6() throws Exception { + Hashtable env = new Hashtable<>(); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + env.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(CONNECT_MILLIS)); + env.put("com.sun.jndi.ldap.read.timeout", String.valueOf(READ_MILLIS)); + try (TestServer server = new NotBindableServer()) { + env.put(Context.PROVIDER_URL, urlTo(server)); + server.start(); + server.starting().join(); + Assert.ThrowingRunnable completion = + () -> assertCompletion(CONNECT_MILLIS - LEFT_MARGIN, + 2 * CONNECT_MILLIS + RIGHT_MARGIN, + () -> new InitialDirContext(env)); + NamingException e = expectThrows(NamingException.class, completion); + String msg = e.getMessage(); + assertTrue(msg != null && msg.contains("timeout") + && msg.contains(String.valueOf(CONNECT_MILLIS)), + msg); + } } - public DeadServerTest(Hashtable env, - ScheduledExecutorService killSwitchPool) - throws IOException - { - super(new DeadServer(), env, killSwitchPool); + static void test7() throws Exception { + // 8000487: Java JNDI connection library on ldap conn is + // not honoring configured timeout + Hashtable env = new Hashtable<>(); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + env.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(CONNECT_MILLIS)); + env.put("com.sun.jndi.ldap.read.timeout", String.valueOf(READ_MILLIS)); + env.put(Context.SECURITY_AUTHENTICATION, "simple"); + env.put(Context.SECURITY_PRINCIPAL, "user"); + env.put(Context.SECURITY_CREDENTIALS, "password"); + try (TestServer server = new NotBindableServer()) { + env.put(Context.PROVIDER_URL, urlTo(server)); + server.start(); + server.starting().join(); + Assert.ThrowingRunnable completion = + () -> assertCompletion(CONNECT_MILLIS - LEFT_MARGIN, + 2 * CONNECT_MILLIS + RIGHT_MARGIN, + () -> new InitialDirContext(env)); + NamingException e = expectThrows(NamingException.class, completion); + String msg = e.getMessage(); + assertTrue(msg != null && msg.contains("timeout") + && msg.contains(String.valueOf(CONNECT_MILLIS)), + msg); + } } - public void performOp(InitialContext ctx) throws NamingException {} -} + // ------ test stub servers ------ -class DeadServerNoTimeoutTest extends DeadServerTest { + static class TestServer extends BaseLdapServer { - public DeadServerNoTimeoutTest(Hashtable env, - ScheduledExecutorService killSwitchPool) - throws IOException - { - super(env, killSwitchPool); - } + private final CompletableFuture starting = new CompletableFuture<>(); - public void handleNamingException(NamingException e, long start, long end) { - if (e instanceof InterruptedNamingException) Thread.interrupted(); + TestServer() throws IOException { } - if (NANOSECONDS.toMillis(end - start) < LdapTimeoutTest.MIN_TIMEOUT) { - System.err.printf("DeadServerNoTimeoutTest fail: timeout should be " + - "at least %s ms, actual time is %s ms%n", - LdapTimeoutTest.MIN_TIMEOUT, - NANOSECONDS.toMillis(end - start)); - fail(); - } else { - pass(); + @Override + protected void beforeAcceptingConnections() { + starting.completeAsync(() -> null); } - } -} - -class DeadServerTimeoutTest extends DeadServerTest { - public DeadServerTimeoutTest(Hashtable env) throws IOException { - super(env); - } - - public void handleNamingException(NamingException e, long start, long end) - { - // non SSL connect will timeout via readReply using connectTimeout - if (NANOSECONDS.toMillis(end - start) < 2_900) { - pass(); - } else { - System.err.println("Fail: Waited too long"); - fail(); + public CompletableFuture starting() { + return starting.copy(); } } -} + static class BindableButNotReadableServer extends TestServer { -class ReadServerNoTimeoutTest extends ReadServerTest { + BindableButNotReadableServer() throws IOException { } - public ReadServerNoTimeoutTest(Hashtable env, - ScheduledExecutorService killSwitchPool) - throws IOException - { - super(env, killSwitchPool); + private static final byte[] bindResponse = { + 0x30, 0x0C, 0x02, 0x01, 0x01, 0x61, 0x07, 0x0A, + 0x01, 0x00, 0x04, 0x00, 0x04, 0x00 + }; + + @Override + protected void handleRequest(Socket socket, + LdapMessage msg, + OutputStream out) + throws IOException { + switch (msg.getOperation()) { + case BIND_REQUEST: + out.write(bindResponse); + out.flush(); + default: + break; + } + } } - public void handleNamingException(NamingException e, long start, long end) { - if (e instanceof InterruptedNamingException) Thread.interrupted(); + static class NotBindableServer extends TestServer { + + NotBindableServer() throws IOException { } - if (NANOSECONDS.toMillis(end - start) < LdapTimeoutTest.MIN_TIMEOUT) { - System.err.printf("ReadServerNoTimeoutTest fail: timeout should be " + - "at least %s ms, actual time is %s ms%n", - LdapTimeoutTest.MIN_TIMEOUT, - NANOSECONDS.toMillis(end - start)); - fail(); - } else { - pass(); + @Override + protected void beforeConnectionHandled(Socket socket) { + try { + TimeUnit.DAYS.sleep(Integer.MAX_VALUE); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } } } -} -class ReadServerTimeoutTest extends ReadServerTest { + // ------ timeouts check utilities ------ - public ReadServerTimeoutTest(Hashtable env) throws IOException { - super(env); - } - - public void handleNamingException(NamingException e, long start, long end) { - System.out.println("ReadServerTimeoutTest: end-start=" + NANOSECONDS.toMillis(end - start)); - if (NANOSECONDS.toMillis(end - start) < 2_500) { - fail(); - } else { - pass(); + /* + * Asserts that the specified executable yields a result or an exception + * within the specified time frame. Interrupts the executable + * unconditionally. + * + * If the executable yields a result or an exception within the specified + * time frame, the result will be returned and the exception will be + * rethrown respectively in a transparent fashion as if the executable was + * executed directly. + */ + public static T assertCompletion(long loMillis, + long hiMillis, + Callable code) + throws Throwable { + if (loMillis < 0 || hiMillis < 0 || loMillis > hiMillis) { + throw new IllegalArgumentException("loMillis=" + loMillis + + ", hiMillis=" + hiMillis); } - } -} + Objects.requireNonNull(code); -class TestServer extends Thread { - ServerSocket serverSock; - boolean accepting = false; + // this queue acts both as an exchange point and a barrier + SynchronousQueue startTime = new SynchronousQueue<>(); - public TestServer() throws IOException { - this.serverSock = new ServerSocket(0); - start(); - } + Callable wrappedTask = () -> { + // by the time this value reaches the "stopwatch" thread it might be + // well outdated and that's okay, we will adjust the wait time + startTime.put(System.nanoTime()); + return code.call(); + }; + + FutureTask task = new FutureTask<>(wrappedTask); + Thread t = new Thread(task); + t.start(); - public int getLocalPort() { - return serverSock.getLocalPort(); - } + final long startNanos; + try { + startNanos = startTime.take(); // (1) wait for the initial time mark + } catch (Throwable e) { + t.interrupt(); + throw e; + } - public boolean accepting() { - return accepting; - } + final long waitTime = hiMillis - + NANOSECONDS.toMillis(System.nanoTime() - startNanos); // (2) adjust wait time - public void close() throws IOException { - serverSock.close(); + try { + T r = task.get(waitTime, MILLISECONDS); // (3) wait for the task to complete + long elapsed = NANOSECONDS.toMillis(System.nanoTime() - startNanos); + if (elapsed < loMillis || elapsed > hiMillis) { + throw new RuntimeException(format( + "After %s ms. returned result '%s'", elapsed, r)); + } + return r; + } catch (ExecutionException e) { + long elapsed = NANOSECONDS.toMillis(System.nanoTime() - startNanos); + if (elapsed < loMillis || elapsed > hiMillis) { + throw new RuntimeException(format( + "After %s ms. thrown exception", elapsed), e); + } + throw e.getCause(); + } catch (TimeoutException e) { + // We trust timed get not to throw TimeoutException prematurely + // (i.e. before the wait time elapses) + long elapsed = NANOSECONDS.toMillis(System.nanoTime() - startNanos); + throw new RuntimeException(format( + "After %s ms. is incomplete", elapsed)); + } finally { + t.interrupt(); + } } -} -class BindableServer extends TestServer { + /* + * Asserts that the specified executable yields no result and no exception + * for at least the specified amount of time. Interrupts the executable + * unconditionally. + */ + public static void assertIncompletion(long millis, Callable code) + throws Exception + { + if (millis < 0) { + throw new IllegalArgumentException("millis=" + millis); + } + Objects.requireNonNull(code); - public BindableServer() throws IOException { - super(); - } + // this queue acts both as an exchange point and a barrier + SynchronousQueue startTime = new SynchronousQueue<>(); - private byte[] bindResponse = { - 0x30, 0x0C, 0x02, 0x01, 0x01, 0x61, 0x07, 0x0A, - 0x01, 0x00, 0x04, 0x00, 0x04, 0x00 - }; + Callable wrappedTask = () -> { + // by the time this value reaches the "stopwatch" thread it might be + // well outdated and that's okay, we will adjust the wait time + startTime.put(System.nanoTime()); + return code.call(); + }; + + FutureTask task = new FutureTask<>(wrappedTask); + Thread t = new Thread(task); + t.start(); - public void run() { + final long startNanos; try { - accepting = true; - Socket socket = serverSock.accept(); - InputStream in = socket.getInputStream(); - OutputStream out = socket.getOutputStream(); - - // Read the LDAP BindRequest - while (in.read() != -1) { - in.skip(in.available()); - break; - } - - // Write an LDAP BindResponse - out.write(bindResponse); - out.flush(); - } catch (IOException e) { - // ignore + startNanos = startTime.take(); // (1) wait for the initial time mark + } catch (Throwable e) { + t.interrupt(); + throw e; } - } -} -class DeadServer extends TestServer { + final long waitTime = millis - + NANOSECONDS.toMillis(System.nanoTime() - startNanos); // (2) adjust wait time - public DeadServer() throws IOException { - super(); - } - - public void run() { - while(true) { - try { - accepting = true; - Socket socket = serverSock.accept(); - } catch (Exception e) { - break; + try { + Object r = task.get(waitTime, MILLISECONDS); // (3) wait for the task to complete + long elapsed = NANOSECONDS.toMillis(System.nanoTime() - startNanos); + if (elapsed < waitTime) { + throw new RuntimeException(format( + "After %s ms. returned result '%s'", elapsed, r)); + } + } catch (ExecutionException e) { + long elapsed = NANOSECONDS.toMillis(System.nanoTime() - startNanos); + if (elapsed < waitTime) { + throw new RuntimeException(format( + "After %s ms. thrown exception", elapsed), e); } + } catch (TimeoutException expected) { + } finally { + t.interrupt(); } } -} -public class LdapTimeoutTest { + // ------ miscellaneous utilities ------ - private static final ExecutorService testPool = - Executors.newFixedThreadPool(3); - private static final ScheduledExecutorService killSwitchPool = - Executors.newScheduledThreadPool(3); - public static int MIN_TIMEOUT = 18_000; - - static Hashtable createEnv() { - Hashtable env = new Hashtable(11); - env.put(Context.INITIAL_CONTEXT_FACTORY, - "com.sun.jndi.ldap.LdapCtxFactory"); - return env; + private static String urlTo(TestServer server) { + String hostAddress = server.getInetAddress().getHostAddress(); + String addr; + if (hostAddress.contains(":")) { // IPv6 + addr = '[' + hostAddress + ']'; + } else { // IPv4 + addr = hostAddress; + } + return "ldap://" + addr + ":" + server.getPort(); } - public static void main(String[] args) throws Exception { - - InitialContext ctx = null; - List results = new ArrayList<>(); - - try { - // run the DeadServerTest with no timeouts set - // this should get stuck indefinitely, so we need to kill - // it after a timeout - System.out.println("Running connect timeout test with 20s kill switch"); - Hashtable env = createEnv(); - results.add( - testPool.submit(new DeadServerNoTimeoutTest(env, killSwitchPool))); - - // run the ReadServerTest with connect timeout set - // this should get stuck indefinitely so we need to kill - // it after a timeout - System.out.println("Running read timeout test with 10ms connect timeout & 20s kill switch"); - Hashtable env1 = createEnv(); - env1.put("com.sun.jndi.ldap.connect.timeout", "10"); - results.add(testPool.submit( - new ReadServerNoTimeoutTest(env1, killSwitchPool))); - - // run the ReadServerTest with no timeouts set - // this should get stuck indefinitely, so we need to kill - // it after a timeout - System.out.println("Running read timeout test with 20s kill switch"); - Hashtable env2 = createEnv(); - results.add(testPool.submit( - new ReadServerNoTimeoutTest(env2, killSwitchPool))); - - // run the DeadServerTest with connect / read timeouts set - // this should exit after the connect timeout expires - System.out.println("Running connect timeout test with 10ms connect timeout, 3000ms read timeout"); - Hashtable env3 = createEnv(); - env3.put("com.sun.jndi.ldap.connect.timeout", "10"); - env3.put("com.sun.jndi.ldap.read.timeout", "3000"); - results.add(testPool.submit(new DeadServerTimeoutTest(env3))); - - - // run the ReadServerTest with connect / read timeouts set - // this should exit after the connect timeout expires - // - // NOTE: commenting this test out as it is failing intermittently. - // - // System.out.println("Running read timeout test with 10ms connect timeout, 3000ms read timeout"); - // Hashtable env4 = createEnv(); - // env4.put("com.sun.jndi.ldap.connect.timeout", "10"); - // env4.put("com.sun.jndi.ldap.read.timeout", "3000"); - // results.add(testPool.submit(new ReadServerTimeoutTest(env4))); - - // run the DeadServerTest with connect timeout set - // this should exit after the connect timeout expires - System.out.println("Running connect timeout test with 10ms connect timeout"); - Hashtable env5 = createEnv(); - env5.put("com.sun.jndi.ldap.connect.timeout", "10"); - results.add(testPool.submit(new DeadServerTimeoutTest(env5))); - - // 8000487: Java JNDI connection library on ldap conn is - // not honoring configured timeout - System.out.println("Running simple auth connection test"); - Hashtable env6 = createEnv(); - env6.put("com.sun.jndi.ldap.connect.timeout", "10"); - env6.put("com.sun.jndi.ldap.read.timeout", "3000"); - env6.put(Context.SECURITY_AUTHENTICATION, "simple"); - env6.put(Context.SECURITY_PRINCIPAL, "user"); - env6.put(Context.SECURITY_CREDENTIALS, "password"); - results.add(testPool.submit(new DeadServerTimeoutTest(env6))); - - boolean testFailed = false; - for (Future test : results) { - while (!test.isDone()) { - if ((Boolean) test.get() == false) - testFailed = true; + /* + * A diagnostic aid that might help with debugging timeout issues. The idea + * is to continuously measure accuracy and responsiveness of the system that + * runs this test. If the system is overwhelmed (with something else), it + * might affect the test run. At the very least we will have traces of that + * in the logs. + * + * This utility does not automatically scale up test timeouts, it simply + * gathers information. + */ + private static void startAuxiliaryDiagnosticOutput() { + System.out.printf("Starting diagnostic output (probe)%n"); + Thread t = new Thread(() -> { + for (int i = 0; ; i = ((i % 20) + 1)) { + // 500, 1_000, 1_500, ..., 9_500, 10_000, 500, 1_000, ... + long expected = i * 500; + long start = System.nanoTime(); + try { + MILLISECONDS.sleep(expected); + } catch (InterruptedException e) { + return; } - } + long stop = System.nanoTime(); + long actual = NANOSECONDS.toMillis(stop - start); + System.out.printf("(probe) expected [ms.]: %s, actual [ms.]: %s%n", + expected, actual); - if (testFailed) { - throw new AssertionError("some tests failed"); } - - } finally { - LdapTimeoutTest.killSwitchPool.shutdown(); - LdapTimeoutTest.testPool.shutdown(); - } + }, "probe"); + t.setDaemon(true); + t.start(); } - } - --- old/test/jdk/com/sun/jndi/ldap/lib/BaseLdapServer.java 2019-08-30 13:49:22.000000000 +0100 +++ new/test/jdk/com/sun/jndi/ldap/lib/BaseLdapServer.java 2019-08-30 13:49:22.000000000 +0100 @@ -35,7 +35,6 @@ import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.RejectedExecutionException; import static java.lang.System.Logger.Level.INFO; @@ -44,6 +43,7 @@ * * Override the following methods to provide customized behavior * + * * beforeAcceptingConnections * * beforeConnectionHandled * * handleRequest * @@ -83,6 +83,7 @@ logger().log(INFO, "Server is accepting connections at port {0}", getPort()); try { + beforeAcceptingConnections(); while (isRunning()) { Socket socket = serverSocket.accept(); logger().log(INFO, "Accepted new connection at {0}", socket); @@ -97,10 +98,10 @@ } connectionsPool.submit(() -> handleConnection(socket)); } - } catch (IOException | RejectedExecutionException e) { + } catch (Throwable t) { if (isRunning()) { throw new RuntimeException( - "Unexpected exception while accepting connections", e); + "Unexpected exception while accepting connections", t); } } finally { logger().log(INFO, "Server stopped accepting connections at port {0}", @@ -109,6 +110,13 @@ } /* + * Called once immediately preceding the server accepting connections. + * + * Override to customize the behavior. + */ + protected void beforeAcceptingConnections() { } + + /* * A "Template Method" describing how a connection (represented by a socket) * is handled. * @@ -240,12 +248,25 @@ /** * Returns the local port this server is listening at. * + * This method can be called at any time. + * * @return the port this server is listening at */ public int getPort() { return serverSocket.getLocalPort(); } + /** + * Returns the address this server is listening at. + * + * This method can be called at any time. + * + * @return the address + */ + public InetAddress getInetAddress() { + return serverSocket.getInetAddress(); + } + /* * Returns a flag to indicate whether this server is running or not. *