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

Maxim Thomas
20 hours ago 2a7bb9d7eda865dbf5fca3ca94a33c325e7ab6e5
opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/JDBCStatementBoundTestCase.java
@@ -52,6 +52,7 @@
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.inOrder;
@@ -100,9 +101,42 @@
         System.clearProperty(bound.property);
      }
      System.clearProperty(JDBCStorage.STATISTICS_TIMEOUT_PROPERTY);
      System.clearProperty(CachedConnection.READ_TIMEOUT_PROPERTY);
      // a static of the pool rather than a property of this storage: left standing, the bound one
      // test puts on its connections is the bound every test after it finds on them
      CachedConnection.readTimeoutMillis = CONFIGURED_READ_TIMEOUT_MILLIS;
      storage.accessMode = AccessMode.READ_ONLY; // an import test opens it for writing
   }
   /** The standing read bound as this JVM was started with it, put back after every test that varies it. */
   private static final int CONFIGURED_READ_TIMEOUT_MILLIS = CachedConnection.readTimeoutMillis;
   /**
    * A connection that keeps the read timeout it is given, the way a driver does. A mock answering
    * a fixed {@code getNetworkTimeout()} cannot tell the two apart: a backstop that reads what the
    * connection carried once and keeps it, and one that reads it again after having changed the
    * value itself - which is how the standing bound of a connection is lost for the rest of its
    * life in the pool.
    */
   private static Connection connectionCarrying(int readTimeoutMillis) throws SQLException {
      final Connection con = mock(Connection.class);
      final AtomicInteger carried = new AtomicInteger(readTimeoutMillis);
      when(con.getNetworkTimeout()).thenAnswer(new Answer<Integer>() {
         @Override
         public Integer answer(InvocationOnMock invocation) {
            return carried.get();
         }
      });
      doAnswer(new Answer<Void>() {
         @Override
         public Void answer(InvocationOnMock invocation) {
            carried.set((Integer) invocation.getArguments()[1]);
            return null;
         }
      }).when(con).setNetworkTimeout(any(Executor.class), anyInt());
      return con;
   }
   /** How long a test waits for a statement running on another thread before it fails. */
   private static final long WAIT_MILLIS = 30000;
@@ -372,6 +406,214 @@
   }
   /**
    * The other half of that, for the read bound a connection of this pool carries all its life:
    * a statement of an unbounded class takes it off for as long as it runs. The socket read
    * timeout of {@code CachedConnection.READ_TIMEOUT_PROPERTY} is armed at the login and never
    * disarmed, so a count of a populated table or the delete that empties a tree before an import
    * would die at it - and die naming no property at all, since a statement of an unbounded class
    * has none in force to name.
    */
   @Test
   public void testABulkStatementTakesTheStandingReadBoundOffTheConnection() throws Exception {
      CachedConnection.readTimeoutMillis = 90000; // as the login of this connection put it on
      final Connection con = connectionCarrying(90000);
      final PreparedStatement bulk = mock(PreparedStatement.class);
      when(bulk.getConnection()).thenReturn(con);
      when(bulk.executeUpdate()).thenReturn(1);
      storage.execute(bulk, StatementBound.BULK);
      final InOrder inOrder = inOrder(con);
      inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq(0));
      inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq(90000));
      assertEquals(con.getNetworkTimeout(), 90000, "the connection was left without the bound it came with");
   }
   /**
    * Only the bound this backend set is this backend's to take off. A read timeout standing in the
    * connection string is the deployment's own - the connect leaves it alone rather than replacing
    * it - and lifting it for a bulk statement would hand the connection back to the pool with the
    * one bound its url asked for gone.
    */
   @Test
   public void testAReadBoundOfTheConnectionStringIsNotTakenOff() throws Exception {
      CachedConnection.readTimeoutMillis = 90000;
      final JDBCBackendCfg cfg = mockCfg(JDBCBackendCfg.class);
      when(cfg.getDBDirectory()).thenReturn("jdbc:postgresql://localhost/test?socketTimeout=600");
      final JDBCStorage bounded = new JDBCStorage(cfg, null);
      final Connection con = connectionCarrying(600000);
      final PreparedStatement bulk = mock(PreparedStatement.class);
      when(bulk.getConnection()).thenReturn(con);
      when(bulk.executeUpdate()).thenReturn(1);
      bounded.execute(bulk, StatementBound.BULK);
      verify(con, never()).setNetworkTimeout(any(Executor.class), anyInt());
   }
   /**
    * A standing read bound at or under the bound of an ordinary statement is worth a word: the
    * statement dies on the socket at it instead of being cancelled at the bound of its own class -
    * which costs the connection the driver closes, and reports neither of the two properties that
    * decided it. Above that bound the two compose, the cancel of the statement coming first and
    * the standing bound staying behind it as the backstop of a cancel that is not acted upon. A
    * class carrying no bound of its own is not cut by this at all: the bound comes off for as long
    * as such a statement runs.
    */
   @Test(timeOut = 120000)
   public void testAStandingReadBoundUnderTheBoundOfAStatementCutsItShort() {
      final int backstop = (120 + JDBCStorage.BACKSTOP_MARGIN_SECONDS) * 1000;
      assertTrue(JDBCStorage.cutsStatementsShort(60000, 120), "a bound under the bound of the statement");
      assertTrue(JDBCStorage.cutsStatementsShort(120000, 120), "a bound the statement reaches at the same moment");
      // Weighed against the socket layer of that bound, not against its cancel: the catalog lookups
      // of openTree() are given no cancel at all, so what ends them is the layer a margin later -
      // and a standing bound anywhere below that ends them earlier, with the backstop arming
      // nothing on top of it because the connection already carries the tighter of the two.
      assertTrue(JDBCStorage.cutsStatementsShort(140000, 120),
         "a bound between the cancel of the statement and the socket layer behind it");
      assertTrue(JDBCStorage.cutsStatementsShort(backstop, 120), "a bound that layer reaches at the same moment");
      assertFalse(JDBCStorage.cutsStatementsShort(backstop + 1, 120), "a bound both layers come before");
      assertFalse(JDBCStorage.cutsStatementsShort(0, 120), "no standing bound at all");
      assertFalse(JDBCStorage.cutsStatementsShort(60000, 0), "a statement of an unbounded class, which is lifted");
   }
   /**
    * What a standing read bound has to stand behind is the loosest bound a statement of this
    * backend carries, not the bound of an ordinary one. The statistics refresh after an import has
    * a property of its own - ten minutes by default, and it legitimately takes as long as a scan of
    * the table it describes - so a standing bound of five cuts it on the socket, closing the
    * importer's connection under a bare class-08 state naming neither property, and the statistics
    * of #859 are then never refreshed. A bulk.timeout a deployment sets is in the same place: the
    * class is no longer lifted, so its bound is weighed like any other.
    */
   @Test(timeOut = 120000)
   public void testTheLoosestBoundOfAStatementIsWhatAStandingBoundHasToOutlive() {
      assertEquals(JDBCStorage.loosestStatementBound().property, JDBCStorage.STATISTICS_TIMEOUT_PROPERTY,
         "the statistics refresh is the loosest bound this backend gives a statement by default");
      assertEquals(JDBCStorage.loosestStatementBound().seconds, 600);
      assertTrue(JDBCStorage.cutsStatementsShort(300000, JDBCStorage.loosestStatementBound().seconds),
         "a standing bound of five minutes was not weighed against the ten of the statistics refresh");
      System.setProperty(JDBCStorage.STATISTICS_TIMEOUT_PROPERTY, "0"); // the refresh left unbounded
      assertEquals(JDBCStorage.loosestStatementBound().property, StatementBound.OPERATION.property);
      assertEquals(JDBCStorage.loosestStatementBound().seconds, 120);
      System.setProperty(StatementBound.BULK.property, "3600"); // a class the lift no longer covers
      assertEquals(JDBCStorage.loosestStatementBound().property, StatementBound.BULK.property);
      assertEquals(JDBCStorage.loosestStatementBound().seconds, 3600);
   }
   /**
    * The bound follows the connection string the pool was registered with, the way every other path
    * that names a pool does. db-directory may be changed on a running backend and the borrow still
    * leaves the pool open() registered with, so a bound resolved against the url config names now
    * would be the answer for a pool this storage never borrows from: a bulk statement of the
    * registered one would find the lift gated off and die at a bound bulk.timeout=0 promises it will
    * not meet, and the reverse pairing would lift a bound that is the deployment's own.
    * <p>
    * It is not resolved again after the change either. Read again while a lift is in flight, the
    * answer of another url would send applyBackstop() to giveBack() and re-arm the bound under the
    * statements the lift took it off for - both of them dying at it, and neither naming a property.
    */
   @Test(timeOut = 120000)
   public void testTheStandingReadBoundFollowsTheUrlThePoolWasRegisteredWith() throws Exception {
      CachedConnection.readTimeoutMillis = 90000;
      final JDBCBackendCfg cfg = mockCfg(JDBCBackendCfg.class);
      when(cfg.getDBDirectory()).thenReturn("jdbc:mysql://registered/db");
      final JDBCStorage registered = openedOn(cfg);
      try {
         assertEquals(registered.standingReadBoundMillis(), 90000, "the bound of the url it registered with");
         // the configuration changed under the running backend, to a url whose own read bound is
         // the deployment's: the borrow still leaves the pool of the url above
         when(cfg.getDBDirectory()).thenReturn("jdbc:mysql://changed/db?socketTimeout=600");
         registered.applyConfigurationChange(cfg);
         assertEquals(registered.standingReadBoundMillis(), 90000,
            "the lift was decided against a pool this storage does not borrow from");
      } finally {
         registered.close();
      }
   }
   /**
    * And it is resolved while the backend opens, not at the first statement that needs it.
    * applyBackstop() is the only place production asks, and it asks only behind a statement of a
    * class carrying no bound of its own - a deployment that gives bulk.timeout a value of its own
    * has no such statement anywhere, so the word owed to an operator whose two bounds are set the
    * wrong way round would never be said at all.
    */
   @Test(timeOut = 120000)
   public void testTheStandingReadBoundIsResolvedWhileTheBackendOpens() throws Exception {
      System.setProperty(StatementBound.BULK.property, "3600"); // no statement of an unbounded class anywhere
      CachedConnection.readTimeoutMillis = 90000;
      final JDBCBackendCfg cfg = mockCfg(JDBCBackendCfg.class);
      when(cfg.getDBDirectory()).thenReturn("jdbc:mysql://resolved-at-open/db");
      final JDBCStorage opened = openedOn(cfg);
      try {
         // what the answer would be if it were resolved now, on the first statement to ask
         CachedConnection.readTimeoutMillis = 37000;
         assertEquals(opened.standingReadBoundMillis(), 90000,
            "the bound was not resolved while the backend opened");
      } finally {
         opened.close();
      }
   }
   /**
    * A storage opened on a configuration, borrowing nothing from a database: open() registers the
    * pool of the url - which costs no connect - and the validating borrow of the open is answered
    * with a mock, so what is left is the registration this suite is about.
    */
   private static JDBCStorage openedOn(JDBCBackendCfg cfg) throws Exception {
      final Connection con = mock(Connection.class);
      final JDBCStorage opening = new JDBCStorage(cfg, null) {
         @Override
         Connection getConnection(boolean trusted) {
            return con;
         }
      };
      opening.open(AccessMode.READ_WRITE);
      return opening;
   }
   /**
    * What the connection carried before is remembered across the lift, not read back off the
    * connection while it is lifted: a bounded statement that outlives the bulk one takes the
    * backstop of its own class, and the standing bound - not the zero of the lift - is what goes
    * back when the last of them is through. Read again mid-flight, it would be the zero, and the
    * connection would go back to the pool with no read bound at all for the rest of its life.
    */
   @Test
   public void testTheStandingReadBoundOutlivesTheLiftAndComesBackAfterIt() throws Exception {
      System.setProperty(StatementBound.OPERATION.property, "7");
      CachedConnection.readTimeoutMillis = 90000;
      final Connection con = connectionCarrying(90000);
      final CountDownLatch bulkRunning = new CountDownLatch(1);
      final CountDownLatch bulkMayFinish = new CountDownLatch(1);
      final CountDownLatch operationRunning = new CountDownLatch(1);
      final CountDownLatch operationMayFinish = new CountDownLatch(1);
      final PreparedStatement bulk = lingering(con, bulkRunning, bulkMayFinish);
      final PreparedStatement operation = lingering(con, operationRunning, operationMayFinish);
      final Background clearTree = start("clear-tree", () -> storage.execute(bulk, StatementBound.BULK));
      awaitOrFail(bulkRunning, "the bulk statement never started");
      final Background entryRead = start("entry-read", () -> storage.execute(operation));
      awaitOrFail(operationRunning, "the entry read never started");
      bulkMayFinish.countDown();
      clearTree.joinOrFail();
      operationMayFinish.countDown();
      entryRead.joinOrFail();
      final InOrder inOrder = inOrder(con);
      inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq(0)); // the bulk statement takes it off
      inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq((7 + JDBCStorage.BACKSTOP_MARGIN_SECONDS) * 1000));
      inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq(90000));
      assertEquals(con.getNetworkTimeout(), 90000, "the connection was left without the bound it came with");
   }
   /**
    * The backstop belongs to the connection, not to the statement that armed it: the first
    * statement to finish must not take it away from the statements still running there.
    */