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

Valery Kharseko
2 days ago cf2068420f92f25985c22a6cdb16c17d9ceb1efb
opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/JDBCStorage.java
@@ -698,6 +698,17 @@
   }
   /**
    * The connection string this storage borrows on, distrusts and closes with: the one
    * {@link #open(AccessMode)} registered with, and only failing that the one config names now. Every path
    * that names a pool goes through here, for the reason given in {@link #getConnection(boolean)} - a
    * db-directory changed on a running backend otherwise sends each of them to a different pool.
    */
   private String poolKey() {
      final String registered=poolConnectionString;
      return registered!=null ? registered : config.getDBDirectory();
   }
   /**
    * Borrows a connection the pool validates whatever the alive window of
    * {@link CachedConnection#ALIVE_BYPASS_PROPERTY} says, for the borrows this class compensates a dropped
    * connection on in no other way: {@link #open(AccessMode)}, {@link #removeStorageFiles()} and the importer
@@ -713,17 +724,89 @@
   // for the pool stands in for every path that takes a connection. A stand-in of the trusted
   // borrow alone let the open, the import and the removal - the three that ask for a validated
   // one - reach a real database instead.
   //
   // It names the pool this storage registered with in open(), not the one config names now.
   // Nothing keeps db-directory from being changed on a running backend - applyConfigurationChange()
   // takes it, isConfigurationChangeAcceptable() refuses nothing, and the component-restart admin
   // action renders a message rather than holding the change back - so re-reading it here would
   // borrow from a pool this storage never registered with, leaving the one it did register with
   // holding a user that never borrows: the leak of #878 back through the configuration. And an
   // unregistered pool is drained the moment another backend that did register with it closes,
   // with this one still borrowing from it (issue #878).
   Connection getConnection(boolean trusted) throws Exception {
      return CachedConnection.getConnection(config.getDBDirectory(), trusted);
      return CachedConnection.getConnection(poolKey(), trusted);
   }
   AccessMode accessMode=AccessMode.READ_ONLY;
   // Whether this storage counts as a user of the pool of its connection string. The pool belongs
   // to the database rather than to this backend - two backends may address one database - so it
   // is reference counted, and this flag keeps an open() or a close() that comes twice from
   // counting twice (issue #878).
   private final AtomicBoolean poolRegistered=new AtomicBoolean();
   // The connection string open() registered with. applyConfigurationChange() replaces config, so
   // reading db-directory again at close() could give back the pool of a database this storage
   // never registered with - leaving the one it did with a user it never loses (issue #878).
   private volatile String poolConnectionString;
   @Override
   public void open(AccessMode accessMode) throws Exception {
      try (final Connection con=getValidatedConnection()) {
         this.accessMode = accessMode;
         storageStatus = StorageStatus.working();
      final boolean claimedHere=poolRegistered.compareAndSet(false, true);
      // Raised once openPool() has returned, which is when a user has actually been added. The
      // claim alone cannot answer for that: releasePool() on a claim openPool() never made would
      // take a user off a pool this storage never added one to - and the pool of a database two
      // backends share would lose the user of the other one, draining connections it is still
      // borrowing.
      boolean registeredHere=false;
      try {
         // Inside the try, so that the registration this call made is given back however the open
         // ends - the registration is taken before the pool is of any use, and a pool holding a
         // user that never borrows keeps its connections for a borrower that is not going to come.
         if (claimedHere) {
            poolConnectionString=config.getDBDirectory();
            CachedConnection.openPool(poolConnectionString);
            registeredHere=true;
         }
         // The validated borrow is the whole of the open, and nothing is taken from it here: the
         // status is set below rather than inside the block, or a throw from the implicit close()
         // - the rollback of the return goes to the database - would leave the storage reporting
         // working() while this method fails and the catch takes its registration back. write()
         // and ImporterImpl both skip the re-open when the status says working, so the pool would
         // be left with no user at all: every connection returned to it destroyed on the spot,
         // pooling off for that database for as long as the server runs (issue #878).
         try (final Connection con=getValidatedConnection()) {
         }
      } catch (Throwable e) {
         // Throwable rather than Exception: an Error out of the borrow - a NoClassDefFoundError
         // from the static initializer of a driver is the one to expect here - would otherwise
         // leave the pool holding a user that never leaves.
         // Only what this call registered is given back: an open that found the registration
         // already made took nothing, and giving it back would release a pool still in use.
         if (registeredHere) {
            releasePool();
         } else if (claimedHere) {
            // The claim was won but no user was added. The claim goes back on its own, without
            // touching the pool: left standing it would send the close() of this storage to
            // releasePool() for a registration it never made.
            poolConnectionString=null;
            poolRegistered.set(false);
         }
         throw e;
      }
      this.accessMode = accessMode;
      storageStatus = StorageStatus.working();
   }
   /** Gives up the registration of this storage with the pool of the database it opened. */
   private void releasePool() {
      if (poolRegistered.compareAndSet(true, false)) {
         final String registered=poolConnectionString;
         poolConnectionString=null;
         if (registered!=null) {
            CachedConnection.closePool(registered);
         }
      }
   }
@@ -740,6 +823,10 @@
      // that it is not reissued for every tree on every open; disabling and re-enabling the
      // backend is the way to try again once the privilege has been granted
      unstampableTrees.clear();
      // A closed backend has no use for its connections. They used to stay open - close() only
      // flipped the status - so disabling or removing a JDBC backend left them behind, and with
      // nothing left to expire the pool entry they could stay open for good (issue #878).
      releasePool();
   }
   // The trees this storage has taken an interest in, and the tables they map to. listTrees() -
@@ -972,7 +1059,12 @@
   Connection newStampConnection(Dialect dialect) throws SQLException {
      final Properties properties=new Properties();
      properties.putAll(dialect.connectProperties);
      final Connection con=DriverManager.getConnection(config.getDBDirectory(), properties);
      // poolKey() rather than the configuration as it stands: this connection is not pooled, but it
      // is a connection to the database of this storage, and db-directory may be changed on a
      // running backend. Reading it again here would stamp the trees of this backend in whichever
      // database the configuration names now, while every other connection of it stays with the
      // one open() registered (issue #878).
      final Connection con=DriverManager.getConnection(poolKey(), properties);
      try {
         con.setAutoCommit(false);
         executeSessionStatement(con, dialect.lockTimeoutSql); // give up instead of waiting for another session
@@ -1762,7 +1854,9 @@
    * connection established before it, and the pool has no other way of hearing about any of them.
    */
   private void distrustPool() {
      CachedConnection.distrustPool(config.getDBDirectory());
      // keyed like every other pool lookup of this storage: a drop reported against the string
      // config names now would be filed on a pool holding none of this storage's connections
      CachedConnection.distrustPool(poolKey());
   }
   /** Returns the randomized delay before the given attempt is replayed, doubling with each attempt up to a cap. */
@@ -2578,22 +2672,65 @@
       * of an online import blocked by an LDAP write on the same table sat until the bound of an
       * entry read and then failed the import.
       */
      ImporterImpl(Connection con, boolean isOpen) {
         // An import writes by definition, so a storage that is not writeable refuses one where the
         // importer is built - which is where it was refused until the write transaction of a read-only
         // storage became one that is granted and checks per operation (#874). Left to that check, an
         // import of such a storage would take a connection out of the pool, begin its transaction and
         // fail at the first tree it clears rather than at its start.
         // What arrives here read-only is a storage that was already open: import-ldif and
         // rebuild-index both close it first, and startImport() opens a closed one READ_WRITE - an
         // import of any storage of this server reopens it that way - so those two arrive writeable.
         if (!accessMode.isWriteable()) {
            throw new ReadOnlyStorageException();
      public ImporterImpl() {
         // The open belongs here with the borrow it precedes (#878): startImport() used to do both,
         // and a failure between them had two owners to give back what each had taken.
         isOpen=getStorageStatus().isWorking();
         if (!isOpen) {
            try {
               open(AccessMode.READ_WRITE);
            }catch (Exception e) {
               throw new StorageRuntimeException(e);
            }
         }
         this.con=con;
         this.isOpen=isOpen;
         txr=new ReadableTransactionImpl(con, StatementBound.BULK);
         txw=new WriteableTransactionTransactionImpl(con, StatementBound.BULK);
         // Nothing holds what this constructor takes until it returns: close() belongs to an
         // object that was built, so a throw below would leave the connection borrowed and the
         // storage this constructor opened open, with nobody left to give either back.
         Connection borrowed=null;
         try {
            // An import writes by definition, so a storage that is not writeable refuses one where the
            // importer is built - which is where it was refused until the write transaction of a read-only
            // storage became one that is granted and checks per operation (#874). Left to that check, an
            // import of such a storage would take a connection out of the pool, begin its transaction and
            // fail at the first tree it clears rather than at its start.
            // Inside the try and in front of the borrow: with the borrow moved in here (#878) the
            // refusal now takes no connection at all, and the open above is still given back by the
            // catch below - which is the half of it a storage that arrives closed and read-only needs.
            if (!accessMode.isWriteable()) {
               throw new ReadOnlyStorageException();
            }
            borrowed=getValidatedConnection();
            txr =new ReadableTransactionImpl(borrowed, StatementBound.BULK);
            txw =new WriteableTransactionTransactionImpl(borrowed, StatementBound.BULK);
            con = borrowed;
            borrowed=null;
         }catch (Throwable e){
            // Throwable rather than Exception, the way close() below catches it and for the same
            // reason: the borrow is handed off to nothing until this constructor returns, and
            // only its close() gives back the permit it took. new WriteableTransactionTransactionImpl
            // runs a StampSession in a field initializer, so an Error out of a bulk import - an
            // OutOfMemoryError is the one to expect - would leave the connection borrowed for the
            // life of the server, and enough of them walk the bound of the pool down to nothing
            // (issue #878).
            if (borrowed!=null) {
               try {
                  borrowed.close();
               }catch (Throwable e2) {
                  // suppressed rather than dropped: the failure being unwound is the one the
                  // caller asked about, and a return that failed on top of it is worth reading
                  e.addSuppressed(e2);
               }
            }
            if (!isOpen) {
               JDBCStorage.this.close();
            }
            if (e instanceof Error) {
               // on its way out as it is: an Error says the JVM is in no state to have this
               // wrapped and reported as a failure of the storage
               throw (Error) e;
            }
            throw e instanceof StorageRuntimeException ? (StorageRuntimeException) e : new StorageRuntimeException(e);
         }
      }
      
      @Override
@@ -2601,6 +2738,34 @@
         aborted = true;
      }
      /**
       * Hands the connection back to the pool and closes the stamp session, whatever went before.
       * Returns the failure the caller is to report: the return rolls back, and the rollback
       * fails on exactly the connection whose commit just did, so the commit stays the exception
       * the caller sees and this one rides along with it instead of replacing it.
       */
      private SQLException releaseConnection(SQLException failure) {
         try {
            con.close();
         } catch (Throwable e) {
            // Throwable rather than SQLException: this close() is the return to the pool, whose
            // rollback a driver is free to fail unchecked. Reported rather than thrown, since a
            // throw out of here would leave with the failure the caller actually came for - the
            // commit above, and in the Throwable branch of close() the Error that branch exists
            // to preserve - dropped on the floor (issue #878).
            final SQLException reported=e instanceof SQLException ? (SQLException) e
               : new SQLException("the connection of the import could not be returned to the pool", e);
            if (failure==null) {
               failure=reported;
            }else {
               failure.addSuppressed(reported);
            }
         } finally {
            txw.stampSession.close();
         }
         return failure;
      }
      // The connection goes back whatever the commit does, and the storage this importer opened
      // is closed whatever the connection does: an importer is closed on the way out of a failed
      // import as readily as a finished one - a clearTree() that reaches the bulk bound is one
@@ -2609,6 +2774,7 @@
      @Override
      public void close() {
         try {
            SQLException failure=null;
            try {
               con.commit();
               if (aborted) {
@@ -2616,15 +2782,27 @@
               }else {
                  updateTableStatistics(con, writtenTrees);
               }
            } finally { // the pooled connection must be returned even when the commit or a statistics statement throws
               try {
                  con.close();
               } finally {
                  txw.stampSession.close();
            } catch (SQLException e) {
               failure=e;
            } catch (Throwable t) {
               // Back to the pool whatever came out of the commit, not only on the SQLException
               // a driver is supposed to throw: nothing else holds this connection, and only
               // its close() gives back the permit it took. A pool is never removed from the
               // map, so a permit lost to an Error out of a bulk import - or to a driver
               // failing unchecked - is lost for the life of the server, and enough of them
               // walk the bound down to nothing (issue #878).
               final SQLException onTheWayOut=releaseConnection(null);
               if (onTheWayOut!=null) {
                  t.addSuppressed(onTheWayOut);
               }
               throw t;
            }
         } catch (SQLException e) {
            throw new StorageRuntimeException(e);
            // Back to the pool even when the commit failed: nothing else holds this connection,
            // so leaving it behind would leak it along with the failure.
            failure=releaseConnection(failure);
            if (failure!=null) {
               throw new StorageRuntimeException(failure);
            }
         } finally {
            if (!isOpen) {
               JDBCStorage.this.close();
@@ -2663,52 +2841,11 @@
   //import
   @Override
   public Importer startImport() throws ConfigException, StorageRuntimeException {
      final boolean wasOpen=getStorageStatus().isWorking();
      if (!wasOpen) {
         try {
            open(AccessMode.READ_WRITE);
         }catch (Exception e) {
            throw new StorageRuntimeException(e);
         }
      }
      final Connection con;
      try {
         con=getValidatedConnection();
      }catch (Exception e){
         // and the storage this method opened goes back with it: ImporterImpl.close() is what closes
         // it again when an import opened it, and no importer is going to be built to reach that
         if (!wasOpen) {
            close();
         }
         throw new StorageRuntimeException(e);
      }
      // outside the catch: the importer of a read-only storage throws ReadOnlyStorageException,
      // which a caller tells apart from any other failure of an import
      boolean built=false;
      try {
         final Importer importer=new ImporterImpl(con, wasOpen);
         built=true;
         return importer;
      }finally {
         // and the connection borrowed above goes back on every path that does not build an
         // importer to hold it: it is the one an import keeps for its whole duration, so leaving it
         // here takes it out of the pool for good, with the transaction it had already begun. A
         // finally rather than a catch, so that it covers what a catch has to name - an Error
         // leaves the pool one connection short exactly as ReadOnlyStorageException did.
         if (!built) {
            try {
               con.close();
            }catch (SQLException ignored) {
               // the importer was never built; the failure to report is the one on its way out
            }
            // and the storage this method opened goes back with the connection, for the reason the
            // borrow above gives: ImporterImpl.close() is what closes it again when an import
            // opened it, and there is no importer here to reach that
            if (!wasOpen) {
               close();
            }
         }
      }
      // Everything this used to do before building the importer - opening a closed storage, and
      // borrowing the connection an import keeps for its whole duration - is the importer's own now
      // (#878). Split between the two, a failure in between had to be given back by whichever of them
      // had taken what, and the constructor's own throw was covered by neither.
      return new ImporterImpl();
   }
   
   //backup