mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valery Kharseko
2 days ago 0d16f10774cdc7bc96bfea7f007c7823e650795d
opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/CachedConnectionTestCase.java
@@ -19,8 +19,14 @@
import org.testng.annotations.AfterClass;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.sql.Connection;
@@ -52,11 +58,13 @@
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNotSame;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertSame;
import static org.testng.Assert.assertTrue;
@@ -83,6 +91,9 @@
   private final StubDriver stub = new StubDriver();
   /** The window as this JVM was started with it, put back after every test that varies it. */
   private static final long CONFIGURED_ALIVE_BYPASS_NANOS = CachedConnection.aliveBypassNanos;
   @BeforeClass
   public void registerStubDriver() throws Exception {
      DriverManager.registerDriver(stub);
@@ -93,13 +104,26 @@
      DriverManager.deregisterDriver(stub);
   }
   /**
    * Most of the tests below seed the pool by hand, and a connection built a moment ago is inside
    * the alive window - they are about what the validation of a borrow does, so the window is off
    * unless the test at hand is one of the window's own.
    */
   @BeforeMethod
   public void validateEveryBorrow() {
      CachedConnection.aliveBypassNanos = 0;
   }
   @AfterMethod
   public void clearProperties() {
      System.clearProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
      System.clearProperty(CachedConnection.POOL_TIMEOUT_PROPERTY);
      System.clearProperty(CachedConnection.TTL_PROPERTY);
      System.clearProperty(CachedConnection.ALIVE_BYPASS_PROPERTY);
      // what has been reported once is remembered for the life of the jvm: left standing, the key
      // of one test is what the next one finds when it asserts that it reported something itself
      CachedConnection.warnedOnce.clear();
      CachedConnection.aliveBypassNanos = CONFIGURED_ALIVE_BYPASS_NANOS;
   }
   /**
@@ -546,7 +570,7 @@
      final Connection pooled = mock(Connection.class);
      when(pooled.isValid(anyInt())).thenReturn(true);
      when(pooled.getNetworkTimeout()).thenReturn(0);
      CachedConnection.cached.get(url).add(new CachedConnection(url, pooled));
      seedPool(url, pooled);
      final Connection borrowed = CachedConnection.getConnection(url);
@@ -572,7 +596,7 @@
      when(pooled.isValid(anyInt())).thenReturn(true);
      doThrow(new SQLException("the driver took the bound and then failed"))
         .when(pooled).setNetworkTimeout(any(Executor.class), anyInt());
      CachedConnection.cached.get(url).add(new CachedConnection(url, pooled));
      seedPool(url, pooled);
      final Connection fresh = mock(Connection.class);
      stub.answerWith(fresh);
@@ -615,7 +639,7 @@
      final Connection pooled = mock(Connection.class);
      when(pooled.isValid(anyInt())).thenReturn(true);
      when(pooled.getNetworkTimeout()).thenReturn(2000);
      CachedConnection.cached.get(url).add(new CachedConnection(url, pooled));
      seedPool(url, pooled);
      assertNotNull(CachedConnection.getConnection(url));
@@ -639,7 +663,7 @@
            Thread.sleep(500); // a database that no longer answers: every validation waits out its bound
            return false;
         });
         CachedConnection.cached.get(url).add(new CachedConnection(url, stale));
         seedPool(url, stale);
      }
      final Connection fresh = mock(Connection.class);
      stub.answerWith(fresh);
@@ -658,7 +682,7 @@
      final String url = StubDriver.PREFIX + "broken-pooled";
      final Connection stale = mock(Connection.class);
      when(stale.isValid(anyInt())).thenReturn(false);
      CachedConnection.cached.get(url).add(new CachedConnection(url, stale));
      seedPool(url, stale);
      final Connection fresh = mock(Connection.class);
      when(fresh.isValid(anyInt())).thenReturn(true);
      stub.answerWith(fresh);
@@ -682,7 +706,7 @@
      final String url = StubDriver.PREFIX + "validation-unchecked";
      final Connection broken = mock(Connection.class);
      when(broken.isValid(anyInt())).thenThrow(new IllegalStateException("driver internal"));
      CachedConnection.cached.get(url).add(new CachedConnection(url, broken));
      seedPool(url, broken);
      final Connection fresh = mock(Connection.class);
      stub.answerWith(fresh);
@@ -1273,8 +1297,7 @@
      });
      final Connection good = mock(Connection.class);
      when(good.isValid(anyInt())).thenReturn(true);
      CachedConnection.cached.get(url).add(new CachedConnection(url, stale));
      CachedConnection.cached.get(url).add(new CachedConnection(url, good));
      seedPool(url, stale, good);
      final Connection fresh = mock(Connection.class);
      stub.answerWith(fresh);
      System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "1");
@@ -1300,7 +1323,7 @@
      final Connection reaped = mock(Connection.class);
      when(reaped.getNetworkTimeout()).thenReturn(0);
      when(reaped.isValid(anyInt())).thenReturn(false);
      CachedConnection.cached.get(url).add(new CachedConnection(url, reaped));
      seedPool(url, reaped);
      final Connection fresh = mock(Connection.class);
      stub.answerWith(fresh);
@@ -1333,6 +1356,387 @@
         "a connection still carrying the read bound of its login went back into the pool");
   }
   /**
    * The case the window exists for: the connection this borrow takes out answered the database a
    * moment ago, and asking it again costs the round trip the operation came to make.
    */
   @Test(timeOut = 120000)
   public void testAConnectionProvenAliveIsNotValidatedAgainWithinTheWindow() throws Exception {
      final String url = StubDriver.PREFIX + "within-the-window";
      CachedConnection.aliveBypassNanos = TimeUnit.HOURS.toNanos(1);
      final Connection parent = mock(Connection.class);
      when(parent.isValid(anyInt())).thenReturn(true);
      stub.answerWith(parent);
      final Connection first = CachedConnection.getConnection(url); // established: it has just answered
      first.close();
      final Connection second = CachedConnection.getConnection(url);
      assertSame(second, first, "the pooled connection was not the one handed back");
      assertEquals(stub.attempts.get(), 1, "the pool established a second connection");
      verify(parent, never()).isValid(anyInt());
   }
   /** Past the window it is the connection the database or a firewall may have dropped meanwhile. */
   @Test(timeOut = 120000)
   public void testAConnectionIsValidatedAgainOnceTheWindowHasPassed() throws Exception {
      final String url = StubDriver.PREFIX + "past-the-window";
      CachedConnection.aliveBypassNanos = TimeUnit.MILLISECONDS.toNanos(1);
      final Connection parent = mock(Connection.class);
      when(parent.isValid(anyInt())).thenReturn(true);
      stub.answerWith(parent);
      final Connection first = CachedConnection.getConnection(url);
      first.close();
      Thread.sleep(20);
      final Connection second = CachedConnection.getConnection(url);
      assertSame(second, first);
      verify(parent).isValid(CachedConnection.VALIDATION_TIMEOUT_SECONDS);
   }
   /** The window switched off validates every borrow, the way this pool did before it existed. */
   @Test(timeOut = 120000)
   public void testAWindowOfZeroValidatesEveryBorrow() throws Exception {
      final String url = StubDriver.PREFIX + "window-of-zero";
      CachedConnection.aliveBypassNanos = 0;
      final Connection parent = mock(Connection.class);
      when(parent.isValid(anyInt())).thenReturn(true);
      stub.answerWith(parent);
      final Connection first = CachedConnection.getConnection(url);
      first.close();
      final Connection second = CachedConnection.getConnection(url);
      assertSame(second, first);
      verify(parent).isValid(CachedConnection.VALIDATION_TIMEOUT_SECONDS);
   }
   /**
    * A connection is trusted for the window that follows the last answer it gave, never for the
    * window that follows its return to the pool. pgjdbc short-circuits both rollback() and
    * commit() when the transaction state is IDLE, so a borrow that issued no statement - the open
    * of a backend, a configuration change that leaves the base DNs alone, an import of nothing -
    * puts a connection back without a byte reaching the server: stamping the return would mark a
    * connection the database dropped meanwhile as the freshest one in the pool.
    */
   @Test(timeOut = 120000)
   public void testTheReturnToThePoolIsNotTakenForProofOfLife() throws Exception {
      final String url = StubDriver.PREFIX + "silent-return";
      CachedConnection.aliveBypassNanos = TimeUnit.MILLISECONDS.toNanos(50);
      final Connection dropped = mock(Connection.class);
      when(dropped.isValid(anyInt())).thenReturn(false); // dropped while it was out of the pool
      stub.answerWith(dropped);
      final Connection borrowed = CachedConnection.getConnection(url);
      Thread.sleep(80); // the answer of the login ages out of the window
      borrowed.close(); // and the rollback of this return never leaves the driver
      final Connection fresh = mock(Connection.class);
      when(fresh.isValid(anyInt())).thenReturn(true);
      stub.answerWith(fresh);
      final Connection next = CachedConnection.getConnection(url);
      verify(dropped).isValid(CachedConnection.VALIDATION_TIMEOUT_SECONDS);
      verify(dropped).close();
      assertSame(((CachedConnection) next).parent, fresh,
         "a connection the database dropped was handed out on the strength of its return to the pool");
   }
   /**
    * A proof taken while the database was going away does not outlive the distrust that reported it.
    * <p>
    * The stamp stands for the moment the connection was asked, not the moment its answer was filed:
    * a validation is given {@link CachedConnection#VALIDATION_TIMEOUT_SECONDS}, and one that started
    * before another operation reported a drop and returned after it would otherwise be the younger of
    * the two. The connection would then be handed out unvalidated for the rest of the window - and
    * LIFO puts it at the head of the deque, so it is the very one the next borrow takes - by the
    * check that exists to stop exactly that.
    */
   @Test(timeOut = 120000)
   public void testAProofTakenWhileTheDatabaseWentAwayIsNotTrusted() throws Exception {
      final String url = StubDriver.PREFIX + "proof-across-a-drop";
      CachedConnection.aliveBypassNanos = TimeUnit.HOURS.toNanos(1); // nothing here ages out of the window
      final AtomicInteger validations = new AtomicInteger();
      final Connection parent = mock(Connection.class);
      // the database goes away while this validation is in flight: another operation of the backend
      // reports the drop of its own connection before this one has answered
      when(parent.isValid(anyInt())).thenAnswer(invocation -> {
         CachedConnection.distrustPool(url);
         validations.incrementAndGet();
         return true;
      });
      stub.answerWith(parent);
      CachedConnection.getConnection(url).close(); // established and returned, proven by its login
      CachedConnection.distrustPool(url);          // an operation reports a drop: what the pool holds predates it
      CachedConnection.getConnection(url).close(); // validated, and a second drop is reported while it is
      final Connection borrowed = CachedConnection.getConnection(url);
      assertEquals(validations.get(), 2,
         "a connection was trusted on a proof that started before the drop it is compared against");
      assertSame(((CachedConnection) borrowed).parent, parent, "the connection answered and was still discarded");
   }
   /**
    * The pool hands out the connection returned last. Without it the window would rarely apply: a
    * connection reached only after a whole cycle of the pool has been idle far longer than it.
    */
   @Test(timeOut = 120000)
   public void testTheConnectionReturnedLastIsBorrowedFirst() throws Exception {
      final String url = StubDriver.PREFIX + "returned-last";
      CachedConnection.aliveBypassNanos = TimeUnit.HOURS.toNanos(1);
      final Connection older = mock(Connection.class);
      when(older.isValid(anyInt())).thenReturn(true);
      stub.answerWith(older);
      final Connection first = CachedConnection.getConnection(url);
      final Connection newer = mock(Connection.class);
      when(newer.isValid(anyInt())).thenReturn(true);
      stub.answerWith(newer);
      final Connection second = CachedConnection.getConnection(url);
      assertNotSame(second, first);
      first.close();
      second.close();
      final Connection borrowed = CachedConnection.getConnection(url);
      assertSame(((CachedConnection) borrowed).parent, newer, "the pool cycled round to its coldest connection");
   }
   /**
    * Whatever dropped one connection - a restart, a failover, a network that went away - dropped
    * every connection established before it, and a borrow inside the window asks the database
    * nothing: so the operation that saw the failure tells the pool, and the rest of that
    * generation is validated once before it is trusted again.
    */
   @Test(timeOut = 120000)
   public void testThePoolIsValidatedAgainAfterTheDatabaseDroppedAConnection() throws Exception {
      final String url = StubDriver.PREFIX + "distrusted-generation";
      CachedConnection.aliveBypassNanos = TimeUnit.HOURS.toNanos(1);
      final Connection parent = mock(Connection.class);
      when(parent.isValid(anyInt())).thenReturn(true);
      stub.answerWith(parent);
      CachedConnection.getConnection(url).close();
      CachedConnection.distrustPool(url);
      final Connection next = CachedConnection.getConnection(url);
      next.close();
      CachedConnection.getConnection(url).close();
      // once for the generation the drop condemned, and not again for the borrow behind it
      verify(parent, times(1)).isValid(CachedConnection.VALIDATION_TIMEOUT_SECONDS);
   }
   /**
    * The whole of the trade the window makes, end to end: a connection the database dropped
    * inside the window is handed out unvalidated - that is the cost - the operation it broke
    * reports the drop, and from there the pool validates the generation the drop condemned
    * instead of handing out the rest of it the same way.
    */
   @Test(timeOut = 120000)
   public void testAConnectionDroppedInsideTheWindowIsHandedOutOnceAndThenValidated() throws Exception {
      final String url = StubDriver.PREFIX + "dropped-inside-the-window";
      CachedConnection.aliveBypassNanos = TimeUnit.HOURS.toNanos(1);
      final Connection parent = mock(Connection.class);
      when(parent.isValid(anyInt())).thenReturn(true);
      stub.answerWith(parent);
      CachedConnection.getConnection(url).close();
      when(parent.isValid(anyInt())).thenReturn(false); // the database dropped it where it lay
      final Connection dropped = CachedConnection.getConnection(url);
      assertSame(((CachedConnection) dropped).parent, parent, "the pooled connection was not the one handed back");
      verify(parent, never()).isValid(anyInt()); // handed out on the strength of its last answer
      // the statement of the caller is where the drop surfaces, and the caller reports it
      CachedConnection.distrustPool(url);
      dropped.close();
      final Connection fresh = mock(Connection.class);
      when(fresh.isValid(anyInt())).thenReturn(true);
      stub.answerWith(fresh);
      final Connection next = CachedConnection.getConnection(url);
      verify(parent).isValid(CachedConnection.VALIDATION_TIMEOUT_SECONDS);
      verify(parent).close();
      assertSame(((CachedConnection) next).parent, fresh, "the rest of the generation was handed out unvalidated");
   }
   /**
    * Seeds the pool the way {@link CachedConnection#close()} fills it - at the end a borrow takes
    * from - so that the connection named first here is the one the next borrow gets.
    */
   private static void seedPool(String url, Connection... parents) {
      for (int i = parents.length - 1; i >= 0; i--) {
         CachedConnection.cached.get(url).addFirst(new CachedConnection(url, parents[i]));
      }
   }
   /**
    * The borrows nothing compensates a dropped connection on - the open of a backend, the removal
    * of its files, the start of an import - ask for a connection the pool validates whatever the
    * window says. Each of them is one borrow of a cold path, and the one that opens a backend
    * issues no statement at all: a connection dropped inside the window would surface there out of
    * the rollback that releases it, with no statement to replay and nothing to tell the pool.
    */
   @Test(timeOut = 120000)
   public void testTheBorrowsNothingCompensatesAreValidated() throws Exception {
      final String url = StubDriver.PREFIX + "validated-borrow";
      CachedConnection.aliveBypassNanos = TimeUnit.HOURS.toNanos(1);
      final Connection parent = mock(Connection.class);
      when(parent.isValid(anyInt())).thenReturn(true);
      stub.answerWith(parent);
      CachedConnection.getConnection(url).close();
      final Connection borrowed = CachedConnection.getConnection(url, false);
      assertSame(((CachedConnection) borrowed).parent, parent, "the pooled connection was not the one handed back");
      verify(parent).isValid(CachedConnection.VALIDATION_TIMEOUT_SECONDS);
   }
   /**
    * A connection closed under the borrow is not handed out on the strength of its last answer:
    * the removal listener of the pool closes every connection it finds in the deque when the pool
    * expires, and it iterates a weakly consistent view. The validation the window replaces
    * answered that as well, out of a flag of the driver rather than out of a round trip.
    */
   @Test(timeOut = 120000)
   public void testAConnectionThePoolClosedIsNotHandedOut() throws Exception {
      final String url = StubDriver.PREFIX + "closed-inside-the-window";
      CachedConnection.aliveBypassNanos = TimeUnit.HOURS.toNanos(1);
      final Connection parent = mock(Connection.class);
      when(parent.isValid(anyInt())).thenReturn(true);
      stub.answerWith(parent);
      CachedConnection.getConnection(url).close();
      // closed where it lay, by the expiry of the pool: a closed connection answers isValid() with
      // false as well, which is what discards it once the window stops trusting it
      when(parent.isClosed()).thenReturn(true);
      when(parent.isValid(anyInt())).thenReturn(false);
      final Connection fresh = mock(Connection.class);
      when(fresh.isValid(anyInt())).thenReturn(true);
      stub.answerWith(fresh);
      final Connection borrowed = CachedConnection.getConnection(url);
      assertSame(((CachedConnection) borrowed).parent, fresh, "a closed connection was handed out on its last answer");
   }
   /**
    * The window is clamped twice: to the idle time the pool keeps a connection for, since a window
    * longer than that is one the pool can never back - the connection it was meant for is gone
    * before it closes - and to {@link CachedConnection#MAX_ALIVE_BYPASS_MS} behind it, since the
    * ttl has no upper bound of its own and a value the unit conversion saturates on would leave
    * every connection of the pool trusted for the life of the server.
    * <p>
    * About the value the class settles on at initialization: the field the pool reads is assigned
    * once, so a ttl set after that changes neither it nor the idle time it was clamped to.
    */
   @Test(timeOut = 120000)
   public void testTheWindowIsClampedToTheIdleTimeOfThePool() {
      System.setProperty(CachedConnection.TTL_PROPERTY, "15000");
      System.setProperty(CachedConnection.ALIVE_BYPASS_PROPERTY, "500");
      assertEquals(CachedConnection.getAliveBypassMillis(), 500L, "a window inside the ttl was not left alone");
      System.setProperty(CachedConnection.ALIVE_BYPASS_PROPERTY, Long.toString(Long.MAX_VALUE));
      assertEquals(CachedConnection.getAliveBypassMillis(), 15000L, "a window of Long.MAX_VALUE was not clamped");
      System.setProperty(CachedConnection.TTL_PROPERTY, "100");
      assertEquals(CachedConnection.getAliveBypassMillis(), 100L, "the clamp is the configured ttl, not the default");
      // the ttl has no upper bound of its own, so the clamp to it does not bound the window either: both set
      // to a value the conversion to nanoseconds saturates on would leave every connection of the pool
      // trusted for the life of the server, which is the outcome this javadoc says the clamp rules out
      System.setProperty(CachedConnection.TTL_PROPERTY, Long.toString(Long.MAX_VALUE));
      System.setProperty(CachedConnection.ALIVE_BYPASS_PROPERTY, Long.toString(Long.MAX_VALUE));
      assertEquals(CachedConnection.getAliveBypassMillis(), CachedConnection.MAX_ALIVE_BYPASS_MS,
         "a window the ttl did not bound was left to saturate");
   }
   /**
    * A setting worth warning about must leave the class usable. Both properties are read by the
    * initializer of {@code aliveBypassNanos}, and both report an unusable value through the set of
    * what has been said once already - which, declared below that initializer, would still be null
    * when the initializer reaches it (JLS 12.4.2). A window longer than the ttl is exactly the
    * tuning the javadoc of the property invites, and it would turn a log line into an
    * ExceptionInInitializerError on the first borrow and a causeless NoClassDefFoundError on every
    * one after it: no connection can be borrowed, so the backend cannot open at all.
    * <p>
    * Asserted on the class loaded afresh rather than on this one, which was initialized long
    * before the property was set.
    */
   @Test(timeOut = 120000, dataProvider = "settingsWorthWarningAbout")
   public void testASettingWorthWarningAboutStillInitializesTheClass(String ttl, String window, long expectedMillis)
         throws Exception {
      System.setProperty(CachedConnection.TTL_PROPERTY, ttl);
      System.setProperty(CachedConnection.ALIVE_BYPASS_PROPERTY, window);
      final Class<?> reloaded = loadedAfresh(CachedConnection.class);
      assertNotSame(reloaded, CachedConnection.class, "the class under test was not loaded afresh");
      final Field field = reloaded.getDeclaredField("aliveBypassNanos");
      field.setAccessible(true);
      assertEquals(field.getLong(null), TimeUnit.MILLISECONDS.toNanos(expectedMillis),
         "the window the reloaded class settled on");
   }
   @DataProvider
   public Object[][] settingsWorthWarningAbout() {
      return new Object[][]{
         // a window longer than the ttl: reported once and used as the ttl
         {"15000", "60000", 15000L},
         // not a number, and negative: reported once and ignored in favour of the default
         {"15000", "half a second", CachedConnection.DEFAULT_ALIVE_BYPASS_MS},
         {"15000", "-1", CachedConnection.DEFAULT_ALIVE_BYPASS_MS},
         // the ttl is read by the same initializer, and reports its own value the same way
         {"30s", "500", 500L}
      };
   }
   /**
    * The class again, defined by a loader of this test rather than taken from the one that has
    * already initialized it: a static initializer runs once per loader, and this is about what it
    * does. Every other class is delegated to the parent, so the reloaded one shares the types it
    * is written against.
    */
   private static Class<?> loadedAfresh(Class<?> type) throws Exception {
      final String name = type.getName();
      final ClassLoader parent = type.getClassLoader();
      final ClassLoader loader = new ClassLoader(parent) {
         @Override
         protected Class<?> loadClass(String candidate, boolean resolve) throws ClassNotFoundException {
            if (!name.equals(candidate)) {
               return super.loadClass(candidate, resolve);
            }
            Class<?> defined = findLoadedClass(candidate);
            if (defined == null) {
               final byte[] bytecode = bytecodeOf(candidate, parent);
               defined = defineClass(candidate, bytecode, 0, bytecode.length);
            }
            if (resolve) {
               resolveClass(defined);
            }
            return defined;
         }
      };
      return Class.forName(name, true, loader);
   }
   private static byte[] bytecodeOf(String name, ClassLoader from) throws ClassNotFoundException {
      try (final InputStream in = from.getResourceAsStream(name.replace('.', '/') + ".class")) {
         if (in == null) {
            throw new ClassNotFoundException(name);
         }
         final ByteArrayOutputStream bytecode = new ByteArrayOutputStream();
         final byte[] chunk = new byte[8192];
         for (int read; (read = in.read(chunk)) >= 0; ) {
            bytecode.write(chunk, 0, read);
         }
         return bytecode.toByteArray();
      } catch (IOException e) {
         throw new ClassNotFoundException(name, e);
      }
   }
   private static SQLException tooManyConnections() {
      // 53300, too_many_connections, of the insufficient_resources class
      return new SQLException("sorry, too many clients already", "53300");