1 /*
   2  * Copyright (c) 2011, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 /*
  25  * @test
  26  * @library lib/
  27  * @run testng/othervm LdapTimeoutTest
  28  * @bug 7094377 8000487 6176036 7056489 8151678
  29  * @summary Timeout tests for ldap
  30  */
  31 
  32 import org.testng.Assert;
  33 import org.testng.annotations.BeforeTest;
  34 import org.testng.annotations.Test;
  35 
  36 import javax.naming.Context;
  37 import javax.naming.NamingException;
  38 import javax.naming.directory.InitialDirContext;
  39 import javax.naming.directory.SearchControls;
  40 import java.io.IOException;
  41 import java.io.OutputStream;
  42 import java.net.Socket;
  43 import java.util.ArrayList;
  44 import java.util.Hashtable;
  45 import java.util.List;
  46 import java.util.Objects;
  47 import java.util.concurrent.Callable;
  48 import java.util.concurrent.CompletableFuture;
  49 import java.util.concurrent.ExecutionException;
  50 import java.util.concurrent.ExecutorService;
  51 import java.util.concurrent.Executors;
  52 import java.util.concurrent.Future;
  53 import java.util.concurrent.FutureTask;
  54 import java.util.concurrent.SynchronousQueue;
  55 import java.util.concurrent.TimeUnit;
  56 import java.util.concurrent.TimeoutException;
  57 
  58 import static java.lang.String.format;
  59 import static java.util.concurrent.TimeUnit.MILLISECONDS;
  60 import static java.util.concurrent.TimeUnit.NANOSECONDS;
  61 import static org.testng.Assert.assertTrue;
  62 import static org.testng.Assert.expectThrows;
  63 
  64 public class LdapTimeoutTest {
  65 
  66     // ------ configure test timeouts here ------
  67 
  68     /*
  69      * Practical representation of "indefinite" timeout.
  70      */
  71     private static final long INFINITY_MILLIS = 20_000;
  72     /*
  73      * The lag.
  74      *
  75      * In other words, how long it takes to observe a timeout after it has
  76      * occurred. This time is provisioned for things like stack unwinding and
  77      * threads communication.
  78      */
  79     private static final long RIGHT_MARGIN    =  3_000;
  80     /*
  81      * The accuracy.
  82      *
  83      * Consider an activity that is supposed to take 10 seconds starts a little
  84      * bit before we start measuring those 10 seconds. So from our point of view
  85      * that 10-second activity may finish within, say, 9.7 seconds.
  86      *
  87      * Another example would be using a timed operation that returns prematurely.
  88      */
  89     private static final long LEFT_MARGIN     =    200;
  90 
  91     private static final long CONNECT_MILLIS  =  3_000;
  92     private static final long READ_MILLIS     = 10_000;
  93 
  94     static {
  95         // quick sanity check to make sure this timeouts configuration is
  96         // consistent and the timeouts do not overlap
  97         assert (LEFT_MARGIN >= 0 && RIGHT_MARGIN >= 0);
  98         assert (2 * CONNECT_MILLIS + RIGHT_MARGIN < READ_MILLIS - LEFT_MARGIN);
  99         assert (2 * CONNECT_MILLIS + READ_MILLIS + RIGHT_MARGIN < INFINITY_MILLIS - LEFT_MARGIN);
 100     }
 101 
 102     @BeforeTest
 103     public void beforeTest() {
 104         startAuxiliaryDiagnosticOutput();
 105     }
 106 
 107     /*
 108      * These are timeout tests and they are run in parallel to reduce the total
 109      * amount of run time.
 110      *
 111      * Currently it doesn't seem possible to instruct JTREG to run TestNG test
 112      * methods in parallel. That said, this JTREG test is still
 113      * a "TestNG-flavored" test for the sake of having org.testng.Assert
 114      * capability.
 115      */
 116     @Test
 117     public void test() throws Exception {
 118         List<Future<?>> futures = new ArrayList<>();
 119         ExecutorService executorService = Executors.newCachedThreadPool();
 120         try {
 121             futures.add(executorService.submit(() -> { test1(); return null; }));
 122             futures.add(executorService.submit(() -> { test2(); return null; }));
 123             futures.add(executorService.submit(() -> { test3(); return null; }));
 124             futures.add(executorService.submit(() -> { test4(); return null; }));
 125             futures.add(executorService.submit(() -> { test5(); return null; }));
 126             futures.add(executorService.submit(() -> { test6(); return null; }));
 127             futures.add(executorService.submit(() -> { test7(); return null; }));
 128         } finally {
 129             executorService.shutdown();
 130         }
 131         int failedCount = 0;
 132         for (var f : futures) {
 133             try {
 134                 f.get();
 135             } catch (ExecutionException e) {
 136                 failedCount++;
 137                 e.getCause().printStackTrace(System.out);
 138             }
 139         }
 140         if (failedCount > 0)
 141             throw new RuntimeException(failedCount + " (sub)tests failed");
 142     }
 143 
 144     static void test1() throws Exception {
 145         Hashtable<Object, Object> env = new Hashtable<>();
 146         env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
 147         // Here and in the other tests it's important to close the server as
 148         // calling `thread.interrupt` from assertion may not be enough
 149         // (depending on where the blocking call has stuck)
 150         try (TestServer server = new NotBindableServer()) {
 151             env.put(Context.PROVIDER_URL, "ldap://localhost:" + server.getPort());
 152             server.start();
 153             // Here and in the other tests joining done purely to reduce timing
 154             // jitter. Commenting out or removing that should not make the test
 155             // incorrect. (ServerSocket can accept connection as soon as it is
 156             // bound, not need to call `accept` before that.)
 157             server.starting().join();
 158             assertIncompletion(INFINITY_MILLIS, () -> new InitialDirContext(env));
 159         }
 160     }
 161 
 162     static void test2() throws Exception {
 163         Hashtable<Object, Object> env = new Hashtable<>();
 164         env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
 165         env.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(CONNECT_MILLIS));
 166         try (TestServer server = new BindableButNotReadableServer()) {
 167             env.put(Context.PROVIDER_URL, "ldap://localhost:" + server.getPort());
 168             server.start();
 169             server.starting().join();
 170             InitialDirContext ctx = new InitialDirContext(env);
 171             SearchControls scl = new SearchControls();
 172             scl.setSearchScope(SearchControls.SUBTREE_SCOPE);
 173             assertIncompletion(INFINITY_MILLIS,
 174                                () -> ctx.search("ou=People,o=JNDITutorial", "(objectClass=*)", scl));
 175         }
 176     }
 177 
 178     static void test3() throws Exception {
 179         Hashtable<Object, Object> env = new Hashtable<>();
 180         env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
 181         try (TestServer server = new BindableButNotReadableServer()) {
 182             env.put(Context.PROVIDER_URL, "ldap://localhost:" + server.getPort());
 183             server.start();
 184             server.starting().join();
 185             InitialDirContext ctx = new InitialDirContext(env);
 186             SearchControls scl = new SearchControls();
 187             scl.setSearchScope(SearchControls.SUBTREE_SCOPE);
 188             assertIncompletion(INFINITY_MILLIS,
 189                                () -> ctx.search("ou=People,o=JNDITutorial", "(objectClass=*)", scl));
 190         }
 191     }
 192 
 193     static void test4() throws Exception {
 194         Hashtable<Object, Object> env = new Hashtable<>();
 195         env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
 196         env.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(CONNECT_MILLIS));
 197         env.put("com.sun.jndi.ldap.read.timeout", String.valueOf(READ_MILLIS));
 198         try (TestServer server = new NotBindableServer()) {
 199             env.put(Context.PROVIDER_URL, "ldap://localhost:" + server.getPort());
 200             server.start();
 201             server.starting().join();
 202             Assert.ThrowingRunnable completion =
 203                     () -> assertCompletion(CONNECT_MILLIS - LEFT_MARGIN,
 204                                            2 * CONNECT_MILLIS + RIGHT_MARGIN,
 205                                            () -> new InitialDirContext(env));
 206             NamingException e = expectThrows(NamingException.class, completion);
 207             String msg = e.getMessage();
 208             assertTrue(msg != null && msg.contains("timeout")
 209                                && msg.contains(String.valueOf(CONNECT_MILLIS)),
 210                        msg);
 211         }
 212     }
 213 
 214     static void test5() throws Exception {
 215         Hashtable<Object, Object> env = new Hashtable<>();
 216         env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
 217         env.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(CONNECT_MILLIS));
 218         env.put("com.sun.jndi.ldap.read.timeout", String.valueOf(READ_MILLIS));
 219         try (TestServer server = new BindableButNotReadableServer()) {
 220             env.put(Context.PROVIDER_URL, "ldap://localhost:" + server.getPort());
 221             server.start();
 222             server.starting().join();
 223             InitialDirContext ctx = new InitialDirContext(env);
 224             SearchControls scl = new SearchControls();
 225             scl.setSearchScope(SearchControls.SUBTREE_SCOPE);
 226             Assert.ThrowingRunnable completion =
 227                     () -> assertCompletion(READ_MILLIS - LEFT_MARGIN,
 228                                            READ_MILLIS + RIGHT_MARGIN,
 229                                            () -> ctx.search("ou=People,o=JNDITutorial", "(objectClass=*)", scl));
 230             NamingException e = expectThrows(NamingException.class, completion);
 231             String msg = e.getMessage();
 232             assertTrue(msg != null && msg.contains("timeout")
 233                                && msg.contains(String.valueOf(READ_MILLIS)),
 234                        msg);
 235         }
 236     }
 237 
 238     static void test6() throws Exception {
 239         Hashtable<Object, Object> env = new Hashtable<>();
 240         env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
 241         env.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(CONNECT_MILLIS));
 242         env.put("com.sun.jndi.ldap.read.timeout", String.valueOf(READ_MILLIS));
 243         try (TestServer server = new NotBindableServer()) {
 244             env.put(Context.PROVIDER_URL, "ldap://localhost:" + server.getPort());
 245             server.start();
 246             server.starting().join();
 247             Assert.ThrowingRunnable completion =
 248                     () -> assertCompletion(CONNECT_MILLIS - LEFT_MARGIN,
 249                                            2 * CONNECT_MILLIS + RIGHT_MARGIN,
 250                                            () -> new InitialDirContext(env));
 251             NamingException e = expectThrows(NamingException.class, completion);
 252             String msg = e.getMessage();
 253             assertTrue(msg != null && msg.contains("timeout")
 254                                && msg.contains(String.valueOf(CONNECT_MILLIS)),
 255                        msg);
 256         }
 257     }
 258 
 259     static void test7() throws Exception {
 260         // 8000487: Java JNDI connection library on ldap conn is
 261         // not honoring configured timeout
 262         Hashtable<Object, Object> env = new Hashtable<>();
 263         env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
 264         env.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(CONNECT_MILLIS));
 265         env.put("com.sun.jndi.ldap.read.timeout", String.valueOf(READ_MILLIS));
 266         env.put(Context.SECURITY_AUTHENTICATION, "simple");
 267         env.put(Context.SECURITY_PRINCIPAL, "user");
 268         env.put(Context.SECURITY_CREDENTIALS, "password");
 269         try (TestServer server = new NotBindableServer()) {
 270             env.put(Context.PROVIDER_URL, "ldap://localhost:" + server.getPort());
 271             server.start();
 272             server.starting().join();
 273             Assert.ThrowingRunnable completion =
 274                     () -> assertCompletion(CONNECT_MILLIS - LEFT_MARGIN,
 275                                            2 * CONNECT_MILLIS + RIGHT_MARGIN,
 276                                            () -> new InitialDirContext(env));
 277             NamingException e = expectThrows(NamingException.class, completion);
 278             String msg = e.getMessage();
 279             assertTrue(msg != null && msg.contains("timeout")
 280                                && msg.contains(String.valueOf(CONNECT_MILLIS)),
 281                        msg);
 282         }
 283     }
 284 
 285     // ------ test stub servers ------
 286 
 287     static class TestServer extends BaseLdapServer {
 288 
 289         private final CompletableFuture<Void> starting = new CompletableFuture<>();
 290 
 291         TestServer() throws IOException { }
 292 
 293         @Override
 294         protected void beforeAcceptingConnections() {
 295             starting.completeAsync(() -> null);
 296         }
 297 
 298         public CompletableFuture<Void> starting() {
 299             return starting.copy();
 300         }
 301     }
 302 
 303     static class BindableButNotReadableServer extends TestServer {
 304 
 305         BindableButNotReadableServer() throws IOException { }
 306 
 307         private static final byte[] bindResponse = {
 308                 0x30, 0x0C, 0x02, 0x01, 0x01, 0x61, 0x07, 0x0A,
 309                 0x01, 0x00, 0x04, 0x00, 0x04, 0x00
 310         };
 311 
 312         @Override
 313         protected void handleRequest(Socket socket,
 314                                      LdapMessage msg,
 315                                      OutputStream out)
 316                 throws IOException {
 317             switch (msg.getOperation()) {
 318                 case BIND_REQUEST:
 319                     out.write(bindResponse);
 320                     out.flush();
 321                 default:
 322                     break;
 323             }
 324         }
 325     }
 326 
 327     static class NotBindableServer extends TestServer {
 328 
 329         NotBindableServer() throws IOException { }
 330 
 331         @Override
 332         protected void beforeConnectionHandled(Socket socket) {
 333             try {
 334                 TimeUnit.DAYS.sleep(Integer.MAX_VALUE);
 335             } catch (InterruptedException e) {
 336                 Thread.currentThread().interrupt();
 337             }
 338         }
 339     }
 340 
 341     // ------ timeouts check utilities ------
 342 
 343     /*
 344      * Asserts that the specified executable yields a result or an exception
 345      * within the specified time frame. Interrupts the executable
 346      * unconditionally.
 347      *
 348      * If the executable yields a result or an exception within the specified
 349      * time frame, the result will be returned and the exception will be
 350      * rethrown respectively in a transparent fashion as if the executable was
 351      * executed directly.
 352      */
 353     public static <T> T assertCompletion(long loMillis,
 354                                          long hiMillis,
 355                                          Callable<T> code)
 356             throws Throwable {
 357         if (loMillis < 0 || hiMillis < 0 || loMillis > hiMillis) {
 358             throw new IllegalArgumentException("loMillis=" + loMillis +
 359                                                        ", hiMillis=" + hiMillis);
 360         }
 361         Objects.requireNonNull(code);
 362 
 363         // this queue acts both as an exchange point and a barrier
 364         SynchronousQueue<Long> startTime = new SynchronousQueue<>();
 365 
 366         Callable<T> wrappedTask = () -> {
 367             // by the time this value reaches the "stopwatch" thread it might be
 368             // well outdated and that's okay, we will adjust the wait time
 369             startTime.put(System.nanoTime());
 370             return code.call();
 371         };
 372 
 373         FutureTask<T> task = new FutureTask<>(wrappedTask);
 374         Thread t = new Thread(task);
 375         t.start();
 376 
 377         final long startNanos;
 378         try {
 379             startNanos = startTime.take(); // (1) wait for the initial time mark
 380         } catch (Throwable e) {
 381             t.interrupt();
 382             throw e;
 383         }
 384 
 385         final long waitTime = hiMillis -
 386                 NANOSECONDS.toMillis(System.nanoTime() - startNanos); // (2) adjust wait time
 387 
 388         try {
 389             T r = task.get(waitTime, MILLISECONDS); // (3) wait for the task to complete
 390             long elapsed = NANOSECONDS.toMillis(System.nanoTime() - startNanos);
 391             if (elapsed < loMillis || elapsed > hiMillis) {
 392                 throw new RuntimeException(format(
 393                         "After %s ms. returned result '%s'", elapsed, r));
 394             }
 395             return r;
 396         } catch (ExecutionException e) {
 397             long elapsed = NANOSECONDS.toMillis(System.nanoTime() - startNanos);
 398             if (elapsed < loMillis || elapsed > hiMillis) {
 399                 throw new RuntimeException(format(
 400                         "After %s ms. thrown exception", elapsed), e);
 401             }
 402             throw e.getCause();
 403         } catch (TimeoutException e) {
 404             // We trust timed get not to throw TimeoutException prematurely
 405             // (i.e. before the wait time elapses)
 406             long elapsed = NANOSECONDS.toMillis(System.nanoTime() - startNanos);
 407             throw new RuntimeException(format(
 408                     "After %s ms. is incomplete", elapsed));
 409         } finally {
 410             t.interrupt();
 411         }
 412     }
 413 
 414     /*
 415      * Asserts that the specified executable yields no result and no exception
 416      * for at least the specified amount of time. Interrupts the executable
 417      * unconditionally.
 418      */
 419     public static void assertIncompletion(long millis, Callable<?> code)
 420             throws Exception
 421     {
 422         if (millis < 0) {
 423             throw new IllegalArgumentException("millis=" + millis);
 424         }
 425         Objects.requireNonNull(code);
 426 
 427         // this queue acts both as an exchange point and a barrier
 428         SynchronousQueue<Long> startTime = new SynchronousQueue<>();
 429 
 430         Callable<?> wrappedTask = () -> {
 431             // by the time this value reaches the "stopwatch" thread it might be
 432             // well outdated and that's okay, we will adjust the wait time
 433             startTime.put(System.nanoTime());
 434             return code.call();
 435         };
 436 
 437         FutureTask<?> task = new FutureTask<>(wrappedTask);
 438         Thread t = new Thread(task);
 439         t.start();
 440 
 441         final long startNanos;
 442         try {
 443             startNanos = startTime.take(); // (1) wait for the initial time mark
 444         } catch (Throwable e) {
 445             t.interrupt();
 446             throw e;
 447         }
 448 
 449         final long waitTime = millis -
 450                 NANOSECONDS.toMillis(System.nanoTime() - startNanos); // (2) adjust wait time
 451 
 452         try {
 453             Object r = task.get(waitTime, MILLISECONDS); // (3) wait for the task to complete
 454             long elapsed = NANOSECONDS.toMillis(System.nanoTime() - startNanos);
 455             if (elapsed < waitTime) {
 456                 throw new RuntimeException(format(
 457                         "After %s ms. returned result '%s'", elapsed, r));
 458             }
 459         } catch (ExecutionException e) {
 460             long elapsed = NANOSECONDS.toMillis(System.nanoTime() - startNanos);
 461             if (elapsed < waitTime) {
 462                 throw new RuntimeException(format(
 463                         "After %s ms. thrown exception", elapsed), e);
 464             }
 465         } catch (TimeoutException expected) {
 466         } finally {
 467             t.interrupt();
 468         }
 469     }
 470 
 471     // ------ miscellaneous utilities ------
 472 
 473     /*
 474      * A diagnostic aid that might help with debugging timeout issues. The idea
 475      * is to continuously measure accuracy and responsiveness of the system that
 476      * runs this test. If the system is overwhelmed (with something else), it
 477      * might affect the test run. At the very least we will have traces of that
 478      * in the logs.
 479      *
 480      * This utility does not automatically scale up test timeouts, it simply
 481      * gathers information.
 482      */
 483     private static void startAuxiliaryDiagnosticOutput() {
 484         System.out.printf("Starting diagnostic output (probe)%n");
 485         Thread t = new Thread(() -> {
 486             for (int i = 0; ; i = ((i % 20) + 1)) {
 487                 // 500, 1_000, 1_500, ..., 9_500, 10_000, 500, 1_000, ...
 488                 long expected = i * 500;
 489                 long start = System.nanoTime();
 490                 try {
 491                     MILLISECONDS.sleep(expected);
 492                 } catch (InterruptedException e) {
 493                     return;
 494                 }
 495                 long stop = System.nanoTime();
 496                 long actual = NANOSECONDS.toMillis(stop - start);
 497                 System.out.printf("(probe) expected [ms.]: %s, actual [ms.]: %s%n",
 498                                   expected, actual);
 499 
 500             }
 501         }, "probe");
 502         t.setDaemon(true);
 503         t.start();
 504     }
 505 }