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

18 files modified
4 files added
8185 ■■■■■ changed files
opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/CachedConnection.java 337 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/JDBCStorage.java 2172 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/backends/pdb/PDBStorage.java 160 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/BackendImpl.java 352 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java 7 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/spi/Storage.java 17 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java 57 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/PendingChanges.java 17 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/backend.properties 18 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/CachedConnectionTestCase.java 292 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/CatalogConnectionTestCase.java 658 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/JDBCStatementBoundTestCase.java 242 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/JDBCStorageRetryTest.java 130 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/PgSqlTestCase.java 144 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/TestCase.java 1054 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/backends/pdb/PDBStorageTest.java 328 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/ReplayedConfigChangeTest.java 967 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/plugins/ShortCircuitPlugin.java 351 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/replication/ReplicationTestCase.java 55 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/replication/UpdateOperationTest.java 458 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/IsServerFailureTest.java 242 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/PendingChangesTest.java 127 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/CachedConnection.java
@@ -37,6 +37,7 @@
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.LongFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -111,6 +112,98 @@
    static final String POOL_TIMEOUT_PROPERTY = "org.openidentityplatform.opendj.jdbc.pool.timeout";
    static final long DEFAULT_POOL_TIMEOUT_SECONDS = 60;
    /**
     * The socket read timeout a connection of this pool carries for the whole of its life, in
     * seconds; 0 for none, which is what every connection of this backend carried before this
     * property existed, and the default.
     * <p>
     * It bounds what neither of the two bounds before it reaches. The bound of
     * {@value #CONNECT_TIMEOUT_PROPERTY} covers the connect and the login and is taken off as soon
     * as the login is through, and the socket read timeout {@code JDBCStorage} arms behind a
     * cancelled statement is armed for the length of that statement alone - so a commit, a
     * rollback, a lookup of the catalog and the rows a cursor drains are all read from a socket
     * with no deadline of any kind, and an operation that meets a database which stopped answering
     * after the login stays parked.
     * <p>
     * The default is a consequence rather than caution: a bound standing on the connection has to
     * exceed the longest silence the database may legitimately produce, and the class the longest
     * statements belong to ({@code JDBCStorage.StatementBound.BULK}) ships unbounded for reasons of
     * its own. There is no value here that does not contradict it - so the deployment that knows how
     * long its database may go without answering is the one that sets this, and the statements of
     * that class run with it taken off for as long as they do.
     * <p>
     * That silence is what this is sized against, rather than the longest statement, because the
     * lift covers statements alone: the commit at the end of an import is a call of its own with no
     * statement in flight behind it - {@code ImporterImpl.close()} commits a whole import in one -
     * and so are the rollback of every borrow and the reads of the catalog. Every one of them runs
     * under this bound whatever the class of the statements before it. It has to exceed the bound of
     * an ordinary statement as well ({@code JDBCStorage.StatementBound.OPERATION}, two minutes by
     * default): under it, such a statement dies on the socket at this value instead of being
     * cancelled at its own, which costs the connection the driver then closes and reports neither
     * property. A backend opening with the two set that way says so once.
     * <p>
     * A read bound standing in the connection string is the deployment's own and is left alone by
     * every part of this: it is not replaced here, and it is not the one taken off there. A value of
     * 0 there is no bound of theirs, though - it is the default of the driver, written out - and
     * this one goes on top of it.
     */
    static final String READ_TIMEOUT_PROPERTY = "org.openidentityplatform.opendj.jdbc.read.timeout";
    static final int DEFAULT_READ_TIMEOUT_SECONDS = 0;
    // Read once, at class initialization, for the reason aliveBypassNanos is: it is read on every
    // connect and on every statement that has to take it off again, and neither is the place to
    // parse a system property. Not final so that a test can vary it without a class loader of its
    // own, and volatile because a non-final static is written neither atomically nor visibly to the
    // threads reading it (JLS 17.7).
    static volatile int readTimeoutMillis = getReadTimeoutMillis();
    /**
     * The bound of {@value #READ_TIMEOUT_PROPERTY} in milliseconds, which is the unit
     * {@code setNetworkTimeout} takes. A value that is not a number, and a negative one, are
     * reported once and ignored in favour of the default - a deployment that asked for a bound and
     * misspelled it gets none, and that is the one thing this property exists to keep from
     * happening quietly. A value past {@link JDBCStorage#MAX_BOUND_SECONDS} is taken down to it: the
     * ceiling there is what a socket read timeout can hold at all, and a value beyond it would reach
     * the driver as a negative timeout - outside the contract of the call, and a value a driver is
     * free to read as anything.
     */
    static int getReadTimeoutMillis() {
        // Clamped against the ceiling rather than through JDBCStorage.clampSeconds(): that ceiling is
        // a compile-time constant and reaches this class inlined, while the call would be a
        // package-private call into another class - and this runs in the initializer of this one,
        // which is loaded by whatever loader defines it. Across two loaders that is an
        // IllegalAccessError rather than a call, and it would leave the class uninitializable.
        final long seconds = Math.min(JDBCStorage.MAX_BOUND_SECONDS,
            getNonNegativeProperty(READ_TIMEOUT_PROPERTY, DEFAULT_READ_TIMEOUT_SECONDS, "s"));
        return (int) (seconds * 1000);
    }
    /**
     * The read bound this class put on a connection of this url, in milliseconds, or 0 for a
     * connection carrying none of ours. What {@code JDBCStorage} has to know before it takes that
     * bound off for a statement of a class that carries no bound of its own: a read timeout
     * standing in the connection string is the deployment's own, and a driver whose property names
     * are not known here was never given one - taking either off would leave the connection
     * unbounded for the rest of its life in the pool, which is a bound taken away from a deployment
     * that asked for one.
     * <p>
     * The dialect is read off the connection string here, the way {@link #getConnection} reads the
     * one it hands {@link #connect}: the two have to answer the same, or the bound taken off would
     * not be the bound that was set.
     */
    static int standingReadBoundMillis(String connectionString) {
        return connectionString == null ? 0
            : standingReadBoundMillis(connectionString, ConnectDialect.of(connectionString));
    }
    private static int standingReadBoundMillis(String connectionString, ConnectDialect dialect) {
        final int millis = readTimeoutMillis;
        if (millis <= 0 || dialect == null || dialect.bounds(connectionString, dialect.readProperties)) {
            return 0;
        }
        return millis;
    }
    /** Bound of the validation of a pooled connection: isValid(0) means "no timeout" in the JDBC contract. */
    static final int VALIDATION_TIMEOUT_SECONDS = 5;
@@ -235,6 +328,54 @@
    private static volatile Executor closer = DIRECT_EXECUTOR;
    /**
     * Returns the bound of one attempt to establish a connection, as configured by the {@value
     * #CONNECT_TIMEOUT_PROPERTY} system property; 0 for the operator asking for no bound of its own.
     * A value beyond what a millisecond bound can carry is taken down to it: three of the four
     * dialects state their properties in milliseconds, and a value that saturates the conversion
     * bounds nothing.
     * <p>
     * Read here rather than at each connect so that every connection this backend establishes is
     * bounded by the same configured value - the borrows of this pool and the connection {@code
     * JDBCStorage} opens outside it for the tree catalog of a backend (#888) alike. A connect
     * bounded tighter than the login of the deployment takes is a backend that stops opening, and
     * one place to read the property is what keeps the two from drifting apart.
     */
    static long getConnectTimeoutSeconds() {
        return Math.min(getNonNegativeProperty(CONNECT_TIMEOUT_PROPERTY, DEFAULT_CONNECT_TIMEOUT_SECONDS, "s"),
            Integer.MAX_VALUE / 1000);
    }
    /**
     * Returns the deadline of a whole borrow, as configured by the {@value #POOL_TIMEOUT_PROPERTY}
     * system property; 0 for the operator asking for no deadline at all.
     * <p>
     * Read here for the reason the bound of a connect is: it is what a database taking no
     * connection for the moment is waited out for, and the connection {@code JDBCStorage} opens
     * outside this pool for the tree catalog of a backend (#888) waits it out for exactly as long -
     * one property, one meaning, whichever of the two is asking.
     */
    static long getPoolTimeoutSeconds() {
        return getNonNegativeProperty(POOL_TIMEOUT_PROPERTY, DEFAULT_POOL_TIMEOUT_SECONDS, "s");
    }
    /**
     * The moment a borrow of this length gives up, or {@link Long#MAX_VALUE} where it gives up
     * never - a property of 0, and a value so large that the milliseconds of it would overflow.
     * <p>
     * The sum is guarded and not only the product: a value under the clamp above but large enough
     * that the moment it names is past the end of the epoch would wrap to a deadline already behind
     * us, and a borrow configured to wait practically forever would give up on its first retryable
     * failure - the opposite of what was asked for.
     */
    static long deadlineOf(long startedAt, long poolTimeoutSeconds) {
        if (poolTimeoutSeconds == 0 || poolTimeoutSeconds >= Long.MAX_VALUE / 1000) {
            return Long.MAX_VALUE;
        }
        final long deadline = startedAt + poolTimeoutSeconds * 1000;
        return deadline < startedAt ? Long.MAX_VALUE : deadline;
    }
    /**
     * Returns the time after which an idle pooled connection is closed, as configured by the
     * {@value #TTL_PROPERTY} system property. An invalid value is ignored in favor of the default.
     * <p>
@@ -399,7 +540,10 @@
        if (borrowers <= pool.max()) {
            return;
        }
        final long poolTimeoutSeconds = getNonNegativeProperty(POOL_TIMEOUT_PROPERTY, DEFAULT_POOL_TIMEOUT_SECONDS, "s");
        // through the helper the borrows and the catalog connect both read it by: the same property
        // has to mean the same thing wherever it is asked, and a clamp that helper grows the day the
        // deadline needs one - getConnectTimeoutSeconds() already has one - must not be missed here
        final long poolTimeoutSeconds = getPoolTimeoutSeconds();
        final String wait = poolTimeoutSeconds == 0
            ? "waits for one to be returned for as long as that takes"
            : "waits up to " + poolTimeoutSeconds + "s for one to be returned and fails if none is";
@@ -878,6 +1022,46 @@
            return false;
        }
        /**
         * Whether one of these is bounded by the connection string, or by a system property this
         * driver reads, as the bound of an established connection has to ask it. Told apart from
         * {@link #declared} by what a 0 means: there the question is whether a property of ours is
         * to be supplied to the connect at all, and on postgresql a parameter of the url outranks
         * that property whatever it says - while a bound put on an established connection with
         * setNetworkTimeout is outranked by nothing, so a "socketTimeout=0" is no bound of the
         * deployment's to stay out of the way of. It is the default of the driver, written out, and
         * reading it as theirs would leave a deployment that asked for a standing bound with none.
         * <p>
         * The names are recognized in the url the way {@link #declared} recognizes them, and out of
         * the system properties from the same list - see the comment on that method.
         */
        private boolean bounds(String connectionString, String... properties) {
            for (final String property : properties) {
                if (boundInUrl(connectionString, property) || setAsSystemProperty(property)) {
                    return true;
                }
            }
            return false;
        }
        /**
         * Whether the connection string bounds this property, under its own name or the last segment
         * of it. The two names are asked independently, the way {@link #declaredInUrl} asks them:
         * stopping at the first name present, even where its value is a zero, let a
         * "...?oracle.jdbc.ReadTimeout=0&ReadTimeout=600" answer with the zero of the name that comes
         * first and hide the bound standing behind it. Such a url is declared() and would then not
         * be bounds(): the login keeps the administrator's 600 s and one of ours goes on top of it
         * with setNetworkTimeout. The two predicates have to look at the same set of names for "the
         * bound taken off is the bound that was set" to hold.
         */
        private boolean boundInUrl(String connectionString, String property) {
            if (isBound(parameterValue(connectionString, property))) {
                return true;
            }
            final int dot = property.lastIndexOf('.');
            return dot >= 0 && isBound(parameterValue(connectionString, property.substring(dot + 1)));
        }
        // Whether the administrator bounded one of these properties themselves. The dialects
        // separate their parameters differently - "?a=1&b=2" (postgresql, mysql), ";a=1;b=2" (sql
        // server), "(A=1)" inside the descriptor of an oracle tns url, where the property also goes
@@ -1079,14 +1263,11 @@
        final Pool pool = poolOf(connectionString);
        final ConnectDialect dialect = ConnectDialect.of(connectionString);
        reportUnknownDialect(connectionString, dialect);
        final long connectTimeoutSeconds = Math.min(
            getNonNegativeProperty(CONNECT_TIMEOUT_PROPERTY, DEFAULT_CONNECT_TIMEOUT_SECONDS, "s"),
            Integer.MAX_VALUE / 1000);
        final long poolTimeoutSeconds = getNonNegativeProperty(POOL_TIMEOUT_PROPERTY, DEFAULT_POOL_TIMEOUT_SECONDS, "s");
        final long connectTimeoutSeconds = getConnectTimeoutSeconds();
        final long poolTimeoutSeconds = getPoolTimeoutSeconds();
        final long ttlMillis = getCacheTtlMillis();
        final long startedAt = System.currentTimeMillis();
        final long deadline = (poolTimeoutSeconds == 0 || poolTimeoutSeconds >= Long.MAX_VALUE / 1000)
            ? Long.MAX_VALUE : startedAt + poolTimeoutSeconds * 1000;
        final long deadline = deadlineOf(startedAt, poolTimeoutSeconds);
        // A thread already holding a connection is not made to wait for one: the two are held at
        // the same time, so waiting for the first to come back would wait for itself.
        final boolean reentrant = pool.heldByCurrentThread();
@@ -1188,6 +1369,13 @@
     * are the ones of a driver, so a driver outside the four leaves every attempt unbounded - and
     * the deadline of the borrow cannot reach into a connect that is already under way, since the
     * driver is the only thing holding the socket.
     * <p>
     * The connect is not the whole of it. {@value #READ_TIMEOUT_PROPERTY} is not put on the
     * connections of such a pool either: {@link #standingReadBoundMillis(String)} answers 0 for a
     * dialect this class does not know, so a read timeout the url may already carry under a name of
     * its own is left alone rather than covered by one of ours. That silence is what this says out
     * loud, since the strict parsing of that property exists precisely so that a deployment which
     * asked for a bound is never quietly left with none.
     */
    private static void reportUnknownDialect(String connectionString, ConnectDialect dialect) {
        if (dialect != null) {
@@ -1197,11 +1385,19 @@
        for (final ConnectDialect candidate : ConnectDialect.values()) {
            known.append(known.length() > 0 ? ", " : "").append(candidate.urlPrefix);
        }
        // Only where one was asked for: a deployment running on the default of that property asked
        // for no standing bound anywhere, and has nothing to act on here.
        final String standingBound = readTimeoutMillis > 0
            ? ", and the " + READ_TIMEOUT_PROPERTY + " asked for is not put on the connections of this pool"
                + " either - a read of one whose database stops answering after the login waits with no deadline"
                + " able to reach it. Such a url may bound the read under a name this backend does not know, which"
                + " is why none is set on top of it: bound it in the url instead"
            : "";
        warnOnce(safeUrl(connectionString) + "|unknown-dialect",
            "%s names a driver whose timeout properties are not known to this backend (%s are): a connect to a"
                + " database that accepts it and does not answer is left without a bound, and the %s property"
                + " cannot end it",
            safeUrl(connectionString), known, POOL_TIMEOUT_PROPERTY);
                + " cannot end it%s",
            safeUrl(connectionString), known, POOL_TIMEOUT_PROPERTY, standingBound);
    }
    /**
@@ -1392,12 +1588,10 @@
            // still under the read bound: both of these are round trips of their own
            conNew.setAutoCommit(false);
            conNew.setTransactionIsolation(TRANSACTION_READ_COMMITTED);
            if (readBoundSet) {
                // a driver that will not take the bound back has warned about it already: the
                // connection serves the borrower that is waiting for it and is closed rather than
                // pooled, so the bound of the login does not outlive it in the pool
                poolable = relaxReadBound(conNew, connectTimeoutSeconds);
            }
            // whatever the driver will not take here it has warned about already: a connection left
            // carrying the read bound of its login serves the borrower that is waiting for it and is
            // closed rather than pooled, so that bound does not outlive it in the pool
            poolable = applyStandingReadBound(conNew, connectionString, dialect, connectTimeoutSeconds, readBoundSet);
        } catch (SQLException | RuntimeException e) { // nothing holds this connection yet: it would leak
            closeQuietly(conNew);
            throw e;
@@ -1407,19 +1601,41 @@
        return established;
    }
    // The second bound of the login is a socket read timeout on mysql, oracle and sql server, in
    // force for the whole life of the connection: left in place it would break every statement
    // slower than it - an import batch, the statistics of a freshly loaded table - so it is lifted
    // as soon as the login is through, restoring the behaviour of a connection this class
    // established before. A read bound the connection string sets itself is never touched here:
    // it is not set at all, so nothing of the administrator's is lifted along with it. Returns
    // whether the bound is gone - a connection still carrying it must not be pooled.
    // Named by the bound the login was given rather than by the property it came from: with
    // CONNECT_TIMEOUT_PROPERTY at 0 the attempt takes its bound from what is left of the deadline
    // of the borrow, so naming that property would point at the one setting that is not in force.
    private static boolean relaxReadBound(Connection con, long boundSeconds) {
        return setNetworkTimeout(con, 0, "statements taking longer than the " + boundSeconds
            + "s the login of this connection was bounded by fail on it, and it is closed rather than pooled");
    /**
     * Gives an established connection the read bound it carries from here on, which is the same
     * call that takes the read bound of its login off.
     * <p>
     * The second is not optional. On mysql, oracle and sql server the second bound of the login is
     * a socket read timeout in force for the whole life of the connection, and left in place it
     * breaks every statement slower than a connect - an import batch, the statistics of a freshly
     * loaded table. What replaces it is {@value #READ_TIMEOUT_PROPERTY}, or the 0 this class has
     * always put here where a deployment asks for nothing.
     * <p>
     * A read bound the connection string sets itself is neither replaced nor lifted: it was not set
     * by us at the login either, so nothing of the administrator's is touched here.
     * <p>
     * Called whether or not the login had a bound to lift, since the standing bound is not the
     * login's: with {@value #CONNECT_TIMEOUT_PROPERTY} at 0 the login is bounded by what is left of
     * the deadline of the borrow instead, and a deployment running that way would otherwise set the
     * property here and get nothing for it.
     * <p>
     * Returns whether the connection may be pooled. A connection still carrying the bound of its
     * login must not be: it would fail the statements of every borrower after this one. A
     * connection that merely never took the standing bound may be - that is the connection this
     * pool handed out before the property existed.
     */
    private static boolean applyStandingReadBound(Connection con, String connectionString, ConnectDialect dialect,
                                                  long loginBoundSeconds, boolean readBoundSet) {
        final int millis = standingReadBoundMillis(connectionString, dialect);
        if (millis == 0 && !readBoundSet) {
            return true; // nothing of ours on this connection: nothing to set here, and nothing to lift
        }
        final String consequence = readBoundSet
            ? "statements taking longer than the " + loginBoundSeconds
                + "s the login of this connection was bounded by fail on it, and it is closed rather than pooled"
            : "this connection carries no read bound of its own, so a read of it the database stops answering waits"
                + " with no deadline able to reach it";
        return setNetworkTimeout(con, millis, consequence) || !readBoundSet;
    }
    /**
@@ -1493,16 +1709,71 @@
    // is indistinguishable from a hang. Throttled, since every operation of the backend borrows
    // through here and would otherwise log a copy of its own.
    private static void warnStall(String connectionString, int attempts, long startedAt, SQLException cause) {
        warnStall(connectionString, "", startedAt,
            waitedMs -> stallMessage(connectionString, attempts, waitedMs, cause));
    }
    /**
     * The same for a connect this class makes for somebody outside the pool - the connection the
     * tree catalog of a backend is written on (#888) - which waits for no pooled connection and
     * must not be described as one.
     * <p>
     * Throttled apart from the borrows of the same url as well as worded apart from them: the two
     * stall on the same database for the same reason, so a borrow that warned a moment ago would
     * otherwise silence the connect that is about to fail - the one of the two an operator has no
     * other line about.
     */
    static void warnStallOutsidePool(String connectionString, String what, int attempts, long startedAt,
            SQLException cause) {
        warnStall(connectionString, "|" + what, startedAt,
            waitedMs -> outsidePoolStallMessage(connectionString, what, attempts, waitedMs, cause));
    }
    private static void warnStall(String connectionString, String throttleKeySuffix, long startedAt,
            LongFunction<String> message) {
        final long now = System.currentTimeMillis();
        if (stallWarningDue(connectionString, throttleKeySuffix, startedAt, now)) {
            logger.warn(LocalizableMessage.raw("%s", message.apply(now - startedAt)));
        }
    }
    /**
     * Whether a stall of this wait is to be reported now: a wait shorter than
     * {@link #STALL_WARNING_AFTER_MS} is no stall yet, and one already reported for this url within
     * {@link #STALL_WARNING_INTERVAL_MS} is not reported again - every worker thread of a server
     * meets a database taking no connection at the same moment, and one line an interval is what
     * an operator can read.
     * <p>
     * Built apart from the logging of it for the reason {@link #stallMessage} is: what it has to
     * keep is a rule a test can hold it to, and the shipped path reaches the throttle only where a
     * connect really has stalled for a second. The suffix is what keeps the two waits apart - a
     * borrow of the pool and the connect the tree catalog of a backend is made on (#888) stall on
     * the same database for the same reason, and a borrow that reported a moment ago must not
     * silence the connect that is about to fail, which has no other line about it at all.
     * <p>
     * Filing the moment is part of deciding it, so that two threads asking at once report once.
     */
    static boolean stallWarningDue(String connectionString, String throttleKeySuffix, long startedAt, long now) {
        if (now - startedAt < STALL_WARNING_AFTER_MS) {
            return;
            return false;
        }
        final AtomicLong lastOfThisUrl =
            lastStallWarning.computeIfAbsent(safeUrl(connectionString), url -> new AtomicLong());
            lastStallWarning.computeIfAbsent(safeUrl(connectionString) + throttleKeySuffix, url -> new AtomicLong());
        final long last = lastOfThisUrl.get();
        if (now - last >= STALL_WARNING_INTERVAL_MS && lastOfThisUrl.compareAndSet(last, now)) {
            logger.warn(LocalizableMessage.raw("%s", stallMessage(connectionString, attempts, now - startedAt, cause)));
        }
        return now - last >= STALL_WARNING_INTERVAL_MS && lastOfThisUrl.compareAndSet(last, now);
    }
    /**
     * The stall of a connect made outside the pool, as it reaches the log. Built apart from the
     * logging of it for the reason {@link #stallMessage} is: the rule it has to keep - neither the
     * connection string nor the message of the driver reaches a log as it stands - is a rule a test
     * can hold it to.
     */
    static String outsidePoolStallMessage(String connectionString, String what, int attempts, long waitedMs,
            SQLException cause) {
        return String.format("%s takes no further connection: the %s connection of this backend is opened outside the"
            + " pool and has been retrying for %d ms (%d attempts), last error: %s", safeUrl(connectionString), what,
            waitedMs, attempts, redact(cause.getMessage(), connectionString));
    }
    /**
opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/JDBCStorage.java
@@ -25,6 +25,7 @@
import org.forgerock.opendj.config.server.ConfigurationChangeListener;
import org.forgerock.opendj.ldap.ByteSequence;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.server.config.server.JDBCBackendCfg;
import org.opends.server.backends.pluggable.spi.*;
import org.opends.server.core.ServerContext;
@@ -36,6 +37,7 @@
import java.io.Closeable;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.*;
@@ -119,6 +121,120 @@
    private JDBCBackendCfg config;
    /** Not read yet: it follows {@link #poolKey()}, which a close and a re-open may leave naming another database. */
    private static final int STANDING_READ_BOUND_UNREAD = -1;
    private volatile int standingReadBound = STANDING_READ_BOUND_UNREAD;
    /**
     * The read bound this backend puts on its connections at their login, in milliseconds, or 0
     * where it puts none - what {@link #applyBackstop} takes off a connection for the length of a
     * statement that carries no bound of its own. Read once and remembered rather than per
     * statement: it follows a system property and the connection string of the pool, and neither of
     * them changes under a running statement.
     * <p>
     * Resolved against {@link #poolKey()} rather than against the configuration as it stands, for
     * the reason {@link #getConnection(boolean)} borrows on that one: db-directory may be changed on
     * a running backend, and the connections whose bound this decides are the ones of the pool
     * {@link #open(AccessMode)} registered with. Read off the url the configuration names now, the
     * lift would be decided for a pool this storage never borrows from - leaving the bound of this
     * backend standing on a statement of an unbounded class, which is what {@code bulk.timeout=0}
     * promises will not happen, or taking a bound of the deployment's own off the connections it
     * really borrows.
     * <p>
     * Resolved once and for all in {@link #open(AccessMode)} rather than left to the first statement
     * that asks: {@link #applyBackstop} is the only caller in production, and it asks only behind a
     * statement of a class carrying no bound of its own - a deployment that gives {@code
     * bulk.timeout} a value has no such statement anywhere, and would never be told that its two
     * bounds are set the wrong way round.
     */
    int standingReadBoundMillis() {
        int millis=standingReadBound;
        if (millis < 0) {
            millis=CachedConnection.standingReadBoundMillis(poolKey());
            reportABoundNoStatementCanOutlive(millis);
            standingReadBound=millis;
        }
        return millis;
    }
    /**
     * Whether a standing read bound cuts a statement carrying a bound of its own short of it. Such
     * a statement then dies on the socket - which costs the connection the driver closes, and names
     * neither of the two properties that decided it - instead of being cancelled at the bound of its
     * own class. A statement of a class with no bound at all is not weighed here: {@link
     * #applyBackstop} takes the standing bound off for as long as one of those runs.
     * <p>
     * Weighed against {@link #backstopMillis} of that bound rather than against the bound itself,
     * because the cancel is not always there to come first: the catalog lookups of {@code openTree()}
     * ask {@code DatabaseMetaData}, which takes no query timeout at all, and any driver is free to
     * refuse one. What ends such a statement is the socket layer of its own class, a margin later,
     * and a standing bound anywhere below that ends it earlier - with {@link #applyBackstop} arming
     * nothing on top of it, since the connection already carries the tighter of the two, so the
     * failure arrives naming neither property.
     */
    static boolean cutsStatementsShort(int standingMillis, int statementSeconds) {
        return standingMillis > 0 && statementSeconds > 0 && standingMillis <= backstopMillis(statementSeconds);
    }
    /** The loosest bound a statement of this backend carries, and the property that gives it. */
    static final class LoosestBound {
        final int seconds;
        final String property;
        LoosestBound(int seconds, String property) {
            this.seconds = seconds;
            this.property = property;
        }
    }
    /**
     * The loosest bound a statement of this backend may be given: what a standing read bound has to
     * stand behind, since every one of those statements was told it may take that long.
     * <p>
     * Not {@link StatementBound#OPERATION} alone. The statistics refresh after an import has a
     * property of its own, ten minutes by default, and legitimately takes as long as a scan of the
     * table it describes; and a deployment that gives {@link StatementBound#BULK} a value takes that
     * class out of the lift of {@link #applyBackstop} and into this weighing, since a bulk statement
     * bounded by a property is a statement the standing bound can cut short like any other.
     */
    static LoosestBound loosestStatementBound() {
        int seconds=statisticsTimeoutSeconds();
        String property=STATISTICS_TIMEOUT_PROPERTY;
        for (final StatementBound bound : StatementBound.values()) {
            final int boundSeconds=bound.seconds();
            if (boundSeconds > seconds) {
                seconds=boundSeconds;
                property=bound.property;
            }
        }
        return new LoosestBound(seconds, property);
    }
    /**
     * Says once that the two bounds were set the wrong way round. The socket read timeout is the
     * layer behind the cancel of a statement, not in front of it: under the bound of the statement
     * it is the one that fires, and what the operator then sees is a connection closed by its driver
     * under a bare state of class 08 - {@link #timedOut} weighs the statement against its own bound,
     * finds it well inside, and passes the failure through as it found it.
     * <p>
     * Said where the backend opens rather than where the bound is first needed, and weighed against
     * {@link #loosestStatementBound()}: a deployment reaches this the moment it configures the two
     * the wrong way round, whatever kind of statement it goes on to run.
     */
    private void reportABoundNoStatementCanOutlive(int millis) {
        final LoosestBound loosest=loosestStatementBound();
        if (cutsStatementsShort(millis, loosest.seconds) && standingReadBoundWarned.compareAndSet(false, true)) {
            logger.warn(LocalizableMessage.raw("jdbc: the read bound of %s is %d ms, which a statement of this backend"
                + " reaches before the %d s of %s it is given: such a statement is cut by the socket read timeout,"
                + " closing the connection and naming neither property, rather than being cancelled at the bound of"
                + " its own class. A standing read bound stands behind the bound of a statement - behind the %d s"
                + " margin of that layer as well, since the cancel in front of it is one a driver may refuse and one"
                + " the catalog lookups of a tree are never given - so it has to be the longer of the two",
                CachedConnection.READ_TIMEOUT_PROPERTY, millis, loosest.seconds, loosest.property,
                BACKSTOP_MARGIN_SECONDS));
        }
    }
    public JDBCStorage(JDBCBackendCfg cfg, ServerContext serverContext) {
        this.config = cfg;
        cfg.addJDBCChangeListener(this);
@@ -136,6 +252,14 @@
        try
        {
            this.config = cfg;
            // The standing read bound is deliberately not reset here. It follows poolKey() - the
            // connection string open() registered the pool with, not the one config names now - so a
            // db-directory changed under a running backend does not move it. Reset, it would be
            // resolved again under a lift already in flight: applyBackstop() would find the answer of
            // another url for the connection whose read bound it has just taken off, fall through to
            // giveBack() and hand that bound back to the statements of an unbounded class still
            // running on it - the failure the lift exists to prevent, and one naming no property.
            // What does move it is a close and a re-open, which is where it is reset (releasePool()).
        }
        catch (Exception e)
        {
@@ -343,7 +467,7 @@
            return 0; // no connection to arm it on: the cancel is the whole bound of such a statement
        }
        synchronized (state) {
            return state.armed;
            return state.applied != null ? state.applied : 0; // a lift is a zero either way: nothing bounds it
        }
    }
@@ -430,6 +554,7 @@
    private final AtomicBoolean backstopUnsupportedWarned = new AtomicBoolean();
    private final AtomicBoolean backstopFailedWarned = new AtomicBoolean();
    private final AtomicBoolean queryTimeoutWarned = new AtomicBoolean();
    private final AtomicBoolean standingReadBoundWarned = new AtomicBoolean();
    /**
     * The socket read timeout of one connection, and the statements running on it. This second
@@ -454,10 +579,19 @@
        int unbounded;
        /** Statements holding this entry, bounded or not: at zero it leaves {@link #backstops}. */
        int holders;
        /** What the connection carried before the backstop armed it, and is given back afterwards. */
        /** What the connection carried before the backstop touched it, and is given back afterwards. */
        int previous;
        /** What the backstop has armed, or 0 when the connection carries {@link #previous}. */
        int armed;
        /**
         * What this backstop has put on the connection: {@code null} where it has put nothing and the
         * connection carries {@link #previous} of its own, 0 where the read bound is taken off for a
         * statement carrying none, and the value armed otherwise.
         * <p>
         * One field rather than a value beside a flag, because "nothing of ours is on this
         * connection" and "our lift is on it" are both a zero of that value: told apart by a boolean
         * beside it, the pair has to be tested together at every site that gives the connection back,
         * and an invariant spelled out at four sites is one three of them can be left out of.
         */
        Integer applied;
        /**
         * Set when the driver would not take a network timeout on this connection: it is not asked
         * again while the statements holding this entry run. A connection is the right scope for
@@ -538,6 +672,11 @@
        return (int) Math.min(Integer.MAX_VALUE, (seconds+BACKSTOP_MARGIN_SECONDS)*1000L);
    }
    /** Whether the read bound of this connection is the one this backstop took off for a statement carrying none. */
    private static boolean lifted(Backstop state) {
        return state.applied != null && state.applied == 0;
    }
    /**
     * Makes the socket read timeout of the connection what the statements in flight on it need: the
     * loosest of their bounds, or nothing of ours at all while one of them carries no bound. Called
@@ -557,29 +696,53 @@
        final int wanted=state.unbounded > 0 || state.bounds.isEmpty() ? 0 : state.bounds.lastKey();
        try {
            if (wanted == 0) {
                if (state.armed != 0) {
                    con.setNetworkTimeout(DIRECT_EXECUTOR, state.previous);
                    state.armed=0;
                // A statement of an unbounded class is running, and the connection carries the read
                // bound this backend gave it at its login (CachedConnection.READ_TIMEOUT_PROPERTY):
                // that bound comes off for as long as the statement does, since a statement told it
                // may take as long as it needs must not be cut by a value armed for another one.
                // Only ours is taken off - a read timeout standing in the connection string is the
                // deployment's own, and lifting it would hand the connection back to the pool with
                // the one bound its url asked for gone.
                if (state.unbounded > 0 && standingReadBoundMillis() > 0) {
                    if (state.applied == null) {
                        state.previous=con.getNetworkTimeout();
                    }
                    if (state.previous > 0) {
                        if (!lifted(state)) { // whether this backstop had armed a value or put nothing on at all
                            con.setNetworkTimeout(DIRECT_EXECUTOR, 0);
                            state.applied=0;
                        }
                        return;
                    }
                }
                giveBack(con, state);
                return;
            }
            if (state.armed == 0) {
            // What the connection carried is read once and remembered until it is given back. Read
            // again while the lift above holds, it would be the 0 of that lift - and the read bound
            // of the connection would go back to the pool gone for the rest of its life, which is
            // how a statement of an unbounded class outliving a bounded one on the same connection
            // takes the deployment's bound away for good.
            if (state.applied == null) {
                state.previous=con.getNetworkTimeout();
            }
            // only ever tighten: a connection that already carries a read timeout carries one a
            // deployment asked for, and this backstop exists to cap a cancel that is not acted
            // upon, not to relax anything. 0 is "no timeout" in the JDBC contract, so it is the
            // one value there is always something to gain by replacing.
            // deployment asked for - the bound standing in its url, or the standing bound of
            // CachedConnection.READ_TIMEOUT_PROPERTY this backend set at its login on their behalf -
            // and this backstop exists to cap a cancel that is not acted upon, not to relax
            // anything. The standing bound being ours to set makes it no less theirs to keep: it is
            // sized to stand behind every statement of this backend, and one that does not is said
            // where the backend opens (reportABoundNoStatementCanOutlive) rather than quietly worked
            // around here, which would leave the property meaning something other than what it says.
            // 0 is "no timeout" in the JDBC contract, so it is the one value there is always
            // something to gain by replacing.
            if (state.previous > 0 && state.previous <= wanted) {
                if (state.armed != 0) {
                    con.setNetworkTimeout(DIRECT_EXECUTOR, state.previous);
                    state.armed=0;
                }
                giveBack(con, state);
                return;
            }
            if (state.armed != wanted) {
            if (state.applied == null || state.applied != wanted) {
                con.setNetworkTimeout(DIRECT_EXECUTOR, wanted);
                state.armed=wanted;
                state.applied=wanted;
            }
        }catch (SQLException | RuntimeException e) {
            state.failed=true; // whatever the cause, this connection is not asked again while it runs
@@ -610,13 +773,25 @@
    }
    /**
     * Gives the connection back the read timeout it carried before this backstop armed one, and
     * forgets having armed it. Best effort by construction: the caller reaches this from a driver
     * call that has just failed, so the connection may well be gone - and where it is, it is the
     * driver that closes it rather than this backend.
     * Gives the connection back the read timeout it carried before this backstop touched it -
     * whether that was a bound armed for a statement or the lift of one that carries none - and
     * forgets having touched it. Nothing to do for a connection this backstop left alone.
     */
    private static void giveBack(Connection con, Backstop state) throws SQLException {
        if (state.applied == null) {
            return; // the connection carries its own value already
        }
        con.setNetworkTimeout(DIRECT_EXECUTOR, state.previous);
        state.applied=null;
    }
    /**
     * The same, best effort by construction: the caller reaches this from a driver call that has
     * just failed, so the connection may well be gone - and where it is, it is the driver that
     * closes it rather than this backend.
     */
    private static void restorePrevious(Connection con, Backstop state) {
        if (state.armed == 0) {
        if (state.applied == null) {
            return; // the connection carries its own value already
        }
        try {
@@ -625,7 +800,7 @@
            // nothing further can be done for this connection here, and the failure to report is the
            // one that brought us into the catch above
        }finally {
            state.armed=0;
            state.applied=null;
        }
    }
@@ -769,6 +944,14 @@
                CachedConnection.openPool(poolConnectionString);
                registeredHere=true;
            }
            // Resolved here, against the connection string the pool was just registered with, rather
            // than left to the first statement that needs it: applyBackstop() is the only caller in
            // production and reaches it only behind a statement of a class carrying no bound of its
            // own, so a deployment that gives bulk.timeout a value of its own reaches it nowhere at
            // all - and the word reportABoundNoStatementCanOutlive() owes an operator whose two
            // bounds are set the wrong way round would never be said. Costs one system property and
            // one scan of the url, once per open.
            standingReadBoundMillis();
            // 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
@@ -791,6 +974,7 @@
                // touching the pool: left standing it would send the close() of this storage to
                // releasePool() for a registration it never made.
                poolConnectionString=null;
                standingReadBound=STANDING_READ_BOUND_UNREAD; // it follows poolKey(), which the next open may register elsewhere
                poolRegistered.set(false);
            }
            throw e;
@@ -804,6 +988,7 @@
        if (poolRegistered.compareAndSet(true, false)) {
            final String registered=poolConnectionString;
            poolConnectionString=null;
            standingReadBound=STANDING_READ_BOUND_UNREAD; // it follows poolKey(), which a re-open may register elsewhere
            if (registered!=null) {
                CachedConnection.closePool(registered);
            }
@@ -823,31 +1008,37 @@
        // 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();
        // what this storage knows of its catalog holds no longer than the open it learnt it in: the
        // table may well be gone by the next one, dropped by an offline tool run in the meantime
        catalogTableOpened=false;
        enrolledTrees.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() -
    // and through it removeStorageFiles() - reads this, so a tree only belongs here once this
    // backend uses it: see toTableName() below for the trees that are merely asked about.
    // The trees this storage has taken an interest in, and the tables they map to: a memo, so that
    // naming the table of a tree costs a map lookup rather than a digest. What a backend owns is
    // recorded in its catalog and not here (#888) - listTrees() and removeStorageFiles() read that
    // - but the distinction the two names below draw is kept all the same: a tree merely asked
    // about is not one this storage has taken an interest in, and it stays out of the memo.
    final LoadingCache<TreeName,String> tree2table = Caffeine.newBuilder()
        .build(JDBCStorage::toTableName);
    /**
     * The table a tree name maps to. A pure function of the name, so that a tree can be read
     * without being entered into tree2table: the compressed schema reads the tree its definitions
     * used to be shared under (#873), a tree this backend does not own, and removeStorageFiles()
     * drops every table tree2table names.
     * The table a tree name maps to. A pure function of the name, so that a tree can be read without
     * being entered into tree2table: the compressed schema reads the tree its definitions used to be
     * shared under (#873), which is a tree this backend does not own.
     * <p>
     * Which of the two a statement takes therefore says who owns the tree it names: a path that
     * creates or writes one - openTree(), clearTree(), deleteTree(), put(), update(), delete() -
     * takes the enrolling {@link #getTableName(TreeName)}, and a read-only path - read(),
     * getRecordCount(), isExistsTable() and the cursor - takes {@link #readTableName(TreeName)},
     * which computes this only for a tree that is not enrolled already. Every tree this backend
     * owns passes through openTree(name, true) as it is opened, so listTrees() still names the
     * complete owned set.
     * Which of the two names a statement takes therefore says whether this backend is claiming the
     * tree it names: a path that creates or writes one - openTree(), clearTree(), deleteTree(), put(),
     * update(), delete() - takes the enrolling {@link #getTableName(TreeName)}, and a path that only
     * asks - read(), getRecordCount(), isExistsTable(), the cursor, and the read of what the catalog
     * records - takes {@link #readTableName(TreeName)}, which computes this only for a tree that is not
     * enrolled already. What a clear may drop is decided by the catalog of the backend (#888) and no
     * longer by this memo, so an entry of it puts no table up for removal; the two names are what keeps
     * the memo an account of the trees this backend claims all the same.
     */
    static String toTableName(TreeName treeName) {
        try {
@@ -870,6 +1061,85 @@
    }
    /**
     * The pseudo base DN of the tree naming the trees of a backend. Every real tree of a backend is
     * named after an entry container, whose prefix is a normalized DN and so always holds a "=",
     * which an identifier of this form cannot collide with.
     */
    static final String CATALOG_BASE_DN="opendj_catalog";
    /**
     * The base DN the compressed schema trees were named under before #881 gave each backend a pair
     * of its own. It carries no backend qualifier, so on a database addressed by several backends -
     * which nothing forbids (#873) - that pair of trees is the same pair for all of them, and a
     * backend must not put a tree another one may be the owner of up for removal. It is the pair
     * {@code PersistentCompressedSchema} migrates from and never writes to again, and it is left
     * exactly where it lies: the definitions of a backend that has not been started since the
     * upgrade are still in it. The pair each backend owns is named after its backend id, is under no
     * such literal, and is enrolled like any other tree.
     */
    static final String SHARED_COMPRESSED_SCHEMA_BASE_DN="compressed_schema";
    /**
     * The pair named under {@link #SHARED_COMPRESSED_SCHEMA_BASE_DN}, spelled out here because the
     * names are private to {@code PersistentCompressedSchema} - where they are the LEGACY_ pair of
     * #881. They are never enrolled, so nothing but this constant can name them - and a tool asking
     * a backend what trees it holds has to be told about them all the same, which is what {@link
     * #listTrees()} uses this for.
     */
    static final List<TreeName> SHARED_COMPRESSED_SCHEMA_TREES=Collections.unmodifiableList(Arrays.asList(
        new TreeName(SHARED_COMPRESSED_SCHEMA_BASE_DN, "compressed_attributes"),
        new TreeName(SHARED_COMPRESSED_SCHEMA_BASE_DN, "compressed_object_classes")));
    /**
     * The tree naming the trees this backend owns: one row per tree, the tree name as its key and the
     * table holding that tree as its value.
     * <p>
     * A table is named after the hash of its tree name, so the catalog of a database can neither be
     * filtered by a per-backend prefix nor read back into a {@link TreeName}. Without a record of its
     * own a backend can therefore only name the trees this very process has already touched - which
     * is precisely what {@link #removeStorageFiles()} cannot have, running as it does before the root
     * container is open. In the offline {@code import-ldif} nothing has touched a tree at all, so
     * {@code --clearBackend} used to clear nothing whatsoever (#888).
     * <p>
     * The catalog is per backend and named after the backend id alone: a process that has opened
     * nothing can still find its table, and backends sharing one database URL - which nothing
     * forbids (#873) - never name each other's trees. The id goes in escaped, for the reason {@link
     * #escapedBackendId} states: a name that does not survive being read back is a table of this
     * backend that its own clear cannot recognize.
     */
    TreeName getCatalogTree() {
        return new TreeName(CATALOG_BASE_DN, escapedBackendId());
    }
    /**
     * Whether the table of the catalog was created, or found, by this storage. A tree is enrolled on
     * every open - about 25 of them for a stock suffix - and asking the catalog whether the table is
     * there would cost a metadata round trip per tree.
     */
    private volatile boolean catalogTableOpened=false;
    /**
     * Serializes the one step above: two transactions opening trees at the same time would otherwise
     * both find the table of the catalog absent and both create it, the second failing the open it
     * belongs to. Held across the lookup and the statement that answer it, and across nothing else.
     */
    private final Object catalogLock=new Object();
    /**
     * The trees the catalog already records at the table this version would record them at, read
     * from it when this storage first opens it and added to as it enrols. A tree named here needs no
     * row written for it: the row would be the one that is already there, and writing one is a
     * statement and a commit on a connection this backend then has to have opened - a stock suffix
     * has about 25 trees, and every open after the first enrols none of them.
     * <p>
     * A row recording another table than {@link #getTableName} would give is not in here: what a
     * removal drops is the table the row records, so a row of a version naming its tables otherwise
     * has to be rewritten rather than trusted. Held no longer than the open it was read in, like
     * {@link #catalogTableOpened}, and given up whenever the catalog itself is.
     */
    private final Set<TreeName> enrolledTrees=ConcurrentHashMap.newKeySet();
    /**
     * The table a tree name maps to, for a statement that only reads it. Answered from the memo of
     * {@link #getTableName(TreeName)} where the tree is in it, and computed without being put there
     * otherwise.
@@ -894,10 +1164,10 @@
     */
    static String storedIdentifier(DatabaseMetaData metaData, String name) throws SQLException {
        if (metaData.storesUpperCaseIdentifiers()) {
            return name.toUpperCase();
            return name.toUpperCase(Locale.ROOT);
        }
        if (metaData.storesLowerCaseIdentifiers()) {
            return name.toLowerCase();
            return name.toLowerCase(Locale.ROOT);
        }
        return name;
    }
@@ -1049,7 +1319,7 @@
    boolean isMysqlBackslashEscape(Connection con) throws SQLException {
        try (final PreparedStatement statement=con.prepareStatement("select @@sql_mode")) {
            final String sqlMode=executeResultSet(statement, rs -> rs.next() ? rs.getString(1) : null);
            return sqlMode==null || !sqlMode.toUpperCase().contains("NO_BACKSLASH_ESCAPES");
            return sqlMode==null || !sqlMode.toUpperCase(Locale.ROOT).contains("NO_BACKSLASH_ESCAPES");
        }
    }
@@ -1084,6 +1354,417 @@
        return con;
    }
    /**
     * A connection of its own for the catalog of a backend, outside the pool for the reason a stamp
     * connection is: the caller of openTree() is inside a transaction and holding a pooled connection
     * already, and a pool that cannot open a second one waits for a peer to return one - which here is
     * the very thread that is waiting.
     * <p>
     * It is established the way a pooled connection is and not the way a stamp connection is: the
     * bounds of {@link CachedConnection.ConnectDialect} rather than of {@link Dialect}, so that a
     * login which never answers is bounded, a bound the administrator set in the connection string is
     * left exactly as they set it, and the read bound of the login is lifted as soon as the login is
     * through (#872). A stamp is a diagnostic aid and gives up rather than queue behind another
     * session; a catalog row is the state a clear reads, and it waits for its lock rather than dying
     * on a read bound. The isolation is the pool's for the same reason: this connection issues the
     * ordinary DML of this class, and the repeatable read a mysql server defaults to gap-locks a
     * catalog two transactions enrol into.
     * <p>
     * The bound of the connect is the one the pool bounds its own connects by, read from {@link
     * CachedConnection#CONNECT_TIMEOUT_PROPERTY} where an operator set it: a login of this database
     * takes what it takes whoever is asking, so a deployment which had to raise that property must not
     * meet a bound of this code's own here - a connect failing where the pooled one beside it succeeds
     * is a backend that stops opening on an installation that opened before this connection existed. A
     * property of 0 is the operator asking for no bound of the connect, and it is honoured here as it
     * is by the pool. What does bound an attempt besides is the deadline of the retry below, which is
     * the pool's own rule and applies to a borrow in exactly the same way; it is no bound of this
     * code's own choosing.
     * <p>
     * The deadline of the whole thing is the pool's as well, {@link
     * CachedConnection#POOL_TIMEOUT_PROPERTY}: a database that takes no connection <em>for the
     * moment</em> - at its connection limit with one of ours on its way back to the pool, or still
     * recovering - is waited out here exactly as a borrow waits it out, by the predicate the pool
     * decides that by ({@link CachedConnection#isWorthRetrying}) and with the same backoff. Without
     * it this connect makes one attempt where the borrow beside it makes many, and loses a race the
     * pooled connection of the very same operation wins. Everything else - a password that is not
     * accepted, a database that is down, a driver that is not on the classpath - is reported to the
     * caller rather than retried behind its back.
     * <p>
     * What this deadline is not is the deque of the pool: the caller of {@code openTree()} is holding
     * a pooled connection already, so waiting for a peer to return one would be waiting for the very
     * thread that is waiting. It is the pool's retry that is wanted here and not its queue, which is
     * why the loop below is its own rather than a borrow of {@link CachedConnection#getConnection}.
     * What one attempt is, is the login and the set-up behind it, exactly as an attempt of a borrow is
     * ({@code CachedConnection.connect}): a session the server takes and then kills off answers the
     * first statement of the set-up rather than the login, and it is the same refusal either way.
     * <p>
     * That is also what this wait is weaker than a borrow at, and it is worth writing down rather than
     * leaving to be discovered: a borrow can be answered by a peer handing a connection back, while
     * nothing here can be answered by anything but a new login. Against a server at its connection
     * limit whose remaining slots this backend's own pool is holding idle, the borrows of that pool
     * clear and this does not - it waits out the deadline and reports the refusal. The deadline is
     * therefore what bounds it, and a deployment which has set both properties to 0 has asked for a
     * wait with no end to it here as much as in the pool.
     * <p>
     * One attempt is bounded by the configured connect timeout and by what is left of that deadline,
     * whichever is the shorter, exactly as an attempt of a borrow is: an attempt left to run its own
     * bound out past the deadline would overrun it by a whole connect timeout, and turning the
     * per-attempt bound off must not turn the deadline off with it. So the connect property of 0 that
     * the round before this one made honoured is the operator asking for no bound <em>of their own</em>
     * here as it is in the pool, and what is left unbounded by both properties at 0 is left unbounded
     * here too - a login the database accepts and never finishes then parks the transaction that asked
     * for it. A borrow of the pool parks in exactly the same way, on exactly the same pair of settings.
     * It does not park the rest of this storage: {@code openCatalog()} establishes this connection
     * before it takes its lock, for that very reason.
     * <p>
     * What every wait here does hold is the caller: this runs inside the write transaction that reached
     * {@code openTree}, so a retry that waits out a database refusing connections holds that
     * transaction's pooled connection, the permit of the pool that connection carries (#878) and every
     * lock the transaction has already taken, for as long as it waits. On a stock suffix {@code
     * RootContainer.open()} is one such write over every tree of the backend.
     * <p>
     * And what it spends besides is the window {@link #write} bounds its own replay by, which is the
     * shorter of the two by default - ten seconds against a minute - and is <em>spent</em> by this wait
     * rather than added to it: this loop runs inside one attempt of that one. A refusal {@code write()}
     * would replay - mysql answers its connection limit with {@code 08004}, postgres reports a database
     * still coming up as {@code 57P03}, and both are read as a connection this backend lost - waited out
     * here for a minute reaches that loop with its window six times over, so it is thrown unreplayed:
     * the retry would have cost the caller the very replay it had before there was any retry here at
     * all. So the deadline is the shorter of {@link CachedConnection#POOL_TIMEOUT_PROPERTY} and what is
     * left of that window, taken from the caller by {@link CatalogSession#boundedAlsoBy}. The window is
     * not something the property could express: lowering it under ten seconds shortens every borrow of
     * the pool with it. A path carrying no such window - the importer, which has no replay above it -
     * waits the property out in full, and a deployment which cannot afford a minute of that sets the
     * property to what it can afford, the same property bounding the same wait as it bounds a borrow.
     * <p>
     * What the window does <em>not</em> bound is one attempt, which is taken from the deadline of the
     * pool as it always was. The two are different questions: the window says how long it is worth
     * waiting before handing the failure to a loop that can still replay it, while a login takes what
     * this database takes whoever is asking. Cut to what is left of a ten second window, a deployment
     * that raised {@link CachedConnection#CONNECT_TIMEOUT_PROPERTY} to two minutes because its login
     * needs them would meet a catalog connect failing where the pooled connection beside it succeeds -
     * the backend that stops opening. So one slow attempt may outlast the window, exactly as one slow
     * conflict outlasts it in {@link #write} itself; what may not is a second attempt begun after the
     * window has already run out, which is a wait bought with a replay that no longer exists.
     *
     * @param budgetDeadline the moment the replay window of the caller runs out, as {@link
     *        System#currentTimeMillis()} reads it, or {@link Long#MAX_VALUE} where nothing above this
     *        connect replays - the same "no deadline at all" this class reads out of {@link
     *        CachedConnection#deadlineOf}, so that the shorter of the two is a plain {@code min}.
     */
    Connection newCatalogConnection(long budgetDeadline) throws SQLException {
        // poolKey() rather than the configuration as it stands, for the reason newStampConnection()
        // gives: 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
        // write the catalog of this backend into whichever database the configuration names now,
        // while its tables are created, read and dropped over the connection open() registered -
        // rows in one database and tables in another, which is #888 again by another route (#878)
        final String connectionString=poolKey();
        final CachedConnection.ConnectDialect dialect=CachedConnection.ConnectDialect.of(connectionString);
        final long connectTimeoutSeconds=CachedConnection.getConnectTimeoutSeconds();
        final long poolTimeoutSeconds=CachedConnection.getPoolTimeoutSeconds();
        final long startedAt=System.currentTimeMillis();
        final long poolDeadline=CachedConnection.deadlineOf(startedAt, poolTimeoutSeconds);
        // the deadline of the whole wait, which is the shorter of the pool's own and what is left of
        // the replay window of the caller. The bound of one attempt below is taken from the pool's
        // alone, deliberately: a login the operator bounded at two minutes because that is what this
        // database takes must not be cut to what is left of a ten second window - that is the connect
        // dying where the pooled one beside it succeeds, which is a backend that stops opening. One
        // slow attempt may still outlast the window, exactly as one slow conflict does; what may not
        // is a second attempt begun after the window has run out, which is a wait for nothing
        final long deadline=Math.min(poolDeadline, budgetDeadline);
        long backoffMs=0;
        int attempts=0;
        while (true) {
            attempts++;
            try {
                // the bound of one attempt and not of the whole wait, the way the pool bounds its own:
                // an attempt left to run its bound out past the deadline would overrun it by a full
                // connect timeout, and turning the per-attempt bound off must not turn this one off.
                // Taken from the deadline of the pool and not from the shorter of the two: see above
                return connectCatalog(connectionString, dialect,
                    CachedConnection.attemptSeconds(connectTimeoutSeconds, poolDeadline));
            }catch (SQLException e) {
                if (!CachedConnection.isWorthRetrying(e, dialect)) {
                    // redacted the way the pool redacts the failure of its own connects: a driver renders
                    // the connection string it could not use into its message as readily as not, and the
                    // connection string of this backend carries the password of the account it works as.
                    // This failure is reported in full - ERR_OPEN_ENV_FAIL, or the log of a clear
                    throw CachedConnection.reported(e, connectionString);
                }
                final long now=System.currentTimeMillis();
                final long remaining=deadline-now;
                if (remaining<=0) {
                    // which of the two bounds ended it, so that an operator reading the line knows whether
                    // the property is the thing to raise: where the replay window of the caller is the
                    // shorter one, raising the property moves nothing
                    throw catalogConnectTimedOut(connectionString, poolTimeoutSeconds,
                        deadline==budgetDeadline, now-startedAt, attempts, e);
                }
                CachedConnection.warnStallOutsidePool(connectionString, "tree catalog", attempts, startedAt, e);
                backoffMs=Math.min(backoffMs==0 ? 1 : backoffMs*2, CachedConnection.MAX_BACKOFF_MS);
                try {
                    Thread.sleep(Math.min(backoffMs, remaining));
                }catch (InterruptedException interrupted) {
                    // the flag is put back - Thread.sleep() clears it, and every frame above this one reads
                    // it to decide whether to unwind - and the wait is over: whoever asked this thread to
                    // stop is not answered by going on to sleep out the rest of a pool timeout. The driver
                    // failure is what this reports, it being the reason there was anything to wait for, and
                    // the interrupt is carried on it as suppressed so that a connect cut short by a
                    // shutdown is not read off the log as a database that would not take a connection
                    Thread.currentThread().interrupt();
                    final SQLException reported=CachedConnection.reported(e, connectionString);
                    reported.addSuppressed(interrupted);
                    throw reported;
                }
            }catch (RuntimeException e) {
                // a driver reporting a connect it will not make as an unchecked failure names the
                // connection string just as readily, and it is not one of the two states a retry waits
                // out: reported and handed on, exactly as the pool hands its own on. reportedUnchecked()
                // answers with the original where it holds no credential, so nothing of a plain
                // programming error is hidden by this
                final Exception reported=CachedConnection.reportedUnchecked(e, connectionString);
                if (reported instanceof SQLException) { // redacted, and reported as the connect failure it is
                    throw (SQLException) reported;
                }
                throw (RuntimeException) reported; // the original: it holds no credential of this backend
            }
        }
    }
    /**
     * The failure of a catalog connect that was worth retrying and ran the deadline out: a timeout by
     * type, so that a caller can tell it from the first refusal, and carrying the state and the vendor
     * code of the last failure of the driver rather than one of its own.
     * <p>
     * Not the {@code 08001} the pool answers a borrow of this shape with, and the difference is not
     * cosmetic: this failure is raised inside {@link #write}, whose classification reads every state
     * of class {@code 08} as a connection the database dropped ({@link #saysTheConnectionIsGone}). A
     * manufactured one would put an attempt whose pooled connection is perfectly healthy into the
     * replay and call {@link #distrustPool} on it over a database that had simply refused a new
     * connection.
     * <p>
     * It buys exactly that and no more, which is worth being precise about: where the driver's own
     * refusal is of class {@code 08} - mysql answers its connection limit with {@code 08004} - the
     * attempt is classified as a dropped connection whatever this method does, the original being the
     * cause of this one and every chain of a failure being walked. What this keeps is the promise that
     * the retry changes no classification: a refusal reaches {@code write()} as the same thing it
     * reached it as before there was any retry here at all.
     * <p>
     * Which of the two bounds ended the wait is named rather than left to be guessed: the property is
     * the thing to raise only where the property is what ran out, and where the replay window of the
     * caller is the shorter one - the default has it at a sixth of the property - raising the property
     * moves nothing at all.
     */
    private static SQLTimeoutException catalogConnectTimedOut(String connectionString, long poolTimeoutSeconds,
            boolean endedByReplayWindow, long waitedMs, int attempts, SQLException last) {
        final SQLTimeoutException timeout=new SQLTimeoutException("no connection to "
            +CachedConnection.safeUrl(connectionString)+" could be opened for the tree catalog within "
            +waitedMs+"ms ("+attempts+" attempts, "+(endedByReplayWindow
                ? "what was left of the replay window of the write that asked for it, which is the shorter"
                    +" bound here: "+CachedConnection.POOL_TIMEOUT_PROPERTY+" is "+poolTimeoutSeconds+"s"
                : CachedConnection.POOL_TIMEOUT_PROPERTY+"="+poolTimeoutSeconds+"s")
            +"): the database took no connection for the moment,"
            +" last error: "+CachedConnection.redact(last.getMessage(), connectionString),
            last.getSQLState(), last.getErrorCode());
        timeout.initCause(CachedConnection.reported(last, connectionString));
        return timeout;
    }
    /**
     * One attempt of {@link #newCatalogConnection}, established and set up or left holding nothing.
     * Failures leave here as the driver reported them, checked and unchecked alike: what a retry is
     * decided on is the chain of the original, and the redaction is the caller's - a redacted copy is
     * rebuilt link by link, so redacting an attempt that is about to be retried would pay for a
     * failure nobody ever sees.
     */
    private Connection connectCatalog(String connectionString, CachedConnection.ConnectDialect dialect,
            long timeoutSeconds) throws SQLException {
        // A driver is free to write into the map it is handed, so every attempt gets one of its own.
        final Properties properties=new Properties();
        final boolean readBoundSet=dialect!=null && timeoutSeconds>0
            && dialect.bound(connectionString, properties, timeoutSeconds);
        final Connection con=DriverManager.getConnection(connectionString, properties);
        try {
            con.setAutoCommit(false);
            con.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
        }catch (SQLException | RuntimeException e) { // nothing else holds this connection yet: it would leak
            closeQuietly(con, e);
            throw e;
        }
        if (readBoundSet) {
            try {
                // only where this code set one: a read bound of the connection string is the
                // administrator's and is not lifted along with it, exactly as the pool leaves it
                con.setNetworkTimeout(Runnable::run, 0);
            }catch (SQLException | RuntimeException e) {
                // A driver that will not take the bound back leaves it in force for the life of the
                // connection, and that bound is the one this attempt was given - near the end of the
                // deadline of the retry, a second. The connection is kept all the same, which is the
                // pool's own answer to this failure: it stops pooling such a connection and still hands
                // it to the borrower that is waiting. Failing here instead would stop the backend opening
                // on a driver whose setNetworkTimeout is not implemented at all, where the pooled
                // connection beside it works - and there is no state to fail with that write() does not
                // read as a connection the database dropped. So it is reported, at the bound in force.
                logger.warn(LocalizableMessage.raw("jdbc: the catalog connection of backend %s keeps the %ds read bound its login was given, so a statement of the catalog slower than that fails on it: %s",
                    config.getBackendId(), timeoutSeconds, stackTraceToSingleLineString(e)));
            }
        }
        return con;
    }
    /** Closes a connection nothing holds yet, reporting the failure of the close on the one being unwound. */
    private static void closeQuietly(Connection con, Throwable unwinding) {
        try {
            con.close();
        }catch (SQLException | RuntimeException e) {
            // the unchecked one as well: this runs from the catch of a failure it must not replace
            // (JLS 14.20.2), which is the rule every close of this class keeps
            unwinding.addSuppressed(e);
        }
    }
    /**
     * The connection the catalog table of a backend is read and written on, and the transaction over
     * it. No other connection touches that table.
     * <p>
     * It belongs to a write transaction and not to the storage, and is opened at the first row that
     * transaction has to write - so what costs a physical connect is a write which enrols a tree the
     * catalog does not already record, and nothing else: a read-only storage opens none, a
     * transaction that opens no tree opens none, and neither does one whose trees are all recorded
     * already, which is every open after the first ({@link JDBCStorage#enrolledTrees} is the storage's and
     * outlives them). The open of a backend is therefore one connect, and so is every later write
     * that names a tree for the first time - {@code dsconfig create-backend-index} reaches exactly
     * that, opening its new tree inside a write of its own on a running server. The connect is
     * retried the way the pool retries its own for that reason; see {@link JDBCStorage#newCatalogConnection}.
     * <p>
     * Storage-scoped rather than transaction-scoped it cannot be: it is closed with the transaction
     * because the rows it writes are the transaction's, and a connection outliving them would be a
     * second pooled-connection lifetime for this class to get right.
     * <p>
     * Why the rows are not written on the caller's connection is in {@link
     * WriteableTransactionTransactionImpl#enrolInCatalog}: they have to be committed, and that commit
     * must not be the caller's. Why the read is not either is in {@link
     * WriteableTransactionTransactionImpl#readEnrolledTrees}: a select of the caller's transaction
     * would hold a lock on the catalog table for the whole life of that transaction, and the rows it
     * decides are written from here.
     */
    final class CatalogSession implements Closeable {
        private Connection con;
        private WriteableTransactionTransactionImpl txn;
        // The moment the replay window of the write() this session belongs to runs out, as
        // System.nanoTime() reads it - null where nothing above this session replays, which is the
        // importer and nothing else. Boxed rather than given a sentinel: nanoTime() is documented to
        // return an arbitrary long, so there is no reading of it that could stand for "no window".
        private Long replayWindowEndsAt;
        /**
         * Tells this session the wall-clock window the {@link JDBCStorage#write} above it bounds its
         * replay by, which the connect of the catalog may not outlast: the connect runs inside one
         * attempt of that loop, so a wait longer than what is left of the window reaches it with the
         * window already spent and is thrown unreplayed - see {@link JDBCStorage#newCatalogConnection}.
         * Called once per attempt, before the operation runs; a session nobody calls it on waits the
         * pool timeout out in full, which is what the importer does.
         */
        void boundedAlsoBy(long replayWindowEndsAtNanos) {
            replayWindowEndsAt=replayWindowEndsAtNanos;
        }
        /**
         * That window as a deadline of the clock the connect measures itself by, or {@link
         * Long#MAX_VALUE} where there is no window - the value {@link CachedConnection#deadlineOf}
         * gives a wait with no end, so that the shorter of the two is a plain {@code min}. The two
         * readings are taken here rather than one of them being carried in: a nanoTime window and a
         * currentTimeMillis deadline are two clocks, and they can only be put together at one moment.
         * A window already spent gives the moment itself, which is one attempt and then a timeout.
         */
        private long budgetDeadline() {
            if (replayWindowEndsAt==null) {
                return Long.MAX_VALUE;
            }
            final long leftNanos=replayWindowEndsAt-System.nanoTime();
            final long now=System.currentTimeMillis();
            return leftNanos<=0 ? now : now+leftNanos/1_000_000L;
        }
        /** The connection, opened at the first read or write the catalog needs and shared by the rest. */
        Connection connection() throws SQLException {
            if (con==null) {
                con=newCatalogConnection(budgetDeadline());
            }
            return con;
        }
        /** Whether this session is holding a connection already, so that a caller knows what it made. */
        boolean isEstablished() {
            return con!=null;
        }
        /**
         * A transaction over that connection, for its row statements alone: an upsert and a delete are
         * per engine, and writing the catalog through the very ones every other tree is written through
         * is what keeps its rows the same shape as theirs. It opens no tree and stamps no table, so the
         * sessions it carries of its own are never opened.
         */
        WriteableTransactionTransactionImpl transaction() throws SQLException {
            final Connection con=connection();
            if (txn==null) {
                txn=new WriteableTransactionTransactionImpl(con);
            }
            return txn;
        }
        void commit() throws SQLException {
            con.commit();
        }
        /**
         * What a failed statement left behind must not poison the write of the next row: postgres
         * refuses every further statement of a transaction whose statement failed (25P02) until it is
         * rolled back, and this connection outlives the row that failed on it.
         */
        void reset() {
            if (con!=null) {
                try {
                    con.rollback();
                }catch (SQLException | RuntimeException e) {
                    // the unchecked one as well: a driver is free to answer a rollback on a connection the
                    // database dropped with one, and this runs from the catch of a failure it must not
                    // replace - the caller goes on to report that failure, and in createCatalogTable() to
                    // tolerate a table another session created while this one was creating it
                    close();
                }
            }
        }
        /**
         * The unchecked failure of a close is taken like the checked one, and the session is given up
         * in a finally: this is called from {@link #reset}, which runs from the catch of a failure it
         * must not replace (JLS 14.20.2) - {@code createCatalogTable()} goes on from there to tolerate
         * a table another session created while this one was creating it. A session whose connection
         * would not close is left holding none rather than holding a dead one.
         * <p>
         * The catch of {@code write()}'s own finally is the same guard one layer out, kept as the
         * belt to this one's braces: it was the only guard while this method let an unchecked failure
         * past, and a session that stops swallowing must not have to be found through a failure it
         * replaced.
         */
        @Override
        public void close() {
            if (con!=null) {
                try {
                    con.close();
                }catch (SQLException | RuntimeException e) {
                    logger.trace(LocalizableMessage.raw("jdbc: unable to close the catalog connection: %s", stackTraceToSingleLineString(e)));
                }finally {
                    con=null;
                    txn=null;
                }
            }
        }
    }
    // The connection the comment statements of one sweep of openTree() calls share. Opening a
    // backend opens every tree it holds (about 25 for a stock suffix), so a connection per stamp
    // would mean that many physical connects on the first open after an upgrade - the one open
@@ -1134,21 +1815,32 @@
            if (con!=null) {
                try {
                    con.rollback();
                }catch (SQLException e) {
                }catch (SQLException | RuntimeException e) {
                    // the unchecked one as well: a driver is free to answer a rollback on a connection the
                    // database dropped with one, and this runs from the catch of a failure it must not
                    // replace - the caller goes on to report that failure, and in createCatalogTable() to
                    // tolerate a table another session created while this one was creating it
                    close();
                }
            }
        }
        /**
         * The unchecked failure of a close is taken like the checked one, and the session is given up
         * in a finally, for the reason {@link CatalogSession#close} gives: this runs from {@link
         * #reset}, which runs from the catch of a failure it must not replace - and a stamp that
         * failed must never become the outcome of the open it was issued from.
         */
        @Override
        public void close() {
            if (con!=null) {
                try {
                    con.close();
                }catch (SQLException e) {
                }catch (SQLException | RuntimeException e) {
                    logger.trace(LocalizableMessage.raw("jdbc: unable to close the comment connection: %s", stackTraceToSingleLineString(e)));
                }finally {
                    con=null;
                }
                con=null;
            }
            mysqlBackslashEscape=null; // it described the session that has just gone
        }
@@ -1337,8 +2029,10 @@
    }
    // Returns the comment currently stored on the table, or null when there is none. The dialect is
    // passed in rather than read off the connection: this runs on the stamp connection, which is
    // not a pooled one, and only for the dialects commentTable() recognizes.
    // passed in rather than read off the connection: the stamp sweep runs this on a connection of its
    // own, a clear runs it on the pooled one it did its work on, and both only for the dialects
    // commentTable() recognizes. It is a read and nothing else, and CachedConnection.close() rolls
    // back before the connection is handed on, so a clear leaves no transaction of its own behind.
    String readStoredComment(Connection con, Dialect dialect, String tableName) throws SQLException {
        final String sql;
        final String arg;
@@ -1353,7 +2047,7 @@
            break;
        case ORACLE:
            sql="select comments from user_tab_comments where table_name=?";
            arg=tableName.toUpperCase();
            arg=tableName.toUpperCase(Locale.ROOT);
            break;
        case MICROSOFT:
            sql="select cast(value as nvarchar(4000)) from sys.extended_properties where class=1 and major_id=object_id(?) and minor_id=0 and name='MS_Description'";
@@ -1377,6 +2071,17 @@
    static final String STATISTICS_TIMEOUT_PROPERTY=STATISTICS_PROPERTY+".timeout";
    private static final int STATISTICS_TIMEOUT_SECONDS_DEFAULT=600;
    /**
     * What the statistics refresh may take, as configured. Read where the refresh runs and again
     * where a standing read bound is weighed against the statements of this backend
     * ({@link #loosestStatementBound()}): it is the loosest bound any statement here is given by
     * default, so a standing bound under it cuts the refresh short of the very property that was
     * meant to bound it.
     */
    static int statisticsTimeoutSeconds() {
        return clampSeconds(Integer.getInteger(STATISTICS_TIMEOUT_PROPERTY,STATISTICS_TIMEOUT_SECONDS_DEFAULT));
    }
    // A bulk load leaves the optimizer statistics of freshly created tables stale (a table that
    // was never analyzed can make the planner badly misestimate the "where k>? order by k" cursor
    // batches - see OpenIdentityPlatform/OpenDJ#859), so refresh them once the data is in place.
@@ -1393,7 +2098,7 @@
        if (dialect==null) { // no portable statistics refresh for other engines
            return false; // nothing was refreshed: reporting success here would make the assertion of the tests vacuous
        }
        final int timeoutSeconds=clampSeconds(Integer.getInteger(STATISTICS_TIMEOUT_PROPERTY,STATISTICS_TIMEOUT_SECONDS_DEFAULT));
        final int timeoutSeconds=statisticsTimeoutSeconds();
        boolean allRefreshed=true;
        for (final TreeName treeName : trees) {
            final String tableName=getTableName(treeName);
@@ -1415,7 +2120,7 @@
                    break;
                case ORACLE:
                    sql="begin dbms_stats.gather_table_stats(user, ?); end;";
                    args=new String[]{tableName.toUpperCase()};
                    args=new String[]{tableName.toUpperCase(Locale.ROOT)};
                    break;
                case MICROSOFT:
                    sql="update statistics "+tableName;
@@ -1469,6 +2174,41 @@
        return allRefreshed;
    }
    /**
     * Whether a table of this name is one the given connection reaches: in its database, and in one of
     * the schemas an unqualified name of it resolves in - see {@link TableScope}, which is where the
     * reason for each half of that question is. Asked of the catalog by name rather than by listing
     * every table of the database: openTree(createOnDemand) asks it for every tree of the backend -
     * about 25 of them for a stock suffix - on every open, on a database this backend may well be
     * sharing with something else.
     */
    boolean isExistsTable(Connection con, TableScope scope, String tableName) {
        // bounded as the operation it is, not as the bulk statement it guards and not as the class of
        // the transaction that happens to ask (#882): it reads a data dictionary rather than the data,
        // so a wait here is the metadata lock of another session
        try {
            return bounded(con, StatementBound.OPERATION, () -> {
                final DatabaseMetaData metaData = con.getMetaData();
                // asked with no schema pattern and read through the scope instead: what an unqualified
                // statement reaches is a path of schemas and not one of them, and a pattern is no way to
                // name a path - nor an exact way to name even one of it, "_" being a wildcard there
                try (final ResultSet rs = metaData.getTables(scope.catalog, null,
                        storedIdentifier(metaData, tableName), new String[]{"TABLE"})) {
                    while (rs.next()) {
                        // the name still has to be compared: "_" is a single-character wildcard in a
                        // metadata pattern, so "opendj_<hash>" also matches a table named "opendjX<hash>"
                        if (tableName.equalsIgnoreCase(rs.getString("TABLE_NAME")) && scope.covers(rs)) {
                            return true;
                        }
                    }
                }
                return false;
            });
        } catch (Exception e) {
            throw new StorageRuntimeException(e);
        }
    }
    @Override
    public void removeStorageFiles() throws StorageRuntimeException {
        final boolean isOpen=getStorageStatus().isWorking();
@@ -1479,36 +2219,670 @@
                throw new StorageRuntimeException(e);
            }
        }
        final Set<TreeName> trees=listTrees();
        if (!trees.isEmpty()) {
            try (final Connection con = getValidatedConnection()) {
                try {
                    for (final TreeName treeName : trees) {
                        try (final PreparedStatement statement = con.prepareStatement("drop table " + getTableName(treeName))) {
                            execute(statement, StatementBound.BULK);
        try (final Connection con = getValidatedConnection()) {
            // where an unqualified name of this connection resolves, which every lookup below is
            // narrowed to: the skip in the loop decides between leaving a row where it is and dropping
            // the table it names, and a table of that name in another database of the server must not
            // be allowed to answer for this one - nor a table of this backend go unfound for living in
            // another schema of the search path than the one the connection works in
            final TableScope scope=TableScope.of(this, con);
            // the catalog names what this backend owns, and only that: listTrees() also names the
            // shared compressed schema trees, which another backend of this database may be the only
            // owner of and which a clear must therefore leave exactly where they lie (#881)
            final List<String> skippedRows=new ArrayList<>(); // rows the read could not act on: reported below
            final Map<TreeName,String> trees=catalogTables(con, scope, skippedRows);
            final TreeName catalogTree=getCatalogTree();
            int dropped=0;
            // the same count with the catalog itself left out, which is what says whether this clear
            // removed anything of the backend: a catalog table standing over rows that name nothing -
            // a backup restored older than the tables it was taken beside - is dropped like any other
            // and would otherwise make a clear that removed no tree at all look like a clear that did
            // something. See reportClearOutcome()
            int droppedTrees=0;
            // counted without the catalog, for the reason droppedTrees is kept apart from dropped: the
            // catalog is walked by this loop like any other table, so a catalog table that went between
            // the lookup of catalogTables() and the loop's own would otherwise be summed up as a tree of
            // this backend that had lost its table
            int missingTrees=0;
            try {
                for (final Map.Entry<TreeName,String> tree : trees.entrySet()) {
                    final String tableName=tree.getValue();
                    final boolean isCatalog=catalogTree.equals(tree.getKey());
                    if (!isExistsTable(con, scope, tableName)) { // a row of the catalog outliving its table
                        reportClearLine(LocalizableMessage.raw(
                            "jdbc: backend %s names tree %s, whose table %s is not there: nothing to drop for it",
                            config.getBackendId(), tree.getKey(), tableName));
                        if (!isCatalog) {
                            missingTrees++;
                        }
                        continue;
                    }
                    con.commit();
                } catch (SQLException e) {
                    dropTable(con, tableName);
                    dropped++;
                    if (!isCatalog) {
                        droppedTrees++;
                    }
                }
                con.commit();
            } catch (Exception e) {
                // every failure of the loop and not the SQLException alone: the lookup deciding each
                // drop answers with a StorageRuntimeException of its own, and a drop left pending by one
                // of those has to go back here rather than wait for the connection to be handed back
                try {
                    con.rollback();
                } catch (SQLException e2) {}
                throw e instanceof StorageRuntimeException ? (StorageRuntimeException) e : new StorageRuntimeException(e);
            }
            // all tables are gone: a table recreated later deserves a fresh stamp attempt, and the
            // memoized table name of a tree nothing holds any more is of no use to anyone
            for (final TreeName treeName : trees.keySet()) {
                tree2table.invalidate(treeName);
                unstampableTrees.remove(treeName);
            }
            try {
                reportClearOutcome(con, scope, dropped, droppedTrees, missingTrees, skippedRows);
            } catch (RuntimeException e) {
                // the clear itself is done and committed: an account of what it left standing must not be
                // the thing that reports it as failed, and a caller retrying it would find nothing to drop
                logger.trace(LocalizableMessage.raw("jdbc: unable to report what the clear left standing: %s",
                    stackTraceToSingleLineString(e)));
            }
        } catch (StorageRuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new StorageRuntimeException(e);
        } finally {
            // the catalog went with the rest: the next tree enrolled creates its table again. The
            // online import needs exactly that - the storage which has just dropped its tables is the
            // one going on to open a root container and enrol every tree of it anew, which is what the
            // forgotten enrolments make it do rather than skip as already recorded.
            catalogTableOpened=false;
            enrolledTrees.clear();
            if (!isOpen) {
                close();
            }
        }
    }
    /**
     * Drops one table of a clear. It is a method of its own so that the order {@link
     * #removeStorageFiles()} drops in can be watched from a test: what names the trees has to outlive
     * them, and that guarantee is the loop's - it holds because the loop walks the catalog's map in
     * the order that map was built in, and a test asserting on the map instead would go on passing
     * over a loop that had stopped doing so.
     */
    void dropTable(Connection con, String tableName) throws SQLException {
        try (final PreparedStatement statement = con.prepareStatement("drop table " + tableName)) {
            // bulk, as #882 made every drop of this backend: nobody waits on a clear, and what it takes
            // follows the size of the table rather than the work of a caller
            execute(statement, StatementBound.BULK);
        }
    }
    /**
     * Where a line of the account a clear gives of itself goes: the logger, and nothing else in
     * production. It is a method of its own so that a case can hold that account to what it says -
     * these lines change no state at all, so every assertion a clear can be given about the database
     * passes just as well with all of them deleted, and this report has now been changed in three
     * rounds of review with nothing able to fail. See {@code TestCase.ReportingStorage}, which
     * collects them.
     */
    void reportClearLine(LocalizableMessage line) {
        logger.warn(line);
    }
    /**
     * Reports what a clear did not remove, once everything the catalog named is gone.
     * <p>
     * An "opendj" table still standing at that point is named by no catalog of this backend, and its
     * name says nothing about whose it is - a table is named after the hash of its tree name. What
     * does say so is the comment a table is stamped with as it is opened (#866): the tree name in
     * plain text. A table whose stamp names a tree of a base DN this backend does not serve belongs to
     * a backend sharing this database (#873) and is passed over in silence; one whose stamp names a
     * tree of this backend is reported as its own, and so as removable by hand; one carrying no stamp
     * at all - left by a version stamping no table, or by a database that refused the comment - can be
     * attributed to nobody and is reported as exactly that. A stamp the database would not give up is
     * reported apart from all of these: it says nothing either way, and counting it as a table without
     * a stamp would turn a connection that died halfway into a confident line about tables this
     * backend may well own.
     * <p>
     * The silence has a cost worth stating: a table stamped with a tree of a base DN that was taken
     * out of the configuration while the backend was disabled reads exactly like a table of a backend
     * sharing the database, the stamp naming the tree and never the backend it belonged to, so it is
     * passed over too. What is left of such a base DN is found by its stamp and removed by hand.
     * <p>
     * The shared compressed schema pair is left out of all of it: it is kept on purpose (#881), so it
     * is no leftover of anything, and naming it here would be asking for the removal of the one thing
     * this code goes out of its way to spare.
     * <p>
     * A clear which removed no tree of this backend is called out ahead of all of it: #888 was exactly
     * such a clear, and it went by without a word in the log. A backend upgraded in place is the one
     * case where a clear drops nothing while there is something to drop - nothing enrols a tree before
     * {@link #removeStorageFiles()} runs, so the first offline clear of such a backend finds no
     * catalog at all - and the line says so rather than leaving it to be found out.
     * <p>
     * The catalog table is no term of that count. It is dropped like any other and by the same loop,
     * so a catalog standing over rows that name nothing - a backup restored older than the tables it
     * was taken beside - is one table dropped and not one tree removed, and the line has to fire there
     * too: what an operator meets in that case is the same clear that removed none of their data.
     * <p>
     * A database which would not say what is standing gets a line of its own, whatever the clear
     * dropped. What was left behind is exactly what could not be found out there, so it is no more a
     * clear that left nothing than one that left something, and the count of what it did drop is the
     * only thing that can still be stated: reporting it through the line above would say "the clear
     * dropped no table at all" of a clear that dropped a dozen.
     * <p>
     * A row of the catalog the read passed over is reported wherever the clear got to, that line
     * depending on nothing this database was asked afterwards: what such a row records is outside the
     * namespace {@link #leftoverTables} scans, so no other line here can name it. The row itself does
     * not survive the clear - the catalog names itself last and the loop drops that table with every
     * row still in it - which is why the line is the only surviving copy of what the row said, and
     * why it names what the row recorded rather than telling an operator to go and look. Nothing this
     * version writes makes such a row - {@link #getTableName} names every table {@code opendj_<hash>}
     * - so it is the account of a database written into by something else.
     * <p>
     * It is a term of the "dropped nothing" line all the same, and for one state only: a catalog whose
     * table is there names itself, so a clear reading any row at all normally drops that one and the
     * term is carried by the drop count beside it. Where it is not is where the catalog table went
     * between the read of its rows and the loop that drops them - another process clearing the same
     * backend - and there the clear has read a row, dropped nothing, and has this row as the whole of
     * what it can say. Without the term it says nothing at all, which is the silence of #888.
     */
    void reportClearOutcome(Connection con, TableScope scope, int dropped, int droppedTrees, int missingTrees,
            List<String> skippedRows) {
        final ClearLeftovers leftovers=leftoverTables(con, scope);
        if (leftovers==null) {
            // a line of its own and not a clause of the one below: this says nothing about whether
            // anything was left behind, so a clear that dropped its tables must not be reported here as
            // one that dropped none - and one that dropped none must still say so, that silence being
            // the whole of #888
            reportClearLine(LocalizableMessage.raw("jdbc: backend %s: %s (it dropped %d table(s) in all, its own catalog among them where there was one) and %d of the trees its catalog names had lost their table already; what else is standing could not be read off this database, so this clear says nothing about it.%s",
                config.getBackendId(), removedTrees(droppedTrees), dropped, missingTrees,
                droppedTrees==0 ? " "+CLEAR_DROPPED_NOTHING : ""));
            reportSkippedRows(skippedRows); // read off the catalog and not off this database: still worth stating
            return;
        }
        final int ours=leftovers.ours.size();
        final int unattributed=leftovers.unattributed.size();
        final int unreadable=leftovers.unreadable.size();
        // first of the lines, and not last: on a backend upgraded in place every table of it is
        // unstamped and lands in the list below, and the operator has to be told why before being
        // handed a list of tables their own backend is very probably still using.
        // Decided on the drops of trees and not on every drop: the catalog table is dropped by the same
        // loop, so a catalog standing over rows that name nothing makes "dropped" one while no tree of
        // this backend was removed - which is the state this line exists to explain.
        // Each tree which had lost its table is logged as the loop skips it; this line only sums them up.
        // "there was something to act on" and not "something is still standing": a clear which dropped
        // its own catalog and removed no tree of the backend is the #888 outcome exactly, and it says so
        // whether or not the scan afterwards found anything to attribute. Without the two terms on the
        // right the line is silent in that case while the same clear on a database whose listing failed
        // announces itself - the same clear, told two ways.
        // The last of them is not spare: a clear normally drops the catalog table it read its rows out
        // of, so a passed-over row comes with a drop - except where that table went while this clear was
        // running, which is a clear that read a row, dropped nothing, and has that row as all it can say
        if (droppedTrees==0 && (missingTrees>0 || ours>0 || unattributed>0 || unreadable>0
                || dropped>0 || !skippedRows.isEmpty())) {
            reportClearLine(LocalizableMessage.raw("jdbc: backend %s: %s (it dropped %d table(s) in all, its own catalog among them where there was one): %d of the trees its catalog names had lost their table already, and %d table(s) of this backend were named by no catalog, %d could not be attributed to anyone and %d could not be read. %s",
                config.getBackendId(), removedTrees(droppedTrees), dropped, missingTrees, ours, unattributed,
                unreadable, CLEAR_DROPPED_NOTHING));
        }
        reportSkippedRows(skippedRows); // after the reason above and among the lists, being a list itself
        if (ours>0) {
            reportClearLine(LocalizableMessage.raw("jdbc: backend %s: %d table(s) of %s hold trees of this backend that its catalog does not name, and the clear left them where they are: %s. A tree is enrolled as it is opened read-write and by no other means, so such a table is one of a tree of a base DN this backend still serves that was taken out of the configuration while it was disabled - an attribute index, say - or one left by a version keeping no catalog: it is this backend's own and can be removed by hand, and re-adding the tree it belongs to adopts it with the rows it still holds",
                config.getBackendId(), ours, scope.name(), leftovers.ours));
        }
        if (unattributed>0) {
            reportClearLine(LocalizableMessage.raw("jdbc: backend %s: %d opendj table(s) of %s are named by no catalog of this backend and carry no tree stamp, so nothing says whose they are: %s. They may hold the trees of a backend sharing this database, which nothing forbids, or be leftovers of a version stamping no table at all - a table is named after the hash of its tree name and can be attributed by no other means. They were left exactly where they are",
                config.getBackendId(), unattributed, scope.name(), leftovers.unattributed));
        }
        if (unreadable>0) {
            reportClearLine(LocalizableMessage.raw("jdbc: backend %s: the stamp of %d opendj table(s) of %s could not be read, so this clear says nothing about whose they are: %s. They were left exactly where they are",
                config.getBackendId(), unreadable, scope.name(), leftovers.unreadable));
        }
    }
    /**
     * What a clear did to the trees of its backend, in the one wording both lines of the report use:
     * a condition an operator greps for - and a case asserts on - must not be phrased one way where
     * the leftover scan answered and another way where it did not.
     */
    private static String removedTrees(int droppedTrees) {
        return droppedTrees==0 ? "the clear removed no tree of this backend"
            : "the clear removed "+droppedTrees+" tree(s) of this backend";
    }
    /**
     * Reports the rows of the catalog the clear could not act on; see {@link #readCatalogRows} for
     * what makes a row one of these and {@link #reportClearOutcome} for why they are a line of their
     * own. Silent where there are none, which is every clear of a catalog this backend wrote.
     * <p>
     * The row is gone by the time this prints and what it recorded is not: the catalog names itself
     * last, so the loop drops the table holding these rows along with every other - and where that
     * table went on its own between the two lookups, it took them with it just the same. That is what
     * the line has to say, and why it carries the recorded name rather than sending an operator to a
     * table that is no longer there.
     */
    private void reportSkippedRows(List<String> skippedRows) {
        if (skippedRows.isEmpty()) {
            return;
        }
        reportClearLine(LocalizableMessage.raw("jdbc: backend %s: %d row(s) of its catalog named nothing this clear could drop and were passed over: %s. The rows are gone with the catalog table, which a clear drops last; whatever they record was left standing, and no other line of this clear names it: the tables of this backend are named after the hash of a tree name, so what such a row records is outside the names a clear can account for. This line is the only surviving copy of it. A catalog holding such a row was written into by something other than this backend",
            config.getBackendId(), skippedRows.size(), skippedRows));
    }
    /**
     * Why a clear can remove no tree while there is something to remove, said wherever one did: it is
     * the silence of #888, and the one thing an operator reading such a line has to be told.
     */
    private static final String CLEAR_DROPPED_NOTHING="A backend upgraded from a version keeping no catalog has to be started once before its first offline \"import-ldif --clearBackend\": nothing enrols a tree before the clear runs, so that first clear finds a catalog that is not there - or, where the tables were restored from a backup taken beside an older one, a catalog that is there and names nothing - and removes no tree either way";
    /** What a clear left standing, told apart by the tree stamp of each table; see {@link #reportClearOutcome}. */
    static final class ClearLeftovers {
        /** Tables whose stamp names a tree of this backend: its own, and removable by hand. */
        final List<String> ours=new ArrayList<>();
        /** Tables carrying no stamp naming a tree: they can be attributed to nobody. */
        final List<String> unattributed=new ArrayList<>();
        /** Tables whose stamp the database would not give up: they are attributed neither way. */
        final List<String> unreadable=new ArrayList<>();
    }
    /**
     * The "opendj" tables this connection reaches - see {@link TableScope} - that this backend can say
     * something about, or {@code null} where the database would not list them. A table stamped with a
     * tree this backend does not serve is in none of the lists: it is a backend sharing this database
     * (#873) that it belongs to, and no part of this clear's outcome.
     */
    ClearLeftovers leftoverTables(Connection con, TableScope scope) {
        // the shared compressed schema pair is left standing on purpose, so it is no leftover of
        // anything and reporting it would be pointing at the one thing this code goes out of its way
        // to keep. Taken out by name and not by stamp: an installation may hold the pair unstamped,
        // from a version that commented no table at all.
        final Set<String> leftOnPurpose=new HashSet<>();
        for (final TreeName treeName : SHARED_COMPRESSED_SCHEMA_TREES) {
            leftOnPurpose.add(readTableName(treeName).toLowerCase(Locale.ROOT));
        }
        final ClearLeftovers leftovers=new ClearLeftovers();
        try {
            // by name and not by row: the listing is asked with no schema pattern and read back through
            // the resolution path (see TableScope), so two schemas of that path holding a table of the
            // same name both answer here - and there is exactly one thing this report can say of that
            // name, the stamp being read through an unqualified statement that resolves whichever of
            // them the path reaches first. Named twice it would read as two leftovers where the clear
            // can account for one. By the name exactly as the database spells it, and not folded: what
            // this collapses is one name in two schemas, while two names differing only in case are
            // two tables of a case-preserving engine and each is a leftover of its own
            final Set<String> standing=new LinkedHashSet<>();
            final DatabaseMetaData metaData=con.getMetaData();
            try (final ResultSet rs=metaData.getTables(scope.catalog, null,
                    storedIdentifier(metaData, "opendj%"), new String[]{"TABLE"})) {
                while (rs.next()) {
                    final String tableName=rs.getString("TABLE_NAME");
                    if (tableName==null) { // a row naming no table names nothing this clear can report
                        continue;
                    }
                    if (!leftOnPurpose.contains(tableName.toLowerCase(Locale.ROOT)) && scope.covers(rs)) {
                        standing.add(tableName);
                    }
                }
            }
            // the stamps are read once the metadata result set is closed: they are queries of this very
            // connection, and a driver may hold it for the whole of that result set
            final Dialect dialect=dialectOf(con);
            if (dialect==null) {
                // no comment readback is known for this engine, so no table of it can be attributed to
                // anyone at all. That is a different thing from a table which carries no stamp, and saying
                // the second would be telling an operator that every table of every backend of this
                // database is of unknown ownership when the truth is that nothing was ever asked
                leftovers.unreadable.addAll(standing);
                return leftovers;
            }
            for (final String tableName : standing) {
                final TreeName stamp;
                try {
                    stamp=stampedTree(con, dialect, tableName);
                } catch (SQLException | RuntimeException e) {
                    // this table alone is unaccounted for, and the ones after it need not be: postgres
                    // refuses every further statement of a transaction whose statement failed (25P02), so
                    // the read that failed is rolled back before the next table is asked about. There is
                    // nothing pending to lose - the clear committed its drops before this ran
                    logger.trace(LocalizableMessage.raw("jdbc: unable to read the stamp of table %s: %s",
                        tableName, stackTraceToSingleLineString(e)));
                    leftovers.unreadable.add(tableName);
                    try {
                        con.rollback();
                    } catch (SQLException e2) {}
                    throw new StorageRuntimeException(e);
                    continue;
                }
            } catch (Exception e) {
                throw new StorageRuntimeException(e);
                if (stamp==null) {
                    leftovers.unattributed.add(tableName);
                } else if (isOwnTree(stamp)) {
                    leftovers.ours.add(tableName+" ("+stamp+")");
                }
            }
            // all tables are gone: forget the mappings so listTrees() consumers skip the dropped trees
            for (final TreeName treeName : trees) {
                tree2table.invalidate(treeName);
                unstampableTrees.remove(treeName); // a table recreated later deserves a fresh stamp attempt
            }
        } catch (SQLException e) {
            logger.trace(LocalizableMessage.raw("jdbc: unable to look for the tables a clear left behind: %s",
                stackTraceToSingleLineString(e)));
            return null;
        }
        if (!isOpen) {
            close();
        return leftovers;
    }
    /**
     * The tree named by the comment this table carries (#866), or {@code null} where it carries none
     * or where what it carries is not the name of a tree. The stamp is the only thing that attributes
     * a table to a backend at all - a table name is a bare hash - and stamping is best-effort, so the
     * absence of one states nothing.
     * <p>
     * A read the database refused is passed to the caller rather than answered as an absent stamp: the
     * two say different things, and the second would let a connection that died halfway be reported as
     * a row of tables nothing can be said about. An engine with no readback of its own is the same
     * distinction one step earlier, and is answered by the caller: it puts every table of such an
     * engine where nothing was asked of it belongs, which is not where a table without a stamp goes.
     */
    private TreeName stampedTree(Connection con, Dialect dialect, String tableName) throws SQLException {
        final String comment=readStoredComment(con, dialect, tableName);
        if (comment==null || comment.isEmpty()) {
            return null;
        }
        try {
            return TreeName.valueOf(comment);
        } catch (RuntimeException e) { // a comment of somebody else's making: no stamp of this backend's kind
            return null;
        }
    }
    /**
     * The base DN the compressed schema trees of this backend are named under since #881, spelled out
     * here for the reason {@link #SHARED_COMPRESSED_SCHEMA_TREES} is: the prefix is built by a private
     * method of {@code PersistentCompressedSchema}, escapes and all. A table stamped with one of these
     * carries this backend's id in plain text, so a clear that finds one standing can say whose it is.
     */
    private String ownCompressedSchemaBaseDN() {
        return SHARED_COMPRESSED_SCHEMA_BASE_DN+"_"+escapedBackendId();
    }
    /**
     * The backend id as one component of a tree name. A tree name is {@code /<base DN>/<id>} and is
     * read back by splitting on its slashes ({@code TreeName.valueOf}), so an id carrying one of them
     * would name a tree that parses into another tree than it was built from - and a table is stamped
     * with that name (#866), so a clear reading the stamp of a table of this backend's own would then
     * fail to recognize it and pass it over in silence. The escape is the one {@code
     * PersistentCompressedSchema} spells its own prefix with, percent first so that the escape of the
     * slash cannot be produced twice, and it leaves an id of the ordinary shape exactly as it is -
     * which is what keeps the table names of an installation unchanged.
     */
    private String escapedBackendId() {
        return config.getBackendId().replace("%", "%25").replace("/", "%2F");
    }
    /**
     * Whether this tree is one of this backend's own: a tree of a base DN it serves, its own catalog,
     * or its own pair of compressed schema trees. The catalog counts because a clear drops it last, so
     * one still standing is a clear of this backend that did not get to the end, and never anything of
     * anybody else's. The compressed schema pair counts because since #881 it is named after the
     * backend id (#873) and so belongs to this backend as plainly as any tree of a base DN it serves -
     * where the legacy pair, named from a literal, belongs to no backend in particular and is reported
     * by nobody.
     */
    private boolean isOwnTree(TreeName treeName) {
        if (getCatalogTree().equals(treeName) || ownCompressedSchemaBaseDN().equals(treeName.getBaseDN())) {
            return true;
        }
        final SortedSet<DN> baseDNs=config.getBaseDN();
        if (baseDNs==null) {
            return false;
        }
        for (final DN baseDN : baseDNs) {
            // every tree of an entry container is named after the normalized form of its base DN,
            // which is what EntryContainer builds its tree names from
            if (treeName.getBaseDN().equals(baseDN.toNormalizedUrlSafeString())) {
                return true;
            }
        }
        return false;
    }
    /**
     * The database and the schemas a connection reaches with an unqualified name: what every table
     * lookup of this backend is narrowed to.
     * <p>
     * The database is the half that has to narrow. Asked with a null catalog the question spans the
     * whole server on some drivers - Connector/J reads a null catalog as "any database" since 8.0, and
     * its databaseTerm being CATALOG it ignores the schema pattern besides - and every answer of such a
     * lookup decides something a table of the same name in another database must have no say in. A
     * clear skips the row of a table that is gone so that it can go on, and a foreign table answering
     * for it turns that skip into an unqualified "drop table" of a table that is not in this database,
     * failing the clear on this attempt and on every attempt after it. An open of a tree creates its
     * table where there is none, and a foreign table answering for it skips the creation, leaving the
     * catalog naming a tree whose table is not here. Two backends of the stock backend id in two
     * databases of one server name their tables alike, so this is the ordinary layout and not a corner
     * of one.
     * <p>
     * The schema is the half that must not narrow to one name. The statements this scope guards are
     * unqualified, and an unqualified name resolves across a path of schemas: the whole
     * {@code search_path} on postgresql, the default schema of the user and then {@code dbo} on sql
     * server. A lookup narrowed to {@code current_schema()} alone would be the stricter question of the
     * two - an installation whose tables were created in {@code public} while the connection now works
     * in a schema of its own reads and writes them unqualified all the same, and asking only about that
     * schema would report them absent: the clear would drop nothing, which is #888 over again, and the
     * next open would create a second, empty set of tables shadowing the populated ones for every later
     * unqualified reference. The path is asked of the connection, so that a lookup answers for exactly
     * the tables the statements behind it reach - no more and no fewer.
     */
    static final class TableScope {
        /** The database of the connection, or {@code null} where the driver names none - oracle has none. */
        final String catalog;
        /**
         * The schemas an unqualified name of this connection resolves in, nearest first, or {@code null}
         * where the schema is no dimension of this engine - mysql, whose schema is its database - or
         * where the connection would not say. A null path narrows nothing, which is the question this
         * class asked before there was anything to narrow it by.
         */
        final List<String> schemas;
        /**
         * Whether the connection answered both questions. One it would not answer leaves the lookup as
         * wide as it ever was - fail-open, which is the safe direction for the schema and the weak one
         * for the database - so the caller asks again rather than latching that answer for the life of a
         * transaction; see {@link ReadableTransactionImpl#takeTableScope()}.
         */
        final boolean answered;
        private TableScope(String catalog, List<String> schemas, boolean answered) {
            this.catalog=catalog;
            this.schemas=schemas;
            this.answered=answered;
        }
        /**
         * What this connection says about where an unqualified name of it resolves. The storage is
         * taken because one engine is asked with a statement rather than with a method of its driver,
         * and a statement of this backend takes the bound of its class (#882).
         */
        static TableScope of(JDBCStorage storage, Connection con) {
            return of(storage, con, true);
        }
        /**
         * The same, told to keep quiet about a connection that will not answer. A transaction asks
         * again for as long as it is refused - a lookup left as wide as the whole server decides a
         * create and a drop - and one refusal per tree of the backend is one line per tree in the log,
         * each with a stack trace, for a thing that was already said.
         */
        static TableScope of(JDBCStorage storage, Connection con, boolean report) {
            String catalog=null;
            List<String> schemas=null;
            boolean answered=true;
            try {
                // an empty name is not the name of a database but a driver's way of saying it has none,
                // and passed to a metadata pattern it means "tables that belong to no catalog" - which is
                // not the same question and would answer nothing
                catalog=emptyToNull(con.getCatalog());
            } catch (Exception e) {
                // said out loud rather than swallowed: this decides a create and a drop, and a lookup
                // that silently reverts to the whole server is the one failure of the two that cannot be
                // seen from its outcome
                answered=false;
                log(report, "jdbc: this connection would not name the database it works in, so a table of another database of this server may answer for one of this backend's: %s", e);
            }
            try {
                schemas=schemaPathOf(storage, con);
            } catch (Exception e) {
                answered=false;
                log(report, "jdbc: this connection would not name the schemas an unqualified name of it resolves in, so a table of any schema may answer for one of this backend's: %s", e);
            }
            return new TableScope(catalog, schemas, answered);
        }
        /** Said once where it is worth saying, and kept for the trace where it would be said again. */
        private static void log(boolean report, String message, Exception e) {
            if (report) {
                logger.warn(LocalizableMessage.raw(message, stackTraceToSingleLineString(e)));
            } else {
                logger.trace(LocalizableMessage.raw(message, stackTraceToSingleLineString(e)));
            }
        }
        /** The schemas an unqualified name resolves in, in the order this engine resolves them. */
        private static List<String> schemaPathOf(JDBCStorage storage, Connection con) throws SQLException {
            final String driverName=driverNameOf(con);
            if (driverName.contains("mysql")) {
                // the schema of Connector/J is the database, and which of the two names it answers with is
                // the databaseTerm of the connection: with CATALOG - the default - getSchema() answers null
                // and the catalog above is the narrowing, and with SCHEMA it is the other way round. Asked
                // rather than assumed, so that neither setting leaves this lookup narrowed by nothing at all
                final String database=emptyToNull(con.getSchema());
                return database==null ? null : Collections.singletonList(database);
            }
            if (driverName.contains("postgres")) {
                // getSchema() is "select current_schema()" on pgjdbc - the first existing schema of the
                // search_path - while an unqualified reference resolves across the whole of it.
                // Behind a savepoint, because this runs on the caller's transaction and postgres refuses
                // every further statement of a transaction whose statement failed (25P02): a query this
                // engine turns out not to have - pgjdbc talks to more than one of them - would otherwise
                // surface as the next statement of the caller failing, with the cause nowhere near it
                final Savepoint before=savepoint(con);
                try (final PreparedStatement statement=con.prepareStatement("select unnest(current_schemas(true))")) {
                    // bounded like every other statement of this backend (#882), and by the class the lookups
                    // this scope narrows take: it reads a session setting rather than the data, and what the
                    // savepoint and the fallback below answer for is a query this engine refuses - not one it
                    // never answers at all, which is a wait holding the open of a tree with nothing to end it
                    final List<String> path=storage.executeResultSet(statement, StatementBound.OPERATION, rs -> {
                        final List<String> read=new ArrayList<>();
                        while (rs.next()) {
                            final String schema=emptyToNull(rs.getString(1));
                            if (schema!=null) {
                                read.add(schema);
                            }
                        }
                        return read;
                    });
                    release(con, before);
                    if (!path.isEmpty()) {
                        return Collections.unmodifiableList(path);
                    }
                } catch (Exception e) { // asked of getSchema() below instead, as well as it can say it
                    undo(con, before);
                    logger.debug(LocalizableMessage.raw("jdbc: unable to read the search path of this connection, which is asked for its current schema instead: %s",
                        stackTraceToSingleLineString(e)));
                }
            }
            final String schema=emptyToNull(con.getSchema());
            if (schema==null) {
                return null;
            }
            if (driverName.contains("microsoft")) {
                // an unqualified name resolves in the default schema of the user and then in dbo
                return Collections.unmodifiableList(Arrays.asList(schema, "dbo"));
            }
            // oracle resolves in the current schema, and past it through synonyms this cannot enumerate:
            // a table reached through one is not found here, and an open creates it again in the schema
            return Collections.singletonList(schema);
        }
        /**
         * A point to put a transaction back to, or {@code null} where this connection is in no
         * transaction to speak of or would not take one. A read of the search path is answered by the
         * connection of whoever asked for the scope, and a failed statement of it is theirs to be
         * spared.
         */
        private static Savepoint savepoint(Connection con) {
            try {
                return con.getAutoCommit() ? null : con.setSavepoint("opendj_search_path");
            } catch (SQLException | RuntimeException e) {
                return null;
            }
        }
        /** Puts the transaction back to where the probe found it, so that its failure stays the probe's. */
        private static void undo(Connection con, Savepoint savepoint) {
            if (savepoint!=null) {
                try {
                    con.rollback(savepoint);
                } catch (SQLException | RuntimeException e) {}
            }
        }
        /** Gives up a savepoint nothing needs any more: a transaction keeps them all until it ends. */
        private static void release(Connection con, Savepoint savepoint) {
            if (savepoint!=null) {
                try {
                    con.releaseSavepoint(savepoint);
                } catch (SQLException | RuntimeException e) {}
            }
        }
        /**
         * Whether the table this row of a listing describes is one this connection reaches. The metadata
         * pattern alone does not settle it: a schema reaches {@link DatabaseMetaData#getTables} as a
         * pattern, where "_" is a single-character wildcard, so a listing narrowed to a schema named
         * "app_data" is answered for by one named "appXdata" as well. The listings of this class are
         * asked with no schema pattern at all - a path is more than one name anyway - and read through
         * this instead.
         */
        boolean covers(ResultSet rs) throws SQLException {
            return isSameCatalog(rs.getString("TABLE_CAT")) && isOnSchemaPath(rs.getString("TABLE_SCHEM"));
        }
        /**
         * Whether the database of a listed table rules it out. A name neither side gives is no
         * narrowing: a driver naming no catalog of its own - oracle has none - must not be read as
         * naming another.
         */
        private boolean isSameCatalog(String ofTable) {
            return catalog==null || ofTable==null || ofTable.isEmpty() || catalog.equalsIgnoreCase(ofTable);
        }
        /** Whether a listed table is in one of the schemas an unqualified name of this connection resolves in. */
        private boolean isOnSchemaPath(String ofTable) {
            if (schemas==null || schemas.isEmpty() || ofTable==null || ofTable.isEmpty()) {
                return true;
            }
            for (final String schema : schemas) {
                if (schema.equalsIgnoreCase(ofTable)) {
                    return true;
                }
            }
            return false;
        }
        /** How the database and the schemas a table count was taken over are named in a log line. */
        String name() {
            final String where=schemas==null || schemas.isEmpty() ? null : String.join(", ", schemas);
            if (catalog!=null && where!=null) {
                return catalog+"."+where;
            }
            if (catalog!=null) {
                return catalog;
            }
            return where!=null ? where : "this connection";
        }
        /** An empty name is the name of nothing: see {@link #of}. */
        private static String emptyToNull(String name) {
            return name==null || name.isEmpty() ? null : name;
        }
    }
    //operation
    /**
     * {@inheritDoc}
@@ -1601,6 +2975,11 @@
            try (con) {
                driver=driverNameOf(con);
                final WriteableTransactionTransactionImpl txn=new WriteableTransactionTransactionImpl(con);
                //the connect of the catalog is made inside this attempt and retries the way a borrow does,
                //up to the pool timeout - six times this window at the defaults. Left to its own deadline
                //it would spend a window it does not own and hand the loop a failure it has classified as
                //replayable with nothing left to replay it in, so it is told where the window ends
                txn.catalogSession.boundedAlsoBy(giveUpAt);
                try {
                    writeOperation.run(txn);
                    committing=true;
@@ -1631,10 +3010,16 @@
                    failure=e;
                    throw e;
                } finally { // the comment connection lives no longer than the trees it stamped, and no longer
                    // than the attempt that opened it: a replay stamps on a session of its own
                    // than the attempt that opened it: a replay stamps on a session of its own. The catalog
                    // connection goes with it, having written every row this attempt had to enrol - and
                    // committed each of them, so a replay finds them recorded and writes none again
                    partlyCommitted=txn.partlyCommitted;
                    try {
                        txn.stampSession.close();
                        try {
                            txn.stampSession.close();
                        } finally {
                            txn.catalogSession.close();
                        }
                    } catch (RuntimeException e) {
                        //the stamp is a diagnostic aid and must not become the outcome of the write: an unchecked
                        //throw out of a driver's close() would otherwise replace the failure being unwound (JLS
@@ -2068,39 +3453,39 @@
            return isExistsTable(treeName);
        }
        /**
         * Where an unqualified name of this transaction's connection resolves, asked of it once. Every
         * lookup of a table below is narrowed to it - the reason is in {@link TableScope} - and asking
         * per lookup would cost a round trip per tree of the backend on every open: pgjdbc answers both
         * halves of it with a select of its own. A transaction holds one connection for the whole of its
         * life, so one answer serves it all.
         */
        // not private: a private member is not inherited, and the writeable transaction below asks
        // for the scope of its own lookups through it
        TableScope tableScope;
        TableScope takeTableScope() {
            // a connection that would not answer is asked again rather than latched: what it says
            // decides a create and a drop, and one refused question would otherwise leave every lookup
            // of this transaction as wide as the whole server. Only the first refusal of a transaction
            // is reported: the ones behind it are the same connection saying the same thing, once per
            // tree of the backend
            if (tableScope==null || !tableScope.answered) {
                tableScope=TableScope.of(JDBCStorage.this, con, tableScope==null);
            }
            return tableScope;
        }
        // Readable, not writeable: the caller that asks about a tree this backend does not own is
        // the compressed schema migration (#873), which probes the shared tree from the writeable
        // transaction of RootContainer.open() but must not create or enrol it. Answering that from
        // the readable transaction keeps the probe available to every reader, and costs nothing:
        // the writeable one inherits it.
        // The name it asks about is the non-enrolling one for that same reason, and the question is
        // narrowed to where an unqualified name of this connection resolves, like every other table
        // lookup of this class: see isExistsTable(Connection, TableScope, String).
        boolean isExistsTable(TreeName treeName) {
            final String tableName = readTableName(treeName);
            // the catalog lookup guarding a create table is bounded as the operation it is, not as
            // the bulk statement it guards, and not as the class of the transaction that happens to
            // ask: it reads a data dictionary rather than the data, so a wait here is the metadata
            // lock of another session
            try {
                return bounded(con, StatementBound.OPERATION, () -> {
                    final DatabaseMetaData metaData = con.getMetaData();
                    // asked of the catalog by name: openTree(createOnDemand) calls this for every tree
                    // of the backend - about 25 of them for a stock suffix, on every open - and listing
                    // every table of the database each time costs the whole catalog once per tree, on a
                    // database this backend may well be sharing with something else
                    try (final ResultSet rs = metaData.getTables(null, null,
                            storedIdentifier(metaData, tableName), new String[]{"TABLE"})) {
                        while (rs.next()) {
                            // the name still has to be compared: "_" is a single-character wildcard in a
                            // metadata pattern, so "opendj_<hash>" also matches a table named "opendjX<hash>"
                            if (tableName.equalsIgnoreCase(rs.getString("TABLE_NAME"))) {
                                return true;
                            }
                        }
                    }
                    return false;
                });
            } catch (Exception e) {
                throw new StorageRuntimeException(e);
            }
            return JDBCStorage.this.isExistsTable(con, takeTableScope(), readTableName(treeName));
        }
    }
    /**
@@ -2120,6 +3505,10 @@
        // write() (and by ImporterImpl.close()) when the transaction is done with.
        final StampSession stampSession=new StampSession();
        // The connection the catalog rows of this transaction are written on, opened at the first
        // row there is to write and closed with the transaction, like the stamp session above.
        final CatalogSession catalogSession=new CatalogSession();
        /**
         * Whether this transaction has committed part of its own work, which takes the attempt out of the
         * replay of {@link JDBCStorage#write}: what it did no longer rolls back as a whole, and a
@@ -2200,6 +3589,18 @@
        public void openTree(TreeName treeName, boolean createOnDemand) {
            if (createOnDemand) {
                checkReadOnly();
                // what makes this tree nameable by a process which has opened nothing: see
                // getCatalogTree(). Written before the table and not after it, on a connection of the
                // catalog's own and committed there, so that the table is never there without a row
                // naming it - on every engine, and not only on the ones whose DDL happens to carry the
                // row along - and so that none of it commits the work of this transaction. Of the two
                // ways a half-done open can end, a catalog naming a table that is not there is the one
                // the removal is ready for - it skips such a row and says so - while a table nothing
                // names is adopted with its stale rows by the next open of that tree and is dropped by no
                // clear ever after. deleteTree() takes the row out after the drop for that same reason,
                // which is why it is not the mirror of this. It writes, so it comes after the read-only
                // check and not before it (#874)
                enrolInCatalog(treeName);
                // Every statement below is a DDL that commits, and each raises partlyCommitted through
                // commitStatement() rather than once for the method: every one of them is guarded by a
                // catalog read, so on an existing backend this method issues nothing at all. Raising the
@@ -2240,7 +3641,7 @@
                }else if (driverName.contains("oracle")) {
                    try {
                        // oracle has no "create index if not exists"; unquoted identifiers are stored in uppercase
                        if (!isExistsIndex(tableName.toUpperCase(),"k_"+tableName.substring("opendj_".length()))) {
                        if (!isExistsIndex(tableName.toUpperCase(Locale.ROOT),"k_"+tableName.substring("opendj_".length()))) {
                            commitStatement("create index k_"+tableName.substring("opendj_".length())+" on "+tableName+" (k)", true);
                        }
                    }catch (SQLException e) {
@@ -2254,12 +3655,341 @@
            }
        }
        /**
         * Records the tree in the catalog of this backend, creating the catalog itself along the way
         * when this is the first tree of the storage. Enrolling is the business of
         * openTree(createOnDemand) alone: naming a tree in order to read it must never put it up for
         * removal, since the tree read may belong to another backend of the same database - the
         * unqualified compressed schema trees such a database may still hold, say (#873).
         * <p>
         * The row is written whenever the catalog does not already record this tree at this table -
         * and not only when the table is created - so that a backend of an installation upgraded to a
         * version keeping a catalog fills it in at its first read-write open instead of waiting for
         * its trees to be created again. What the catalog already records is read once, when this
         * storage first opens it; see {@link #enrolledTrees}.
         * <p>
         * The row is written on a connection of the catalog's own and committed there, never on the one
         * this transaction runs on. It has to be committed: the open which fills the catalog of a
         * backend upgraded from a version keeping none creates no table at all, so there is nothing
         * else of {@link #openTree} to carry those rows, and a transaction failing after them would
         * take every one back - leaving the tables named by nothing and the next clear dropping
         * nothing, which is #888 over again. And that commit must not be this transaction's:
         * {@code RootContainer.open()} opens every tree of every base DN in a single write, a commit
         * anywhere inside it takes the whole write out of the replay - {@link #replayReason} reads
         * {@link #partlyCommitted} before it asks anything else - and a deadlock at the twentieth tree
         * would then fail the backend open where master replayed it. A connection of its own is what
         * gives the row a commit that is not the caller's.
         */
        void enrolInCatalog(TreeName treeName) {
            final TreeName catalog=getCatalogTree();
            if (catalog.equals(treeName)) {
                return; // the catalog holds no row of its own: catalogTables() adds it when its table is there
            }
            if (SHARED_COMPRESSED_SCHEMA_BASE_DN.equals(treeName.getBaseDN())) {
                return; // a tree this backend may not be the only owner of: see the constant
            }
            openCatalog(catalog);
            if (enrolledTrees.contains(treeName)) {
                return; // already recorded, at the table this open would record it at
            }
            try {
                catalogSession.transaction().upsert(catalog,
                    ByteString.valueOfUtf8(treeName.toString()),
                    ByteString.valueOfUtf8(getTableName(treeName)));
                // committed where it is written, so that the row is there before the table on every
                // engine and not only where the "create table" below happens to carry it - and on the
                // catalog's own connection, so that this commit is none of the caller's: see above
                catalogSession.commit();
                enrolledTrees.add(treeName);
            } catch (SQLException | RuntimeException e) {
                // the unchecked one as well, exactly as unenrolFromCatalog() takes it: upsert() answers a
                // failed statement with a StorageRuntimeException of its own, and what that statement left
                // behind has to be rolled back all the same. This connection outlives the row that failed
                // on it and carries every remaining tree of this open - postgres refuses every further
                // statement of a transaction whose statement failed (25P02), so a reset skipped here fails
                // the twenty-odd enrolments behind it with a cause nowhere near the one that started it
                catalogSession.reset();
                throw e instanceof StorageRuntimeException ? (StorageRuntimeException) e : new StorageRuntimeException(e);
            }
        }
        /**
         * The connection the catalog is read and written on, established where this transaction has
         * not established it yet.
         * <p>
         * The unchecked failure of the connect is taken like the checked one, the way every other
         * catalog path of this class takes it: {@link JDBCStorage#newCatalogConnection} hands on a
         * driver's unchecked answer to a connect it will not make as the unchecked failure it is, so a
         * catch of {@code SQLException} alone would let that one past unwrapped and without the line
         * saying which connection of this backend could not be made.
         */
        Connection catalogConnection() {
            try {
                return catalogSession.connection();
            } catch (SQLException | RuntimeException e) {
                throw e instanceof StorageRuntimeException ? (StorageRuntimeException) e
                    : new StorageRuntimeException("jdbc: backend "+config.getBackendId()
                        +" could not open the connection its tree catalog is read and written on", e);
            }
        }
        /**
         * Makes the catalog of this backend usable, once per open of the storage: its table is created
         * where there is none, and what it already records is read where there is one.
         * <p>
         * Serialized on the storage, so that two transactions opening trees at the same time cannot both
         * find the table absent and both go on to create it. It serializes this storage and nothing
         * else, which is why the create tolerates a table that turned up while it was being made: an
         * offline tool beside a running server is a pair no lock of one process can order. The stamp -
         * the one thing under it that is nobody's dependency - is issued outside it.
         * <p>
         * Two things are kept out of the lock because they are the slow ones. The flag is read before
         * it is taken at all, which is every {@code openTree} of this storage but the first few: it is
         * volatile and it is raised after {@link #enrolledTrees} has been filled, so a reader that sees
         * it up sees that memo whole. And the connection of the catalog is established before it: that
         * connect retries a database taking no connection for the moment for up to the deadline of a
         * borrow (see {@link JDBCStorage#newCatalogConnection}), and made under the lock it would hold
         * every other transaction of this storage that goes on to open a tree for the whole of that
         * wait - a queue the borrows of the pool, each waiting on its own thread, never form.
         * <p>
         * What that costs is a connect to every transaction which finds the flag down and then loses the
         * race for the lock. The loser gives that connection up rather than hold it: the winner has
         * filled {@link #enrolledTrees}, so the caller is about to find its tree recorded and write
         * nothing at all, and the session is lazy - the rarer loser that does have a tree to enrol opens
         * another. The race is for the first openTree of a storage, so what this can cost against a
         * database with no connection to give is one refused login per racing transaction, where the
         * connect made under the lock cost one and made the others wait out the same refusal in turn.
         */
        void openCatalog(TreeName catalog) {
            if (catalogTableOpened) {
                return;
            }
            // what this call established, and not what the transaction was already holding: deleteTree()
            // opens the session before it drops anything, so a later openTree of the same transaction
            // must not give away a connection it did not make
            final boolean established=!catalogSession.isEstablished();
            catalogConnection();
            final boolean lostTheRace;
            synchronized (catalogLock) {
                lostTheRace=catalogTableOpened;
                if (!lostTheRace) {
                    if (isExistsTable(catalog)) {
                        readEnrolledTrees(catalog);
                    } else {
                        createCatalogTable(catalog);
                        // nothing to read from a table that has just been created, and nothing this open
                        // enrols may be skipped as already recorded
                    }
                    catalogTableOpened=true;
                }
            }
            if (lostTheRace) {
                if (established) {
                    // the winner has filled enrolledTrees, so the caller is about to find its tree recorded
                    // and write nothing: the connection this call made is given up rather than held idle for
                    // the rest of the transaction, and the session being lazy, the rarer loser that does
                    // have a tree to enrol opens another. Outside the lock, for the reason the connect is:
                    // a close is a round trip of its own, and against a database that has stopped answering
                    // it does not return at all - connectCatalog() lifts the read bound of the login on
                    // every connection it hands back, so there is no bound of ours left to end this one
                    catalogSession.close();
                }
                return;
            }
            // stamped with its tree name like any table of a tree (#866), and for a reason of its own: a
            // clear reports what it did not drop, and the catalog of a backend sharing this database
            // (#873) is the one table such a report could otherwise attribute to nobody. It costs one
            // stamp per open of the storage, not one per tree - the flag above is what keeps it to one -
            // and it is issued outside the lock: it is a diagnostic aid on a session and a bound of its
            // own, with no business holding up every openTree of this storage
            commentTable(catalog, dialectOf(con), stampSession);
        }
        /**
         * Reads what the catalog already records, so that the trees it names are not enrolled again on
         * an open which would write the rows that are already there; see {@link #enrolledTrees}. Run
         * once per open of the storage, behind the very flag that keeps the catalog from being opened
         * again, and it costs the one select a clear pays for anyway.
         * <p>
         * Read on the catalog's own connection and committed there, so that no transaction of a caller
         * ever touches the catalog table. A select of the caller's transaction would hold a lock on it
         * until that transaction ended - the whole of {@code RootContainer.open()} - and the rows this
         * read decides are written on the catalog's connection: a clear of this backend queueing for the
         * table in between would then be waiting for the caller while the caller waited for it, a pair
         * of sessions no deadlock detector of the database can see, one of them being blocked inside
         * this process rather than in the server. {@link #removeStorageFiles()} and {@link #listTrees()}
         * read that table on a connection of their own, which is the same argument read the other way:
         * neither is inside a transaction of a caller, and both are done with it when they commit.
         */
        void readEnrolledTrees(TreeName catalog) {
            try {
                final Connection catalogCon=catalogSession.connection();
                for (final Map.Entry<TreeName,String> row : readCatalogRows(catalogCon, getTableName(catalog)).entrySet()) {
                    // a row recording another table than this version would record is not the row this
                    // open would leave behind: a removal drops the table the row records, so such a row is
                    // rewritten - and committed - exactly like one that is not there at all. Asked through
                    // the non-enrolling name of #881: reading what the catalog records is not taking an
                    // interest in the tree it names, and a row this decides not to trust must not have put
                    // its tree in the memo of the trees this backend names its tables for
                    if (readTableName(row.getKey()).equals(row.getValue())) {
                        enrolledTrees.add(row.getKey());
                    }
                }
                catalogCon.commit(); // the read ends here and holds nothing of the catalog after it
            } catch (SQLException | RuntimeException e) {
                // the unchecked one as well, for the reason enrolInCatalog() takes it: this connection is
                // the one every enrolment of this open goes on to write its row on
                catalogSession.reset();
                throw e instanceof StorageRuntimeException ? (StorageRuntimeException) e : new StorageRuntimeException(e);
            }
        }
        /**
         * Creates the table of the catalog, on the catalog's own connection for the reason its rows are
         * written there - see {@link #enrolInCatalog}. It takes no index of the kind openTree() gives a
         * tree: the catalog is read whole and written by key, never iterated by key range, so the index
         * a cursor needs would serve nothing here. The stamp it does take is given by the caller, on
         * every open rather than on creation alone.
         * <p>
         * A read-write open of a JDBC backend needs the privilege to create this table, where a version
         * keeping no catalog issued no DDL at all on an installation whose tables were already there.
         * An account that may write its rows but not create a table is a configuration this can meet,
         * so the failure says which table it was and why the backend wanted it, rather than reaching
         * the operator as a bare SQL error inside ERR_OPEN_ENV_FAIL.
         */
        void createCatalogTable(TreeName catalog) {
            final String tableName=getTableName(catalog);
            try {
                final Connection catalogCon=catalogSession.connection();
                try (final PreparedStatement statement=catalogCon.prepareStatement("create table "+tableName+" ("+getTableDialect()+")")) {
                    // bulk like every other create table of this backend (#882): it is DDL nobody waits on,
                    // and the class of a client operation is not what a statement of this kind can be given
                    execute(statement, StatementBound.BULK);
                }
                catalogCon.commit();
            } catch (SQLException | RuntimeException e) {
                // the unchecked one as well, for the reason enrolInCatalog() takes it: what the statement
                // left behind has to be rolled back whatever class the failure arrived in, this connection
                // being the one the rows of this open are written on
                catalogSession.reset();
                // a table that turned up between the lookup and this statement is what was wanted, whoever
                // made it: the lock this runs under orders the transactions of one storage, and an offline
                // tool beside a running server - the pair #888 is about - is ordered by nothing at all.
                // The lookup is asked inside a catch and must not become the answer: it goes to the
                // database on the caller's connection, which is often the very thing that has just failed,
                // and it reports its own failure as a StorageRuntimeException - thrown from here it would
                // replace the create failure below with a bare metadata error saying nothing about the
                // catalog. So a lookup that will not answer is carried by the failure it could not settle.
                boolean alreadyThere;
                try {
                    alreadyThere=isExistsTable(catalog);
                } catch (RuntimeException lookup) {
                    e.addSuppressed(lookup);
                    alreadyThere=false;
                }
                if (alreadyThere) {
                    logger.debug(LocalizableMessage.raw("jdbc: table %s was created by another session while this one was creating it: %s",
                        tableName, stackTraceToSingleLineString(e)));
                    return;
                }
                throw new StorageRuntimeException("jdbc: backend "+config.getBackendId()+" could not create table "
                    +tableName+", which holds the catalog naming the trees it owns: a read-write open of a JDBC"
                    +" backend needs the privilege to create it, and a clear of one names nothing without it", e);
            }
        }
        /**
         * Takes the tree out of the catalog: a row is what puts a table up for removal, and this one is
         * gone. Written and committed on the catalog's own connection, like the enrolment - see {@link
         * #enrolInCatalog} - which is what keeps the caller's transaction from being able to roll it
         * back over a table that is already dropped.
         */
        void unenrolFromCatalog(TreeName treeName, boolean enrolled) {
            final TreeName catalog=getCatalogTree();
            if (catalog.equals(treeName)) {
                catalogTableOpened=false; // its own table is gone: the next enrolment creates it again
                enrolledTrees.clear(); // and records every tree anew, this one having recorded nothing
                return;
            }
            if (SHARED_COMPRESSED_SCHEMA_BASE_DN.equals(treeName.getBaseDN())) {
                // the symmetry of enrolInCatalog() and nothing more: no row of this pair was ever written,
                // so the delete would find none. What keeps the pair out of a clear is that a clear drops
                // what the catalog names and the catalog does not name them; see the constant
                return;
            }
            if (!enrolled) {
                // no row to delete - the catalog table is not there at all - and nothing to order this
                // against: a tree the catalog does not name is not one an enrolment may skip
                enrolledTrees.remove(treeName);
                return;
            }
            try {
                // deleteRow() and not delete(): the read-only check belongs to the caller of deleteTree,
                // which made it, and the transaction this row is written through is one of this class's own
                catalogSession.transaction().deleteRow(catalog, ByteString.valueOfUtf8(treeName.toString()));
                catalogSession.commit();
            } catch (SQLException | RuntimeException e) {
                // the unchecked one as well: deleteRow() answers a failed statement with a
                // StorageRuntimeException, and what that statement left behind has to be rolled back all
                // the same - this connection outlives the row that failed on it
                catalogSession.reset();
                throw e instanceof StorageRuntimeException ? (StorageRuntimeException) e : new StorageRuntimeException(e);
            } finally {
                // taken out of what this storage knows the catalog records after the delete and never
                // before it, so that the memo and the catalog never disagree in the direction that
                // makes an enrolment write a row a committed delete then takes back out. After the
                // attempt whatever became of it: a delete that failed leaves a row the next enrolment
                // has to write again rather than skip as already recorded, which costs an upsert of a
                // row that is already there and no more.
                //
                // What no ordering of these two lines can do is order this against an openTree of the
                // very same tree on another thread, and it is worth saying which fix was ruled out
                // rather than leaving it to be proposed again. Such an openTree landing between the
                // commit above and this line skips its enrolment - the memo still names the tree - and
                // goes on to create the table, leaving a table nothing names; the ordering before this
                // one reached the same end state by the other route, the enrolment writing a row this
                // delete then removed. A lock over the memo and the row closes neither, since the
                // table is created and dropped outside it either way: only a lock held across the DDL
                // of both would, and that one deadlocks. A transaction holding catalogLock and blocked
                // in the database on a "drop table" of a tree a second transaction of this storage is
                // still writing would be waiting for that transaction, while it waited for the lock at
                // its next openTree - a cycle the database cannot see, where today it is a plain wait
                // that ends when the second transaction does.
                //
                // So the catalog is consistent given that no two transactions open and delete the same
                // tree at once, and that is the layer above's to keep: a tree is opened read-write and
                // deleted from the configuration framework, which orders the changes of one entry, or
                // from EntryContainer.clear() with the backend disabled.
                enrolledTrees.remove(treeName);
            }
        }
        /** Whether a delete of this tree has a row of the catalog to take out; see {@link #unenrolFromCatalog}. */
        boolean isEnrolledTree(TreeName treeName) {
            if (getCatalogTree().equals(treeName) || SHARED_COMPRESSED_SCHEMA_BASE_DN.equals(treeName.getBaseDN())) {
                return false;
            }
            return catalogTableOpened || isExistsTable(getCatalogTree());
        }
        /**
         * Whether the table already carries the index of this name, asked where the table itself is
         * asked for - see {@link TableScope}. A table name carries no backend id and no database, so two
         * databases of one server hold identical table <em>and</em> index names, and Connector/J 8 binds
         * no schema predicate for a null catalog: a neighbouring database answering here would skip the
         * create index of this one for good, leaving every "where k>? order by k" batch of every cursor
         * a full scan behind it.
         */
        boolean isExistsIndex(String tableName, String indexName) throws SQLException {
            final TableScope scope=takeTableScope();
            // the index lookup takes the operation bound of #882 like every other catalog read of this
            // class: it asks a data dictionary rather than the data, so a wait here is the metadata lock
            // of another session - and it is narrowed to the scope every table lookup here is narrowed to
            return bounded(con, StatementBound.OPERATION, () -> {
                // approximate=true: with false the oracle driver runs ANALYZE on every call
                try (final ResultSet rs = con.getMetaData().getIndexInfo(null, null, tableName, false, true)) {
                try (final ResultSet rs = con.getMetaData().getIndexInfo(scope.catalog, null, tableName, false, true)) {
                    while (rs.next()) {
                        if (indexName.equalsIgnoreCase(rs.getString("INDEX_NAME"))) {
                        if (indexName.equalsIgnoreCase(rs.getString("INDEX_NAME")) && scope.covers(rs)) {
                            return true;
                        }
                    }
@@ -2280,6 +4010,34 @@
        @Override
        public void deleteTree(TreeName treeName) {
            checkReadOnly();
            // The row is taken out on the catalog's own connection rather than left to this transaction:
            // that transaction is the last thing the delete could still be rolled back by - write()
            // replays a class 40 conflict and rethrows everything else unreplayed - and the row would be
            // rolled back over a table that is already gone, with nothing ever to put it right: a deleted
            // tree is not opened again, so no enrolment and no unenrolment reaches it a second time. It
            // holds for the branch where there is no table to drop as much as for the one where the drop
            // commits of its own accord.
            // That connection is opened here, before anything is dropped: a connect this backend cannot
            // make costs nothing at this point, where one failing after the drop would leave exactly the
            // half-done state the sentence above is about.
            final boolean enrolled=isEnrolledTree(treeName);
            if (enrolled) {
                catalogConnection();
            }
            // The table dropped is the one the tree names, where a clear drops the one its row records.
            // The two are the same table by the time anything is deleted: a row recording another one is
            // not taken as an enrolment - readEnrolledTrees() keeps it out of enrolledTrees - so the
            // openTree that every delete of a tree comes after has rewritten it to this name.
            // A row is written before its table is created and taken out after its table is dropped,
            // never the other way round: of the two ways a half-done change can end, a catalog naming a
            // table that is not there is the one the removal is ready for - it skips such a row and says
            // so - while a table nothing names is adopted with its stale rows by the next open of that
            // tree and is dropped by no clear ever after. So this is deliberately not the mirror of
            // openTree(): an unenrolment left pending before the drop would be committed by the drop
            // itself on mysql and oracle, where DDL commits the transaction it finds open before it
            // executes, and would then stand even where the drop goes on to fail - ORA-00054 on a tree
            // another session holds, say, which write() does not replay, it being neither a class 40
            // state nor ORA-00060.
            if (isExistsTable(treeName)) {
                try {
                    commitStatement("drop table " + getTableName(treeName), true);
@@ -2287,7 +4045,8 @@
                    throw new StorageRuntimeException(e);
                }
            }
            // forget the mapping so listTrees() consumers (updateTableStatistics) skip the dropped table
            unenrolFromCatalog(treeName, enrolled);
            // the memoized table name of a tree nothing holds any more is of no use to anyone
            tree2table.invalidate(treeName);
            unstampableTrees.remove(treeName); // a table recreated later deserves a fresh stamp attempt
        }
@@ -2382,6 +4141,16 @@
        @Override
        public boolean delete(TreeName treeName, ByteSequence key) {
            checkReadOnly();
            return deleteRow(treeName, key);
        }
        /**
         * The statement of {@link #delete} without its read-only check, for the rows this class writes
         * on a transaction of its own making: the catalog of a backend is written through a transaction
         * over a connection of its own, whose access mode is read again as it is built, and the check
         * that matters was made by the caller of {@code openTree} or {@code deleteTree}.
         */
        boolean deleteRow(TreeName treeName, ByteSequence key) {
            try (final PreparedStatement statement=con.prepareStatement("delete from "+getTableName(treeName)+" where h="+hashParam(con)+" and k=?")){
                statement.setString(1,key2hash.get(ByteBuffer.wrap(key.toByteArray())));
                statement.setBytes(2,real2db(key.toByteArray()));
@@ -2523,8 +4292,11 @@
                throw new UnsupportedOperationException();
            }
            if (writeTableName==null) {
                // the enrolling name, unlike the read statements above: this writes to the tree, so
                // it is one this backend owns, and removeStorageFiles() has to know about it
                // the enrolling name, unlike the read statements above: this writes to the tree, so it is
                // one this backend owns and its table belongs in the memo of the storage. What a clear
                // drops is what the catalog of the backend names (#888), and openTree(name, true) is the
                // one thing that writes there - a tree written through a cursor is one the backend opened
                // to get the cursor, which is where its row comes from
                writeTableName=getTableName(treeName);
            }
            try (final PreparedStatement statement=con.prepareStatement("delete from "+writeTableName+" where h="+hashParam(con)+" and k=?")){
@@ -2637,9 +4409,185 @@
        }
    }
    
    /**
     * {@inheritDoc}
     * <p>
     * Answered from the catalog of the backend rather than from the trees this process happens to
     * have touched: {@link #removeStorageFiles()} runs before anything has touched one (#888).
     * <p>
     * What a tool has to be shown is not what a clear may drop: the shared compressed schema trees
     * are deliberately not enrolled - a backend must not offer a tree another one may own for removal
     * - and would go unnamed by {@code dbtest} for it, so they are added here when their tables are
     * there. {@link #catalogTables(Connection, TableScope)} is what the removal reads, and it
     * names them not.
     * <p>
     * The catalog itself is among the names, being a tree of this backend like any other: {@code
     * dbtest list-raw-dbs} counts it and {@code dump-raw-db} resolves its name, which is the one way
     * of seeing from outside the server what a clear of this backend would drop.
     */
    @Override
    public Set<TreeName> listTrees() {
        return tree2table.asMap().keySet();
        // validated, like the borrows of open() and removeStorageFiles(): since the catalog this reads
        // from, this borrow issues its statements far from itself and compensates a dropped connection
        // in no other way - a write is replayed and a read tells the pool, and this does neither, so a
        // connection dropped inside the alive window would surface out of a listing of tree names
        try (final Connection con=getValidatedConnection()) {
            return listTrees(con);
        } catch (StorageRuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new StorageRuntimeException(e);
        }
    }
    Set<TreeName> listTrees(Connection con) throws SQLException {
        final TableScope scope=TableScope.of(this, con);
        final Set<TreeName> trees=new HashSet<>(catalogTables(con, scope).keySet());
        for (final TreeName treeName : SHARED_COMPRESSED_SCHEMA_TREES) {
            // asked of the database, not assumed: the pair belongs to no backend in particular, and
            // once #881 gives each backend a pair of its own an installation may hold neither table.
            // Narrowed to this database: a pair of the same name in another database of the server
            // would otherwise have this backend name two trees it does not hold
            if (isExistsTable(con, scope, readTableName(treeName))) {
                trees.add(treeName);
            }
        }
        return trees;
    }
    /**
     * The trees the catalog of this backend names, each with the table recorded as holding it, the
     * catalog itself among them. Empty when the catalog table is not there - a backend which has
     * never been opened read-write - which is what tells {@link #removeStorageFiles()} it has nothing
     * it may drop.
     * <p>
     * The table name is taken from the row rather than recomputed from the tree name, so that a
     * removal drops what was enrolled even if the naming of tables were ever to change.
     * <p>
     * The scope is the caller's rather than asked for here: it is not free of a round trip - pgjdbc
     * answers both halves of it with a select of its own - and a clear and a listTrees() both narrow a
     * lookup of their own by it, so they pass what they have instead of every reader asking twice over.
     */
    Map<TreeName,String> catalogTables(Connection con, TableScope scope) throws SQLException {
        return catalogTables(con, scope, new ArrayList<>()); // nobody to tell: the descriptions go nowhere
    }
    /**
     * The same, telling the caller what the read passed over: a clear accounts for every row of its
     * catalog, and a row it could not act on is one nothing else in its report would name - the table
     * such a row records is outside the namespace {@link #leftoverTables} scans. See {@link
     * #reportClearOutcome}.
     */
    Map<TreeName,String> catalogTables(Connection con, TableScope scope, List<String> skippedRows) throws SQLException {
        final TreeName catalogTree=getCatalogTree();
        final String catalogTable=getTableName(catalogTree);
        // narrowed to this database: a catalog of the same name in another database of the server
        // would send the select below at a table that is not here, failing the clear it answers
        if (!isExistsTable(con, scope, catalogTable)) {
            return Collections.emptyMap();
        }
        final Map<TreeName,String> trees=readCatalogRows(con, catalogTable, skippedRows);
        // The catalog names every tree of the backend but itself, and is put last on purpose: the
        // removal drops the trees in this order, and what names them has to outlive them. Dropping a
        // table is DDL, which mysql and oracle commit as they go, so a removal that fails halfway is
        // finished by the next attempt rather than leaving behind tables nothing names any more.
        trees.remove(catalogTree); // no row should name it; one that does must not hold back the order
        trees.put(catalogTree, catalogTable);
        return trees;
    }
    /**
     * The rows of the catalog table as they stand, tree by tree: the caller has already established
     * that the table is there - {@link #catalogTables} by a lookup of its own, an enrolment by having
     * just created it or found it - so this asks the database nothing but the select.
     * <p>
     * A row this backend cannot have written is skipped and reported rather than trusted. What a clear
     * drops is the table a row records, dropped by that name, so a row recording something outside the
     * namespace this backend names its tables in points at a table that is nobody's business of this
     * one's - and a row naming no tree at all, or naming one that is not a tree name, would otherwise
     * fail every clear from here on rather than the one thing it describes.
     * <p>
     * Every row passed over is described into {@code skippedRows}, the warn above being addressed to
     * whoever is reading the log at that moment and this to the account a clear gives of itself: such
     * a row is a tree the clear cannot see, so what the row records is dropped by nothing - while the
     * row itself goes with the catalog table it sits in, which the clear names last and drops. That is
     * what makes the line the only surviving copy of what such a row said, and why it carries the
     * recorded name. A reader with nobody to tell - a read of {@code dbtest}, or the one an enrolment makes
     * - hands in a list of its own and lets it go, which is one allocation per read of a whole table
     * and no convention to get wrong.
     */
    Map<TreeName,String> readCatalogRows(Connection con, String catalogTable) throws SQLException {
        return readCatalogRows(con, catalogTable, new ArrayList<>());
    }
    Map<TreeName,String> readCatalogRows(Connection con, String catalogTable, List<String> skippedRows)
            throws SQLException {
        final Map<TreeName,String> trees=new LinkedHashMap<>();
        // the rows are read inside the bound rather than from a live ResultSet: #882 took the
        // executeResultSet() that returned one away, so a transfer cannot run with nothing bounding it
        try (final PreparedStatement statement=con.prepareStatement("select k,v from "+catalogTable)) {
            executeResultSet(statement, rs -> {
                while (rs.next()) {
                    final byte[] key=rs.getBytes("k");
                    if (key==null) { // no tree is named by a row with no key, and a clear must not fail over one
                        logger.warn(LocalizableMessage.raw("jdbc: table %s holds a row naming no tree at all: skipped",
                            catalogTable));
                        skippedRows.add("a row naming no tree at all");
                        continue;
                    }
                    final String name=new String(db2real(key), StandardCharsets.UTF_8);
                    final TreeName treeName;
                    try {
                        treeName=TreeName.valueOf(name);
                    } catch (RuntimeException e) { // reported rather than passed off as a backend with fewer trees
                        logger.warn(LocalizableMessage.raw("jdbc: table %s holds \"%s\", which is not the name of a tree: skipped",
                            catalogTable, name));
                        skippedRows.add("\""+name+"\", which is not the name of a tree");
                        continue;
                    }
                    final byte[] table=rs.getBytes("v");
                    final String tableName=table==null || table.length==0
                        ? readTableName(treeName) // a row of a version which recorded the name and not the table
                        : new String(table, StandardCharsets.UTF_8);
                    // The prefix and not the whole of the name: the table recorded is taken from the row
                    // rather than derived again so that a removal drops what was enrolled even if the naming
                    // of tables were ever to change, and every naming this backend could take up is inside
                    // the namespace it already scans for what a clear left standing. What the shape does have
                    // to rule out is anything that is not a bare identifier: this value is read back from a
                    // table and reaches a "drop table" that no driver will take a bind parameter for.
                    if (!isOwnTableName(tableName)) {
                        logger.warn(LocalizableMessage.raw("jdbc: table %s records tree %s at \"%s\", which is no table of this backend: skipped",
                            catalogTable, treeName, tableName));
                        skippedRows.add(treeName+" at \""+tableName+"\", which is no table of this backend");
                        continue;
                    }
                    trees.put(treeName, tableName);
                }
                return null;
            });
        }
        return trees;
    }
    /**
     * Whether a name read back from the catalog is one of this backend's tables: inside the namespace
     * it names them in, and a bare identifier besides. A clear drops the table a row records, by that
     * name, in a statement built by concatenation - the DDL of no engine here takes a bind parameter
     * for it - so a row is trusted to name a table of this backend and nothing else. The existence
     * lookup in front of the drop would answer no for most of what this rules out; it is not what
     * makes it safe.
     */
    static boolean isOwnTableName(String tableName) {
        if (!tableName.toLowerCase(Locale.ROOT).startsWith("opendj")) {
            return false;
        }
        for (int i=0;i<tableName.length();i++) {
            final char c=tableName.charAt(i);
            if (!(c>='a' && c<='z') && !(c>='A' && c<='Z') && !(c>='0' && c<='9') && c!='_' && c!='$') {
                return false;
            }
        }
        return true;
    }
    final class ImporterImpl implements Importer {
@@ -2739,10 +4687,12 @@
        }
        /**
         * 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.
         * Hands the connection back to the pool and closes the sessions the transaction opened
         * beside it - the stamp one and the catalog one (#888), both outside the pool and neither
         * outliving the import that opened it - 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 {
@@ -2761,7 +4711,11 @@
                    failure.addSuppressed(reported);
                }
            } finally {
                txw.stampSession.close();
                try {
                    txw.stampSession.close();
                } finally {
                    txw.catalogSession.close();
                }
            }
            return failure;
        }
opendj-server-legacy/src/main/java/org/opends/server/backends/pdb/PDBStorage.java
@@ -42,6 +42,7 @@
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
@@ -101,7 +102,37 @@
{
  private static final int IMPORT_DB_CACHE_SIZE = 32 * MB;
  private static final double MAX_SLEEP_ON_RETRY_MS = 50.0;
  /**
   * Number of attempts a {@link WriteableStorageImpl#write} makes before it propagates the conflict to the caller.
   * <p>
   * It is a budget of attempts and not of time, so it is only ever reached by the conflicts that report quickly.
   * PersistIt reports a write-write conflict only once it has waited on it, up to
   * {@code SharedResource.DEFAULT_MAX_WAIT_TIME} - a minute, which this backend never lowers - so a conflict slower
   * to report than {@link #MAX_RETRY_WINDOW_NANOS} spends the whole window inside its first attempt, is granted the
   * single replay that window's exemption guarantees, and gives up on the window after two attempts rather than
   * after this many.
   */
  static final int MAX_RETRIES = 10;
  /**
   * Wall-clock budget the replays of a {@link WriteableStorageImpl#write} may spend, in nanoseconds. It is checked
   * between attempts, so an attempt already running is never interrupted, and never before one replay has been
   * made: the loop returns after at most this window plus two attempts. It bounds the conflicts that are slow to
   * report, which {@link #MAX_RETRIES} alone does not - an operation whose own work takes seconds would otherwise
   * multiply that wait by the attempt count.
   */
  static final long MAX_RETRY_WINDOW_NANOS = 10L * 1000L * 1000L * 1000L; //10 s
  /**
   * Upper bound of the random delay before the second attempt, in milliseconds; it doubles with every attempt. This
   * is the bound of the flat sleep this loop took before it was bounded, so the first replay is delayed exactly as
   * it was and only the later ones back off.
   */
  private static final double BASE_SLEEP_ON_RETRY_MS = 50.0;
  /** Upper bound the doubled delay is capped at, in milliseconds. */
  private static final double MAX_SLEEP_ON_RETRY_MS = 1000.0;
  private static final String VOLUME_NAME = "dj";
  private static final String JOURNAL_NAME = VOLUME_NAME + "_journal";
  /** The buffer / page size used by the PersistIt storage. */
@@ -635,8 +666,11 @@
    public void write(WriteOperation operation) throws Exception
    {
      final Transaction txn = db.getTransaction();
      for (;;)
      final long startedAt = System.nanoTime();
      final long giveUpAt = startedAt + retryWindowNanos;
      for (int attempt = 1;; attempt++)
      {
        final RollbackException conflict;
        txn.begin();
        try
        {
@@ -653,8 +687,7 @@
        }
        catch (final RollbackException e)
        {
          // retry after random sleep (reduces transactions collision. Drawback: increased latency)
          Thread.sleep((long) (Math.random() * MAX_SLEEP_ON_RETRY_MS));
          conflict = e;
        }
        catch (final Exception e)
        {
@@ -665,6 +698,68 @@
        {
          txn.end();
        }
        // decided and slept for outside the try statement: the sleep used to run before the finally ended the
        // rolled back transaction, holding it open for the whole backoff and lengthening the window every other
        // writer collides with
        //System.nanoTime() - giveUpAt is the overflow safe form of the comparison, and attempt > 1 keeps the
        //window from ending the loop before a single replay: persistit reports a write-write conflict only once
        //it has waited on it, up to SharedResource.DEFAULT_MAX_WAIT_TIME - a minute, which this backend never
        //lowers - so one attempt can outlast the window on its own, and it is the attempt after that one which
        //is likeliest to succeed, the transaction that blocked it having just finished
        //one clock sample for both, so that the elapsed time reported is the one the give up was decided on
        final long now = System.nanoTime();
        final boolean capSpent = attempt >= maxRetries;
        if (capSpent || (attempt > 1 && now - giveUpAt >= 0))
        {
          final long elapsedMs = TimeUnit.NANOSECONDS.toMillis(now - startedAt);
          //which of the two bounds was spent, so that the config change paths - which report this as a bare stack
          //trace with no message id - say whether raising the attempts or the window is what would have helped
          final String boundSpent = capSpent ? "attempt cap" : "retry window";
          final StorageRuntimeException spent = new StorageRuntimeException(
              "pdb: backend '" + config.getBackendId() + "' did not apply the transaction after " + attempt
                  + " attempts in " + elapsedMs + " ms, the " + boundSpent + " being spent; the last conflict was "
                  + conflict);
          // the conflict is suppressed rather than made the cause, because a cause is what every caller strips
          // this message off with: write(WriteOperation) below unwraps a StorageRuntimeException that carries one
          // and throws the cause in its place, and EntryContainer.throwAllowedExceptionTypes:1121 rethrows a
          // StorageRuntimeException unchanged only while getCause() is null, wrapping it a second time otherwise.
          // Either way the caller would be left holding a bare RollbackException, whose StorageRuntimeException
          // message is only its class name - which is all ERR_OPEN_ENV_FAIL would then print at startup
          spent.addSuppressed(conflict);
          //warned once, at exhaustion only, unlike JDBCStorage which warns on every replay: a conflict is routine
          //on the ordinary add and modify path of this engine and a line per replay would flood the log. It names
          //the bound that was spent for the same reason the exception does, and it is the only rendering that can
          //carry the stack of the conflict: stackTraceToSingleLineString, the form the config change paths report
          //this exception with, walks the causes and never prints a suppressed exception
          logger.warn(LocalizableMessage.raw("pdb: giving up on the transaction of backend '%s' after %d attempts"
              + " in %d ms, the %s being spent: %s", config.getBackendId(), attempt, elapsedMs, boundSpent,
              stackTraceToSingleLineString(conflict)));
          throw spent;
        }
        if (logger.isTraceEnabled())
        {
          logger.trace("pdb: replaying the transaction after %s, attempt %d of %d", conflict, attempt, maxRetries);
        }
        try
        {
          // retry after random sleep (reduces transactions collision. Drawback: increased latency), growing with
          // every attempt so that a contention the first delays did not outlast still has a chance to clear
          Thread.sleep(retryDelayMillis(attempt));
        }
        catch (final InterruptedException e)
        {
          //sleep cleared the interrupt flag: restore it, and report the conflict being replayed rather than the
          //interrupt, which would hide from the caller what actually went wrong. Wrapped the way the exhausted
          //loop above wraps it, and for the same reason: a RollbackException carries no message of its own, so
          //every caller that wraps one reports nothing but its class name
          Thread.currentThread().interrupt();
          final StorageRuntimeException interrupted = new StorageRuntimeException(
              "pdb: backend '" + config.getBackendId() + "' was interrupted while replaying the transaction after "
                  + attempt + " attempts; the last conflict was " + conflict);
          interrupted.addSuppressed(conflict);
          interrupted.addSuppressed(e);
          throw interrupted;
        }
      }
    }
  }
@@ -915,6 +1010,10 @@
  private PDBMonitor monitor;
  private MemoryQuota memQuota;
  private StorageStatus storageStatus = StorageStatus.working();
  /** Attempt bound of a {@link WriteableStorageImpl#write}, {@link #MAX_RETRIES} outside the tests. */
  private final int maxRetries;
  /** Wall-clock bound of a {@link WriteableStorageImpl#write}, {@link #MAX_RETRY_WINDOW_NANOS} outside the tests. */
  private final long retryWindowNanos;
  /**
   * Creates a new persistit storage with the provided configuration.
@@ -928,7 +1027,34 @@
  // FIXME: should be package private once importer is decoupled.
  public PDBStorage(final PDBBackendCfg cfg, ServerContext serverContext) throws ConfigException
  {
    this(cfg, serverContext, MAX_RETRIES, MAX_RETRY_WINDOW_NANOS);
  }
  /**
   * Creates a new persistit storage whose replay bounds are the given ones rather than {@link #MAX_RETRIES} and
   * {@link #MAX_RETRY_WINDOW_NANOS}.
   * <p>
   * Only a test builds one of these, and it does so to stop the two bounds racing each other: with the shipped
   * values a run of replays spends a random share of the window on backoff alone, so a test of the attempt cap
   * can be ended by the window on a loaded machine, and a test of the window has to spend seconds of build time
   * to reach it.
   *
   * @param cfg
   *          The configuration.
   * @param serverContext
   *          This server instance context
   * @param maxRetries
   *          Number of attempts a write makes before it propagates the conflict to the caller.
   * @param retryWindowNanos
   *          Wall-clock budget the replays of a write may spend, in nanoseconds.
   * @throws ConfigException if memory cannot be reserved
   */
  PDBStorage(final PDBBackendCfg cfg, ServerContext serverContext, int maxRetries, long retryWindowNanos)
      throws ConfigException
  {
    this.serverContext = serverContext;
    this.maxRetries = maxRetries;
    this.retryWindowNanos = retryWindowNanos;
    backendDirectory = getBackendDirectory(cfg);
    config = cfg;
    cfg.addPDBChangeListener(this);
@@ -1095,6 +1221,24 @@
    return new ImporterImpl();
  }
  /**
   * {@inheritDoc}
   * <p>
   * A transaction the engine rolled back is replayed, bounded twice: by {@link #MAX_RETRIES} attempts and by the
   * {@link #MAX_RETRY_WINDOW_NANOS} wall-clock window, whichever is spent first - except that the window alone
   * never ends the replays before one has been made. It is bounded because the
   * configuration change paths of the pluggable backend hold an entry container's exclusive lock across this
   * method, and every reader of that suffix then waits - untimed and uninterruptibly - until it returns, so a
   * conflict that never clears would park every worker thread of that suffix rather than fail one operation.
   * <p>
   * Once the bound is spent the conflict is reported as a {@link StorageRuntimeException} naming the backend, the
   * attempts spent, the time they took and which of the two bounds ran out. It carries the conflict as a
   * suppressed exception rather than as its cause: a cause is unwrapped below and thrown in its place, and
   * {@code EntryContainer.throwAllowedExceptionTypes} likewise passes a {@link StorageRuntimeException} through
   * untouched only while it has no cause. Given a cause, both hand the caller a bare RollbackException instead,
   * and the message of a {@link StorageRuntimeException} wrapping one is just its class name - which is all
   * {@code ERR_OPEN_ENV_FAIL} would report when this happens as a backend starts.
   */
  @Override
  public void write(final WriteOperation operation) throws Exception
  {
@@ -1108,6 +1252,14 @@
    }
  }
  /** Returns the randomized delay before the given attempt is replayed, doubling with each attempt up to a cap. */
  //package private like the JDBCStorage copy it is taken from, so that a test can pin the growth and the cap
  static long retryDelayMillis(int attempt)
  {
    final double bound = Math.min(MAX_SLEEP_ON_RETRY_MS, BASE_SLEEP_ON_RETRY_MS * (1 << Math.min(attempt - 1, 5)));
    return (long) (Math.random() * bound);
  }
  private Exception unwrap(StorageRuntimeException e) throws Exception
  {
    if (e.getCause() != null)
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/BackendImpl.java
@@ -18,11 +18,13 @@
package org.opends.server.backends.pluggable;
import static org.forgerock.util.Reject.*;
import static org.forgerock.util.Utils.closeSilently;
import static org.opends.messages.BackendMessages.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -50,9 +52,11 @@
import org.opends.server.backends.pluggable.spi.Storage;
import org.opends.server.backends.pluggable.spi.StorageInUseException;
import org.opends.server.backends.pluggable.spi.StorageRuntimeException;
import org.opends.server.backends.pluggable.spi.TreeName;
import org.opends.server.backends.pluggable.spi.WriteOperation;
import org.opends.server.backends.pluggable.spi.WriteableTransaction;
import org.opends.server.core.AddOperation;
import org.opends.server.core.BackendConfigManager;
import org.opends.server.core.DeleteOperation;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.ModifyDNOperation;
@@ -845,83 +849,333 @@
    return true;
  }
  /**
   * {@inheritDoc}
   * <p>
   * {@link Storage#write(WriteOperation)} replays its operation after a transaction conflict, so
   * the operation below is confined to work a rollback undoes: the trees are deleted and opened
   * there, while the registries, which no rollback reaches, are updated once the write has
   * committed. Getting this the wrong way round leaves the change half applied, and its replay
   * reports the missing half rather than the conflict that caused it.
   * <p>
   * What makes the operation replayable is that the base DNs to remove and to add are worked out
   * once, ahead of the write, so that no attempt can see different work to do than the attempt it
   * is replacing.
   */
  @Override
  public ConfigChangeResult applyConfigurationChange(final PluggableBackendCfg newCfg)
  {
    final ConfigChangeResult ccr = new ConfigChangeResult();
    try
    // Read once: importLDIF, rebuildBackend, exportLDIF and verifyBackend all assign this field
    // and null it out again, and this method now goes on using it past the commit.
    final RootContainer rc = rootContainer;
    if (rc == null)
    {
      if(rootContainer != null)
      return ccr;
    }
    final SortedSet<DN> newBaseDNs = newCfg.getBaseDN();
    // Ask the root container what this backend holds rather than the configuration it was last
    // given: a base DN which an earlier, failed change left behind is work to do, and a
    // configuration which was never applied is not. RootContainer.getBaseDNs() is a live view of
    // the registered containers, so take a copy of it before anything registers one.
    final Set<DN> currentBaseDNs = new HashSet<>(rc.getBaseDNs());
    final List<EntryContainer> deleted = new ArrayList<>();
    for (DN baseDN : currentBaseDNs)
    {
      if (!newBaseDNs.contains(baseDN))
      {
        rootContainer.getStorage().write(new WriteOperation()
        final EntryContainer ec = rc.getEntryContainer(baseDN);
        // Answered exactly, never with an ancestor's container: getEntryContainer walks up the DN
        // until it finds one, which is how an entry is routed to the base DN above it, and one
        // backend holds hierarchically related base DNs whenever a registry which refused one left
        // its container behind. Deleting the trees an ancestor answered with is deleting the trees
        // of a base DN this backend is still serving.
        if (ec == null || !baseDN.equals(ec.getBaseDN()))
        {
          @Override
          public void run(WriteableTransaction txn) throws Exception
          {
            SortedSet<DN> newBaseDNs = newCfg.getBaseDN();
            // Check for changes to the base DNs.
            removeDeletedBaseDNs(newBaseDNs, txn);
            if (!createNewBaseDNs(newBaseDNs, ccr, txn))
            {
              return;
            }
            baseDNs = new HashSet<>(newBaseDNs);
            // Put the new configuration in place.
            cfg = newCfg;
          }
        });
          // Unregistered since the copy above was taken, which is what closing the root container
          // leaves behind: importLDIF, rebuildBackend and exportLDIF all do that, as does a backend
          // being disabled. There is nothing to delete and nothing to say about the rest of the
          // change, so none of it is attempted - and a result is returned rather than an exception,
          // which is what the administration framework is owed whatever happens.
          ccr.setResultCode(serverContext.getCoreConfigManager().getServerErrorResultCode());
          ccr.addMessage(ERR_BACKEND_BASEDN_NO_LONGER_HELD.get(getBackendID(), baseDN));
          return ccr;
        }
        deleted.add(ec);
      }
    }
    catch (Exception e)
    final List<DN> added = new ArrayList<>();
    for (DN baseDN : newBaseDNs)
    {
      ccr.setResultCode(serverContext.getCoreConfigManager().getServerErrorResultCode());
      ccr.addMessage(LocalizableMessage.raw(stackTraceToSingleLineString(e)));
      if (!currentBaseDNs.contains(baseDN))
      {
        added.add(baseDN);
      }
    }
    if (deleted.isEmpty() && added.isEmpty())
    {
      // The common case - index-entry-limit, db-cache-percent, preload-time-limit and the rest,
      // which the entry containers apply through their own listeners. There is no storage work to
      // do, so no transaction is opened to commit nothing.
      baseDNs = new HashSet<>(newBaseDNs);
      cfg = newCfg;
      return ccr;
    }
    // Opened by the write operation, registered only once it has committed.
    final List<EntryContainer> created = new ArrayList<>();
    try
    {
      try
      {
        changeBaseDNTrees(rc, deleted, added, created);
      }
      catch (Exception e)
      {
        logger.traceException(e);
        ccr.setResultCode(serverContext.getCoreConfigManager().getServerErrorResultCode());
        // The failure alone never says which base DNs the change was about, so name them.
        //
        // Only persistit and je roll the whole write back, leaving nothing at all applied and
        // neither registry to touch. The jdbc backend does not, on any of its engines: its
        // commitStatement() issues the statement and commits it, and commitsBeforeDdl() decides
        // only which side of the statement the attempt stops being replayable on, never whether a
        // completed "create table" or "drop table" survives the rollback of the write around it.
        // Neither does cassandra, which has no transaction to roll back.
        // giveUpBaseDNsWhoseTreesAreGone below reads what is actually left rather than trusting
        // either answer.
        ccr.addMessage(ERR_BACKEND_CANNOT_CHANGE_BASEDNS.get(
            getBackendID(), baseDNsOf(deleted), added, stackTraceToSingleLineString(e)));
        // Read before the entry containers are closed: what a container holds is what says which
        // trees belong to it.
        giveUpBaseDNsWhoseTreesAreGone(rc, deleted, ccr);
        closeSilently(created);
        return ccr;
      }
      // The change is durable from here on, so every base DN is seen through even if one fails.
      for (EntryContainer ec : deleted)
      {
        deregisterDeletedBaseDN(rc, ec, ccr);
      }
      registerNewBaseDNs(rc, created, ccr);
      // Put the new configuration in place.
      cfg = newCfg;
    }
    finally
    {
      // What the root container ended up holding, not what was asked for: a base DN whose
      // registration failed is not one this backend serves, and getBaseDNs() is what the monitors,
      // isIndexed() and closeBackend() are answered from. Taken on the way out of every path which
      // reached the write, the failed ones included, so that the two never disagree. The change
      // which had no storage work to do sets it from the new configuration above; the one which
      // found a base DN this backend no longer holds leaves it alone, since the root container it
      // would be read from is being closed underneath it.
      baseDNs = new HashSet<>(rc.getBaseDNs());
    }
    return ccr;
  }
  private void removeDeletedBaseDNs(SortedSet<DN> newBaseDNs, WriteableTransaction txn) throws DirectoryException
  /**
   * Deletes the trees of the base DNs being removed and opens the ones being added, as the single
   * write operation a storage engine may replay.
   * <p>
   * The trees of a removed base DN are deleted while it is still registered, so its entry container
   * is held exclusively for as long as the write runs, retries included, as
   * {@link RootContainer#close()}, EntryContainer's index delete listener and AttributeIndex all do.
   * That keeps out the operations which arrive during that window; an operation which had taken hold
   * of the container before the lock still ends up in a closed one once it is released, as it did
   * before this ordering.
   * <p>
   * The locks are given up with the write and are never held into the registry work which follows it.
   * {@link BackendConfigManager} guards its registry with a single lock which the server already
   * takes in the opposite order - {@code shutdownLocalBackends}, a backend being disabled and
   * {@code applyConfigurationDelete} all hold it while finalizing a backend, which closes its root
   * container and locks every entry container in turn. Holding the container lock into
   * {@code deregisterBaseDN} would deadlock a base DN change against a shutdown, with no timeout on
   * either side.
   */
  private void changeBaseDNTrees(final RootContainer rc, final List<EntryContainer> deleted,
      final List<DN> added, final List<EntryContainer> created) throws Exception
  {
    for (DN baseDN : cfg.getBaseDN())
    for (EntryContainer ec : deleted)
    {
      if (!newBaseDNs.contains(baseDN))
      // Taken outside the try, because EntryContainer.lock() has no throwing path - the write side
      // of a ReentrantReadWriteLock, then a drain which swallows the interrupt - so every one of
      // them is held by the time it is entered, and unlocking what was never locked cannot happen.
      ec.lock();
    }
    try
    {
      rc.getStorage().write(new WriteOperation()
      {
        // The base DN was deleted.
        serverContext.getBackendConfigManager().deregisterBaseDN(baseDN);
        EntryContainer ec = rootContainer.unregisterEntryContainer(baseDN);
        ec.close();
        ec.delete(txn);
        @Override
        public void run(WriteableTransaction txn) throws Exception
        {
          // Give up what a previous, rolled back attempt had opened: its trees are gone, and its
          // entry containers still hold the configuration listeners they registered.
          closeSilently(created);
          created.clear();
          // Opening the added base DNs comes first, so that the failure this operation is most
          // likely to meet is met while everything is still there to roll back to. Once a tree
          // has been deleted, a storage engine which does not undo that has nothing to give
          // back.
          for (DN baseDN : added)
          {
            created.add(rc.openEntryContainer(baseDN, txn, AccessMode.READ_WRITE));
          }
          for (EntryContainer ec : deleted)
          {
            ec.delete(txn);
          }
        }
      });
    }
    finally
    {
      for (EntryContainer ec : deleted)
      {
        ec.unlock();
      }
    }
  }
  private boolean createNewBaseDNs(Set<DN> newBaseDNs, ConfigChangeResult ccr, WriteableTransaction txn)
  /**
   * Gives up the base DNs whose trees the failed write took with it. There is anything to give up
   * only on an engine which does not roll the write back whole: mysql and oracle commit their DDL of
   * their own accord, cassandra has no transaction at all, and the jdbc backend commits after each
   * statement on every engine it supports. A base DN kept registered without its trees answers every
   * operation with a storage error, where its removal was meant to leave a plain "no such entry";
   * one whose trees the rollback put back is left exactly as it was.
   * <p>
   * The trees the same write created for a base DN which is not being added after all are left where
   * they are, and this is the only place which could have taken them back. The configuration naming
   * that base DN was stored before this listener was called - {@code
   * ConfigurationHandler.replaceEntry} writes the entry, and only then notifies its change listeners
   * - and the failure does not take it back, so {@link RootContainer#open} opens that base DN again
   * from it the next time this backend is opened, adopting the trees which survived and creating the
   * ones which did not. Deleting them here would take away the trees of a base DN the stored
   * configuration still asks this backend to serve, and would reach only the base DNs whose opening
   * succeeded anyway: one which failed while being opened never became an entry container, and
   * nothing but its own trees names them.
   */
  private void giveUpBaseDNsWhoseTreesAreGone(RootContainer rc, List<EntryContainer> deleted, ConfigChangeResult ccr)
  {
    for (DN baseDN : newBaseDNs)
    if (deleted.isEmpty())
    {
      if (!rootContainer.getBaseDNs().contains(baseDN))
      return;
    }
    final Set<TreeName> storedTrees;
    try
    {
      storedTrees = rc.getStorage().listTrees();
    }
    catch (Exception e)
    {
      // Nothing can be said about what survived, so nothing is given up on the strength of it - and
      // that is the case where the failure itself says least about what this backend is left
      // serving, so it is said outright rather than left to a bare admin action.
      logger.traceException(e);
      ccr.setAdminActionRequired(true);
      ccr.addMessage(ERR_BACKEND_CANNOT_LIST_TREES_AFTER_BASEDN_CHANGE.get(
          getBackendID(), stackTraceToSingleLineString(e)));
      return;
    }
    for (EntryContainer ec : deleted)
    {
      if (!storedTrees.containsAll(treeNamesOf(ec)))
      {
        try
        {
          // The base DN was added.
          EntryContainer ec = rootContainer.openEntryContainer(baseDN, txn, AccessMode.READ_WRITE);
          rootContainer.registerEntryContainer(baseDN, ec);
          serverContext.getBackendConfigManager().registerBaseDN(baseDN, this, false);
        }
        catch (Exception e)
        {
          logger.traceException(e);
        ccr.setAdminActionRequired(true);
        deregisterDeletedBaseDN(rc, ec, ccr);
      }
    }
  }
          ccr.setResultCode(serverContext.getCoreConfigManager().getServerErrorResultCode());
          ccr.addMessage(ERR_BACKEND_CANNOT_REGISTER_BASEDN.get(baseDN, e));
          return false;
  private static Set<TreeName> treeNamesOf(EntryContainer ec)
  {
    final Set<TreeName> names = new HashSet<>();
    for (Tree tree : ec.listTrees())
    {
      names.add(tree.getName());
    }
    return names;
  }
  private void deregisterDeletedBaseDN(RootContainer rc, EntryContainer ec, ConfigChangeResult ccr)
  {
    final DN baseDN = ec.getBaseDN();
    final BackendConfigManager backendConfigManager = serverContext.getBackendConfigManager();
    try
    {
      backendConfigManager.deregisterBaseDN(baseDN);
    }
    catch (Exception e)
    {
      logger.traceException(e);
      if (backendConfigManager.getLocalBackendWithBaseDN(baseDN) == this)
      {
        // deregisterBaseDN puts its new registry in place only once it has succeeded, so this base
        // DN is still routed here. Leave the entry container registered: closeBackend() reclaims a
        // base DN through rootContainer.getBaseDNs(), and one taken out of there would stay claimed
        // by a backend which no longer holds it until the server is restarted. That is the opposite
        // of what deregisterBaseDNsWhoseTreesAreGone does, and for the opposite reason: there the
        // registry has already stopped routing to the base DN, so keeping the container only leaves
        // a storage error where a "no such entry" was meant to be, while here the registry is still
        // routing to it and dropping the container is what would leave that error behind.
        ccr.setResultCode(serverContext.getCoreConfigManager().getServerErrorResultCode());
        ccr.setAdminActionRequired(true);
        ccr.addMessage(ERR_BACKEND_CANNOT_DEREGISTER_BASEDN.get(baseDN, stackTraceToSingleLineString(e)));
        return;
      }
      // It is not registered here, which is what an earlier change whose registerBaseDN failed
      // leaves behind. Nothing routes to it, so there is nothing to hold on to.
    }
    rc.unregisterEntryContainer(baseDN);
    closeSilently(ec);
  }
  private void registerNewBaseDNs(RootContainer rc, List<EntryContainer> created, ConfigChangeResult ccr)
  {
    for (EntryContainer ec : created)
    {
      final DN baseDN = ec.getBaseDN();
      boolean registered = false;
      try
      {
        rc.registerEntryContainer(baseDN, ec);
        registered = true;
        serverContext.getBackendConfigManager().registerBaseDN(baseDN, this, false);
      }
      catch (Exception e)
      {
        logger.traceException(e);
        ccr.setResultCode(serverContext.getCoreConfigManager().getServerErrorResultCode());
        ccr.setAdminActionRequired(true);
        ccr.addMessage(ERR_BACKEND_CANNOT_REGISTER_BASEDN.get(baseDN, stackTraceToSingleLineString(e)));
        if (!registered)
        {
          // Nothing else can reclaim it: closeBackend() and RootContainer.close() both work from
          // the registered containers, and this one keeps the configuration listeners its
          // constructor registered for as long as it is alive.
          closeSilently(ec);
        }
      }
    }
    return true;
  }
  private static List<DN> baseDNsOf(List<EntryContainer> entryContainers)
  {
    final List<DN> baseDNs = new ArrayList<>(entryContainers.size());
    for (EntryContainer ec : entryContainers)
    {
      baseDNs.add(ec.getBaseDN());
    }
    return baseDNs;
  }
  /**
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java
@@ -2423,8 +2423,11 @@
  }
  /**
   * Delete this entry container from disk. The entry container should be
   * closed before calling this method.
   * Deletes this entry container from disk, that is, every tree {@link #listTrees()} enumerates.
   * The entry container may be open or closed: the trees are taken from the attribute and VLV index
   * maps, which {@link #close()} closes the indexes of but leaves populated, so the same set is
   * deleted either way. A {@code close()} which cleared those maps would turn a call made after it
   * into a partial deletion, silently. Either way the container is not to be used afterwards.
   *
   * @param txn a non null transaction
   * @throws StorageRuntimeException If an error occurs while removing the entry container.
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/spi/Storage.java
@@ -73,13 +73,24 @@
  <T> T read(ReadOperation<T> readOperation) throws Exception;
  /**
   * Executes a write operation. In case of a write operation rollback, implementations must ensure
   * the write operation is retried until it succeeds.
   * Executes a write operation. In case of a write operation rollback, implementations may replay the write
   * operation rather than propagate the failure: a {@link WriteOperation} is required to be idempotent for
   * exactly that reason. A replay must be bounded - by a number of attempts, by a window of time, or by both -
   * so that a conflict which does not clear reaches the caller instead of being retried forever. The pluggable
   * backend holds locks across this method, up to the exclusive lock of an entry container, and every thread
   * waiting on one of those locks waits for as long as this method does.
   * <p>
   * A caller that mutates state around this method must handle that bound being spent. Removing an entry from an
   * in-memory map before the write so that a replay still finds the work to do, or reading configuration back out
   * of the operation once it returns, both assume the write is applied; when it is not, this method throws with
   * that state already changed and the transaction not applied, and the caller is the only place that can reconcile
   * the two.
   *
   * @param writeOperation
   *          the write operation to execute
   * @throws Exception
   *           if a problem occurs with the underlying storage engine
   *           if a problem occurs with the underlying storage engine, including a conflict that outlasted the
   *           replays the implementation makes
   */
  void write(WriteOperation writeOperation) throws Exception;
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java
@@ -2105,7 +2105,21 @@
  public void publishReplicaOfflineMsg()
  {
    final CSN offlineCSN = pendingChanges.putReplicaOfflineMsg();
    dsrsShutdownSync.replicaOfflineMsgSent(getBaseDN(), offlineCSN);
    if (offlineCSN != null)
    {
      /*
       * Only a message which really was published is announced: the shutdown of a collocated
       * replication server waits for it to be forwarded, and would spend the whole grace
       * period waiting for one which never reached the wire.
       */
      dsrsShutdownSync.replicaOfflineMsgSent(getBaseDN(), offlineCSN);
    }
    else if (logger.isTraceEnabled())
    {
      logger.trace("Replica " + getServerId() + " of domain baseDN=" + getBaseDN()
          + " could not announce itself offline: a change which is still in flight holds"
          + " the message back, and " + pendingChanges.size() + " change(s) are pending");
    }
  }
  /**
@@ -2865,12 +2879,16 @@
   * Returns whether the provided result code reports a failure of this server rather
   * than a change which can not be applied: the backend being offline or rebuilt
   * (OPENDJ-49), or the storage failing to serve the operation.
   * <p>
   * Package private for the tests, which pin what it answers for every registered result
   * code directly rather than through a running server.
   *
   * @param result the result code of a replayed operation
   * @param serverErrorResultCode the result code this server puts on an internal error
   * @return {@code true} if the operation failed on the server itself
   */
  private static boolean isServerFailure(ResultCode result, ResultCode serverErrorResultCode)
  @VisibleForTesting
  static boolean isServerFailure(ResultCode result, ResultCode serverErrorResultCode)
  {
    /*
     * The result code the server puts on an internal error is configurable and is not
@@ -3936,10 +3954,39 @@
        return;
      }
      enableService();
      sessionGeneration++;
      /*
       * The flag is cleared before the session is started, where disable() sets it before
       * stopping one: enableService() ends with startListenService(), so the listener it
       * starts can list a delivery and hand it to a replay thread while this method is
       * still running. A replay thread which reads a flag that still says "disabled"
       * gives the change up at the top of its replay loop, and abandonReplay() does not
       * ask for it again - a domain on its way down owns its session - so the change is
       * left listed, uncommitted and owned by nobody. Nothing would replay it: the
       * replication server only sends it again over a session which is restarted, so this
       * domain's ServerState, and every change which depends on that one, would be held
       * back for as long as the session lives.
       */
      disabled = false;
      boolean started = false;
      try
      {
        enableService();
        sessionGeneration++;
        started = true;
      }
      finally
      {
        if (!started)
        {
          /*
           * The other half of the same invariant: a domain whose session could not be
           * started owns that session the way a disabled one does, so the flag goes back
           * where it was rather than leave the replay threads believing there is a session
           * of theirs to restart.
           */
          disabled = true;
        }
      }
    }
  }
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/PendingChanges.java
@@ -123,9 +123,17 @@
  }
  /**
   * Add a replica offline message to the pending list.
   * Add a replica offline message to the pending list and publish it, if the changes which
   * come before it have all been published.
   * <p>
   * The message carries the newest CSN of the replica, so a change which is still in flight
   * holds it back - and there is nobody left to publish it afterwards: the caller announces
   * the replica offline while its service is being disabled, and the broker stops right after.
   * Such a message is given up on rather than left queued, so that it is neither reported as
   * sent nor published later on the session which follows.
   *
   * @return the CSN of the message which was added
   * @return the CSN of the message which was published, or {@code null} if it could not be
   *         published
   */
  public synchronized CSN putReplicaOfflineMsg()
  {
@@ -136,7 +144,10 @@
    pendingChanges.put(offlineCSN, pendingChange);
    pushCommittedChanges();
    return offlineCSN;
    // pushCommittedChanges() removes whatever it published, so the message is still listed
    // here if and only if a change before it held it back.
    final boolean heldBack = pendingChanges.remove(offlineCSN) != null;
    return heldBack ? null : offlineCSN;
  }
  /**
opendj-server-legacy/src/messages/org/opends/messages/backend.properties
@@ -1114,3 +1114,21 @@
ERR_COMPSCHEMA_CANNOT_MIGRATE_619=The compressed schema definitions of backend '%s' could not be migrated from \
 the shared tree '%s' to '%s': %s. The backend cannot be opened, because its entries were encoded against the \
 definitions that were not migrated and would decode as the wrong attributes
ERR_BACKEND_CANNOT_DEREGISTER_BASEDN_620=An error occurred while attempting to deregister base DN %s \
 from the Directory Server:  %s
ERR_BACKEND_CANNOT_CHANGE_BASEDNS_621=The base DNs of backend %s could not be changed (to remove: %s, \
 to add: %s):  %s. A storage engine which rolls the whole write back leaves the backend exactly as it \
 was; on one which does not, the base DNs whose trees are gone have been given up. The base DNs being \
 added are not being served, but the configuration which has been stored still names them, so the next \
 time this backend is opened it opens them from that configuration, keeping whatever trees the failed \
 change created for them and creating the ones it did not. A base DN being removed of which only some \
 trees survived is given up with those trees still in the storage, where nothing names them afterwards; \
 removing them means re-creating the backend
ERR_BACKEND_CANNOT_LIST_TREES_AFTER_BASEDN_CHANGE_622=The base DN change of backend %s failed, and the \
 trees which survived it could not be listed:  %s. No base DN has been given up on the strength of that, \
 so this backend may still be serving one whose trees are gone, which answers every operation against it \
 with a storage error
ERR_BACKEND_BASEDN_NO_LONGER_HELD_623=The base DNs of backend %s could not be changed: base DN %s is no \
 longer one this backend holds, which is what closing its root container leaves behind - an LDIF import, \
 an index rebuild, an LDIF export and the backend being disabled all do that. Nothing has been changed; \
 submit the change again once that has finished
opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/CachedConnectionTestCase.java
@@ -104,6 +104,9 @@
    /** 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;
    /** 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;
    @BeforeClass
    public void registerStubDriver() throws Exception {
        DriverManager.registerDriver(stub);
@@ -131,10 +134,12 @@
        System.clearProperty(CachedConnection.POOL_MAX_PROPERTY);
        System.clearProperty(CachedConnection.TTL_PROPERTY);
        System.clearProperty(CachedConnection.ALIVE_BYPASS_PROPERTY);
        System.clearProperty(CachedConnection.READ_TIMEOUT_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;
        CachedConnection.readTimeoutMillis = CONFIGURED_READ_TIMEOUT_MILLIS;
    }
    /**
@@ -1668,6 +1673,28 @@
        assertEquals(CachedConnection.attemptSeconds(30, Long.MAX_VALUE), 30);
    }
    /**
     * A deadline so far off that naming it overflows is a wait with no end, and not one already
     * behind us: a borrow configured to wait practically forever would otherwise give up on its
     * first retryable failure, which is the opposite of what was asked for. The sum is what has to
     * be guarded and not only the product - a value under the clamp of the product can still name a
     * moment past the end of the epoch.
     */
    @Test
    public void testADeadlineTooFarOffToNameIsAWaitWithNoEnd() throws Exception {
        final long startedAt = System.currentTimeMillis();
        // 0 is the operator asking for no deadline at all
        assertEquals(CachedConnection.deadlineOf(startedAt, 0), Long.MAX_VALUE);
        // ... and so is a value whose milliseconds would not fit a long at all
        assertEquals(CachedConnection.deadlineOf(startedAt, Long.MAX_VALUE / 1000), Long.MAX_VALUE);
        // the one the product guard lets through, which is the largest value it does: a second under
        // the clamp, so the milliseconds of it still fit a long - by 1807 of them - while the moment
        // they name, counted from now, does not. Guarded by the sum alone
        assertEquals(CachedConnection.deadlineOf(startedAt, Long.MAX_VALUE / 1000 - 1), Long.MAX_VALUE);
        // and an ordinary value still names the moment it says
        assertEquals(CachedConnection.deadlineOf(startedAt, 60), startedAt + 60_000);
    }
    /** The connection string holds the credentials of the backend: a stall report must not carry them. */
    @Test
    public void testLoggedConnectionStringCarriesNoCredentials() throws Exception {
@@ -1768,6 +1795,18 @@
        assertFalse(stall.contains("S3cret"), stall);
        assertTrue(stall.contains("jdbc:postgresql://h:5432/db"), stall);
        assertTrue(stall.contains("4000 ms") && stall.contains("(3 attempts)"), stall);
        // and so is the stall of a connect made outside the pool - the connection the tree catalog of a
        // backend is written on (#888) - which is under the same rule and describes the same url
        final String outside = CachedConnection.outsidePoolStallMessage(url, "tree catalog", 3, 4000,
            new SQLException("FATAL: too many connections for " + url));
        assertFalse(outside.contains("S3cret"), outside);
        assertTrue(outside.contains("jdbc:postgresql://h:5432/db"), outside);
        assertTrue(outside.contains("4000 ms") && outside.contains("(3 attempts)"), outside);
        assertTrue(outside.contains("tree catalog"), outside);
        // and says what it is: a borrow of the pool is what this connect is not, and an operator
        // reading it must not be sent to the pool for a stall the pool has no part in
        assertFalse(outside.contains("pooled one"), outside);
    }
    /**
@@ -1965,6 +2004,259 @@
    }
    /**
     * What a connection carries once the login is through, where a deployment asked for a read
     * bound of its own: that bound rather than the bound of the login, which is a value nothing
     * slower than a connect is meant to be measured against. Without one, this is the lift above -
     * the behaviour of every connection this backend established before the property existed.
     */
    @Test(timeOut = 120000)
    public void testAnEstablishedConnectionCarriesTheReadBoundAskedFor() throws Exception {
        final String url = StubDriver.PREFIX + "standing-read-bound";
        CachedConnection.readTimeoutMillis = 90000;
        final Connection parent = mock(Connection.class);
        stub.answerWith(parent);
        CachedConnection.connect(url, CachedConnection.ConnectDialect.MYSQL, 30,
            CachedConnection.poolOf(url), false);
        verify(parent).setNetworkTimeout(any(Executor.class), eq(90000));
        verify(parent, never()).setNetworkTimeout(any(Executor.class), eq(0));
    }
    /**
     * And it carries it whether or not the login had a bound of its own to lift. The read bound of
     * a login is only ever set where the connect is bounded, so a deployment that runs with
     * {@code connect.timeout=0} - the one setting that leaves a connect to the deadline of the
     * borrow alone - would otherwise set this property and get nothing for it.
     */
    @Test(timeOut = 120000)
    public void testTheReadBoundIsSetWhereTheLoginHadNoneToLift() throws Exception {
        final String url = StubDriver.PREFIX + "read-bound-without-a-login-bound";
        CachedConnection.readTimeoutMillis = 90000;
        final Connection parent = mock(Connection.class);
        stub.answerWith(parent);
        CachedConnection.connect(url, CachedConnection.ConnectDialect.MYSQL, 0,
            CachedConnection.poolOf(url), false);
        verify(parent).setNetworkTimeout(any(Executor.class), eq(90000));
    }
    /**
     * A read bound standing in the connection string is the deployment's own: the connect does not
     * replace it with this one, exactly as it does not set the read bound of a login on top of it.
     */
    @Test(timeOut = 120000)
    public void testAReadBoundOfTheUrlIsNotReplacedByTheConfiguredOne() throws Exception {
        final String url = StubDriver.PREFIX + "own-read-bound?socketTimeout=1000";
        CachedConnection.readTimeoutMillis = 90000;
        final Connection parent = mock(Connection.class);
        stub.answerWith(parent);
        CachedConnection.connect(url, CachedConnection.ConnectDialect.MYSQL, 30,
            CachedConnection.poolOf(url), false);
        verify(parent, never()).setNetworkTimeout(any(Executor.class), anyInt());
    }
    /**
     * Which connections carry a read bound of this backend's own making, as the backstop of
     * {@code JDBCStorage} has to know it: a statement of an unbounded class takes that bound off
     * for as long as it runs, and it may only take off what this class put on. A bound of the url
     * is the deployment's, and a driver whose property names are not known here was never given
     * one - lifting either would leave the connection unbounded for the rest of its life.
     */
    @Test(timeOut = 120000)
    public void testOnlyTheReadBoundThisClassSetsIsItsOwnToLift() {
        CachedConnection.readTimeoutMillis = 90000;
        assertEquals(CachedConnection.standingReadBoundMillis("jdbc:mysql://localhost:3306/db"), 90000,
            "the bound this class sets on a connection of a dialect it knows");
        assertEquals(CachedConnection.standingReadBoundMillis("jdbc:mysql://localhost:3306/db?socketTimeout=1000"), 0,
            "a read bound of the url was reported as this backend's own");
        assertEquals(CachedConnection.standingReadBoundMillis("jdbc:h2:mem:db"), 0,
            "a driver this class sets no read bound on was reported as bounded by it");
        CachedConnection.readTimeoutMillis = 0;
        assertEquals(CachedConnection.standingReadBoundMillis("jdbc:mysql://localhost:3306/db"), 0,
            "a connection carries no standing bound where none is configured");
    }
    /**
     * The bound is configured in seconds and reaches the driver in milliseconds; 0, a negative
     * value and a value that is no number all leave a connection unbounded, which is what this
     * backend did before the property existed. A value past the ceiling of a socket read timeout is
     * taken down to it rather than left to overflow the {@code int} of setNetworkTimeout, where it
     * would arrive as a negative timeout - a value outside the contract, and one a driver is free
     * to read as anything at all.
     */
    @Test(timeOut = 120000)
    public void testTheReadBoundIsConfiguredInSeconds() {
        assertEquals(CachedConnection.getReadTimeoutMillis(), 0, "a connection is unbounded by default");
        System.setProperty(CachedConnection.READ_TIMEOUT_PROPERTY, "90");
        assertEquals(CachedConnection.getReadTimeoutMillis(), 90000);
        System.setProperty(CachedConnection.READ_TIMEOUT_PROPERTY, "0");
        assertEquals(CachedConnection.getReadTimeoutMillis(), 0);
        System.setProperty(CachedConnection.READ_TIMEOUT_PROPERTY, "-1");
        assertEquals(CachedConnection.getReadTimeoutMillis(), 0, "a negative value was not read as no bound");
        System.setProperty(CachedConnection.READ_TIMEOUT_PROPERTY, "a minute and a half");
        assertEquals(CachedConnection.getReadTimeoutMillis(), 0,
            "a value that is no number was not ignored in favour of the default");
        System.setProperty(CachedConnection.READ_TIMEOUT_PROPERTY, Integer.toString(Integer.MAX_VALUE));
        assertEquals(CachedConnection.getReadTimeoutMillis(), JDBCStorage.MAX_BOUND_SECONDS * 1000,
            "a value past the ceiling of a socket read timeout was not taken down to it");
    }
    /**
     * A driver that will not take the standing bound leaves a connection with no bound of ours on
     * it, which is what every connection of this pool carried before the property existed and no
     * reason to keep this one out of the pool. The connection that must not be pooled is the one
     * still carrying the read bound of its login: there the call that failed was a call to take
     * something off, and the bound left on it fails every statement slower than a connect.
     */
    @Test(timeOut = 120000)
    public void testAConnectionThatWouldNotTakeTheStandingBoundIsStillPooled() throws Exception {
        final String url = StubDriver.PREFIX + "unsettable-standing-bound";
        CachedConnection.readTimeoutMillis = 90000;
        final Connection parent = mock(Connection.class);
        doThrow(new SQLException("setNetworkTimeout is not supported"))
            .when(parent).setNetworkTimeout(any(Executor.class), anyInt());
        stub.answerWith(parent);
        final CachedConnection.Pool pool = CachedConnection.poolOf(url);
        // Metered, and holding a permit of the pool as a borrow does: an unmetered connection is
        // closed rather than pooled whatever bound it carries, which would answer this on the
        // accounting of the pool instead of on the bound the case is about.
        assertTrue(pool.tryReserve(), "the pool of this url would not reserve a place for the connection");
        final CachedConnection borrowed = CachedConnection.connect(url, CachedConnection.ConnectDialect.MYSQL, 0,
            pool, true);
        borrowed.close();
        verify(parent, never()).close();
        assertEquals(pool.idleCount(), 1,
            "a connection carrying no bound of ours was kept out of the pool");
    }
    /**
     * A read parameter of the url set to 0 is no bound of the deployment's: 0 is what every one of
     * these drivers reads as "wait as long as it takes", which is the default this property exists
     * to replace. It tells the two bounds apart, and only on postgresql, where a parameter of the
     * url outranks the property this class supplies: the login there is left carrying no bound of
     * ours, and rightly so, while the bound of this property is no property of a connect at all - it
     * is a setNetworkTimeout of an established connection, which no url outranks. Read as a bound of
     * theirs, a "socketTimeout=0" - the default of pgjdbc, written out - would leave a deployment
     * that asked for this one with no bound and no report of why.
     */
    @Test(timeOut = 120000)
    public void testAReadParameterOfTheUrlSetToZeroIsNoBoundOfTheDeployments() {
        CachedConnection.readTimeoutMillis = 90000;
        assertEquals(CachedConnection.standingReadBoundMillis("jdbc:postgresql://localhost/db?socketTimeout=0"), 90000,
            "a postgresql url turning the read bound off was read as a bound of the deployment's own");
        assertEquals(CachedConnection.standingReadBoundMillis("jdbc:mysql://localhost:3306/db?socketTimeout=0"), 90000,
            "a mysql url turning the read bound off was read as a bound of the deployment's own");
        assertEquals(CachedConnection.standingReadBoundMillis("jdbc:postgresql://localhost/db?socketTimeout=30"), 0,
            "a read bound of a postgresql url is the deployment's own and stands");
    }
    /**
     * The predicate deciding whether a url bounds the read reads the same set of names as the one
     * deciding whether it declares it. It used to stop at the first name present even where the
     * value there was a zero, so a url naming the bound under both names of the oracle driver - the
     * dotted one turned off, the last segment set - was declared() and not bounds(): the login kept
     * the administrator's value, because a property of ours is not supplied over a declared one, and
     * a bound of ours then went on top of it with setNetworkTimeout. Contrived, but "the bound taken
     * off is the bound that was set" holds only while the two look at the same names.
     */
    @Test(timeOut = 120000)
    public void testAReadBoundUnderEitherNameOfTheUrlIsTheDeploymentsOwn() {
        CachedConnection.readTimeoutMillis = 90000;
        assertEquals(CachedConnection.standingReadBoundMillis(
            "jdbc:oracle:thin:@//localhost:1521/db?oracle.jdbc.ReadTimeout=0&ReadTimeout=600"), 0,
            "a bound standing under the last segment of the name was read as no bound at all");
        assertEquals(CachedConnection.standingReadBoundMillis(
            "jdbc:oracle:thin:@//localhost:1521/db?oracle.jdbc.ReadTimeout=0&ReadTimeout=0"), 90000,
            "a url turning the read bound off under both of its names is no bound of the deployment's");
    }
    /**
     * With nothing configured this is the lift and nothing else - the read bound of the login comes
     * off and no bound of ours goes on top of it, which is what every connection of this pool
     * carried before the property existed. The default of this property is what makes the change
     * that introduced it no change at all for a deployment that does not ask for one.
     */
    @Test(timeOut = 120000)
    public void testTheDefaultTakesTheBoundOfTheLoginOffAndPutsNothingOnTopOfIt() throws Exception {
        final String url = StubDriver.PREFIX + "default-read-bound";
        CachedConnection.readTimeoutMillis = 0;
        final Connection parent = mock(Connection.class);
        stub.answerWith(parent);
        CachedConnection.connect(url, CachedConnection.ConnectDialect.MYSQL, 30,
            CachedConnection.poolOf(url), false);
        verify(parent).setNetworkTimeout(any(Executor.class), eq(0));
        verify(parent, times(1)).setNetworkTimeout(any(Executor.class), anyInt());
    }
    /**
     * A driver that would take neither the standing bound nor the lift leaves the connection that
     * must not be pooled: what it is left carrying is the read bound of a connect, and every borrow
     * after this one would meet it - which is the case above, reached by the other of the two paths
     * that call for a setNetworkTimeout once the login is through.
     */
    @Test(timeOut = 120000)
    public void testAConnectionThatWouldTakeNeitherTheStandingBoundNorTheLiftIsNotPooled() throws Exception {
        final String url = StubDriver.PREFIX + "unsettable-over-a-login-bound";
        CachedConnection.readTimeoutMillis = 90000;
        final Connection parent = mock(Connection.class);
        doThrow(new SQLException("setNetworkTimeout is not supported"))
            .when(parent).setNetworkTimeout(any(Executor.class), anyInt());
        stub.answerWith(parent);
        final CachedConnection.Pool pool = CachedConnection.poolOf(url);
        // Metered, and holding a permit of the pool as a borrow does: an unmetered connection is
        // closed rather than pooled whatever bound it carries, which would answer this on the
        // accounting of the pool instead of on the bound the case is about.
        assertTrue(pool.tryReserve(), "the pool of this url would not reserve a place for the connection");
        final CachedConnection borrowed = CachedConnection.connect(url, CachedConnection.ConnectDialect.MYSQL, 30,
            pool, true);
        borrowed.close();
        verify(parent).close();
        assertEquals(pool.idleCount(), 0,
            "a connection still carrying the read bound of its login went back into the pool");
    }
    /**
     * The initializer reads this property the way it reads the two windows above - a value that is
     * no number, or a negative one, is reported once and ignored in favour of the default - and
     * reporting it has to leave the class usable: the set that report is deduplicated through is
     * declared above every field whose initializer can reach it (JLS 12.4.2), so a field of this
     * one moved above that set would turn a typo in a property into an ExceptionInInitializerError
     * that no test of a class already initialized would ever meet.
     */
    @Test(timeOut = 120000, dataProvider = "readBoundsWorthWarningAbout")
    public void testAReadBoundWorthWarningAboutStillInitializesTheClass(String configured) throws Exception {
        System.setProperty(CachedConnection.READ_TIMEOUT_PROPERTY, configured);
        final Class<?> reloaded = loadedAfresh(CachedConnection.class);
        assertNotSame(reloaded, CachedConnection.class, "the class under test was not loaded afresh");
        final Field field = reloaded.getDeclaredField("readTimeoutMillis");
        field.setAccessible(true);
        assertEquals(field.getInt(null), 0, "the bound the reloaded class settled on");
    }
    @DataProvider
    public Object[][] readBoundsWorthWarningAbout() {
        return new Object[][]{{"a minute and a half"}, {"-1"}};
    }
    /**
     * 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.
     */
opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/CatalogConnectionTestCase.java
New file
@@ -0,0 +1,658 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.backends.jdbc;
import org.forgerock.opendj.server.config.server.JDBCBackendCfg;
import org.opends.server.DirectoryServerTestCase;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.SQLTimeoutException;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;
import static org.forgerock.opendj.config.ConfigurationMock.mockCfg;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
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.assertTrue;
import static org.testng.Assert.fail;
/**
 * The connection the tree catalog of a backend is read and written on (#888): what it hands its
 * driver, and what it does with the connection it gets back. It is established outside the pool -
 * the caller of {@code openTree()} is holding a pooled connection already - so nothing the pool is
 * asserted on covers it, and a container suite covers it only where a container starts: it skips
 * itself whole otherwise, which leaves a bound nothing exercises.
 * <p>
 * No database is needed for any of it. The url is a postgresql one the pgjdbc driver cannot parse,
 * so {@code DriverManager} falls through to the probe of this class, while {@code
 * CachedConnection.ConnectDialect} still reads it as postgres - which is what makes the connect fill
 * in bounds at all, and what a url of an engine of nobody's would not.
 */
@SuppressWarnings("javadoc")
public class CatalogConnectionTestCase extends DirectoryServerTestCase {
    /**
     * What a caller with no replay above it hands the connect: the importer is the one such caller in
     * the product, and every case here that is not about the window itself asks the way it asks, so
     * that what it pins is the property and not a window of the test's own.
     */
    private static final long NO_REPLAY_WINDOW = Long.MAX_VALUE;
    private ProbeDriver probeDriver;
    @BeforeClass
    public void registerProbeDriver() throws SQLException {
        probeDriver = new ProbeDriver();
        DriverManager.registerDriver(probeDriver);
    }
    @AfterClass(alwaysRun = true)
    public void deregisterProbeDriver() throws SQLException {
        if (probeDriver != null) {
            DriverManager.deregisterDriver(probeDriver);
        }
    }
    /**
     * Nothing of one case reaches the next: the probe is a field of the class and a case that fails
     * before its finally would otherwise leave its stubbed connection, its refusals or the properties
     * of its last attempt to be read by whatever runs after it.
     */
    @BeforeMethod
    public void resetProbe() {
        probeDriver.lastProperties = null;
        probeDriver.answer = null;
        probeDriver.refusal = null;
        probeDriver.refusalsLeft.set(0);
        probeDriver.attempts.set(0);
        probeDriver.refusalDelayMs = 0;
        probeDriver.interruptOnAttempt = false;
    }
    private static JDBCStorage storageFor(String url) {
        final JDBCBackendCfg cfg = mockCfg(JDBCBackendCfg.class);
        when(cfg.getBackendId()).thenReturn("catalogProbe");
        when(cfg.getDBDirectory()).thenReturn(url);
        return new JDBCStorage(cfg, null);
    }
    /**
     * The bound of the connect is the one the operator configured for this backend's connects, and
     * not a literal of the code: a deployment whose login legitimately takes longer than the default
     * raises {@link CachedConnection#CONNECT_TIMEOUT_PROPERTY} for it, and a catalog connect bounded
     * tighter than that fails in 08001 - which is no conflict a write replays, so the backend stops
     * opening on an installation that opened before this connection existed.
     * <p>
     * Asked with no deadline over it, so that what the case pins is the configured bound alone: what
     * a deadline does to it is the case below.
     */
    @Test
    public void testTheCatalogConnectTakesTheConfiguredBound() throws Exception {
        final String previous = System.getProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        final String previousPool = System.getProperty(CachedConnection.POOL_TIMEOUT_PROPERTY);
        System.setProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY, "120");
        System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "0");
        try {
            probeDriver.lastProperties = null;
            storageFor(ProbeDriver.URL).newCatalogConnection(NO_REPLAY_WINDOW).close();
            assertBoundedAt(probeDriver.lastProperties, 120);
        } finally {
            restore(previous);
            restorePool(previousPool);
        }
    }
    /**
     * One attempt is never left to run past the deadline the retry of this connect is given, which is
     * the deadline of a borrow ({@link CachedConnection#POOL_TIMEOUT_PROPERTY}): an attempt bounded
     * looser than what is left of it would overrun it by a whole connect timeout. The pool bounds its
     * own attempts by exactly this rule, and a connect established the way a pooled one is takes it.
     */
    @Test
    public void testTheCatalogConnectIsNeverBoundedPastTheDeadlineOfItsRetry() throws Exception {
        final String previous = System.getProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        final String previousPool = System.getProperty(CachedConnection.POOL_TIMEOUT_PROPERTY);
        System.setProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY, "120");
        System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "20");
        try {
            storageFor(ProbeDriver.URL).newCatalogConnection(NO_REPLAY_WINDOW).close();
            // a range and not the 20 exactly: the bound is what is left of the deadline when the attempt
            // is made, so a pause of a second anywhere before it - a collection, the first touch of a
            // class on a loaded box - makes it 19, and the case is about the deadline and not the clock
            assertBoundedWithin(probeDriver.lastProperties, 15, 20);
        } finally {
            restore(previous);
            restorePool(previousPool);
        }
    }
    /**
     * A database taking no connection <em>for the moment</em> - at its connection limit, or still
     * recovering - is waited out rather than reported: one attempt loses a race the borrow beside it
     * wins, and this connect is on the critical path of the first read-write open of every backend.
     */
    @Test
    public void testTheCatalogConnectWaitsOutADatabaseTakingNoConnectionForTheMoment() throws Exception {
        final String previous = System.getProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        final String previousPool = System.getProperty(CachedConnection.POOL_TIMEOUT_PROPERTY);
        // both, since both are read on every connect: an ambient connect bound would change the
        // per-attempt bound these cases run under without changing anything they assert on
        System.clearProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "60");
        probeDriver.refusal = new SQLException("too many clients already", "53300");
        probeDriver.refusalsLeft.set(2);
        try {
            storageFor(ProbeDriver.URL).newCatalogConnection(NO_REPLAY_WINDOW).close();
            assertEquals(probeDriver.attempts.get(), 3,
                "a connect refused for the moment was not retried the way a borrow of the pool retries it");
        } finally {
            restore(previous);
            restorePool(previousPool);
        }
    }
    /**
     * And everything else is the caller's to see rather than waited out behind its back: a password
     * that is not accepted does not become a minute of silence and then the same failure.
     * <p>
     * A guard rather than a regression test, and worth saying so: the head before this connect had a
     * retry made one attempt and reported it, so it satisfies this case by having no loop at all.
     * What the case is here for is the loop that does exist staying this narrow - a predicate widened
     * to any refusal turns a wrong password into a minute of silence per open, and nothing else in
     * this suite would notice.
     */
    @Test
    public void testTheCatalogConnectDoesNotRetryAFailureThatWillNotClear() throws Exception {
        final String previous = System.getProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        final String previousPool = System.getProperty(CachedConnection.POOL_TIMEOUT_PROPERTY);
        // both, since both are read on every connect: an ambient connect bound would change the
        // per-attempt bound these cases run under without changing anything they assert on
        System.clearProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "60");
        probeDriver.refusal = new SQLException("password authentication failed", "28P01");
        probeDriver.refusalsLeft.set(Integer.MAX_VALUE);
        try {
            storageFor(ProbeDriver.URL).newCatalogConnection(NO_REPLAY_WINDOW);
            fail("a connect that will not clear was retried instead of being reported");
        } catch (SQLException expected) {
            assertEquals(expected.getSQLState(), "28P01", "the failure of the driver was not the one reported");
            assertEquals(probeDriver.attempts.get(), 1, "a failure that will not clear was attempted more than once");
        } finally {
            restore(previous);
            restorePool(previousPool);
        }
    }
    /**
     * The wait ends at the deadline of a borrow, as a timeout by type and carrying the state of the
     * driver's own last refusal: a state of this code's making would be read by {@code write()} as a
     * connection the database dropped, and the retry must change no classification.
     */
    @Test
    public void testTheCatalogConnectGivesUpAtTheDeadlineOfABorrow() throws Exception {
        final String previous = System.getProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        final String previousPool = System.getProperty(CachedConnection.POOL_TIMEOUT_PROPERTY);
        // both, since both are read on every connect: an ambient connect bound would change the
        // per-attempt bound these cases run under without changing anything they assert on
        System.clearProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "1");
        // a vendor code beside the state, since both are carried over: an oracle failure says what it
        // is in the ORA number and not in the SQLState, so a timeout dropping the code would answer 0
        // where the classifier reading it expects the driver's own
        probeDriver.refusal = new SQLException("the database system is starting up", "57P03", 3113);
        probeDriver.refusalsLeft.set(Integer.MAX_VALUE);
        try {
            storageFor(ProbeDriver.URL).newCatalogConnection(NO_REPLAY_WINDOW);
            fail("a connect refused for the whole deadline was not given up on");
        } catch (SQLTimeoutException expected) {
            // the state of the driver's own refusal and not one of this code's making: a manufactured
            // 08001 is read by write() as a connection the database dropped, which would replay an
            // attempt whose pooled connection is healthy and distrust the pool over it
            assertEquals(expected.getSQLState(), "57P03",
                "the failure the deadline ended carried another state than the driver's own");
            assertEquals(expected.getErrorCode(), 3113,
                "the failure the deadline ended dropped the vendor code of the driver's own");
            assertNotNull(expected.getCause(), "the driver's own failure was not carried as the cause");
            assertTrue(probeDriver.attempts.get() > 1,
                "the deadline was reached without the connect having been retried at all");
            // and it says which of the two bounds ran out, the property being the thing to raise only
            // where the property is what ended the wait
            assertTrue(expected.getMessage().contains(CachedConnection.POOL_TIMEOUT_PROPERTY + "=1s"),
                "the timeout did not name the bound that ended it: " + expected.getMessage());
        } finally {
            restore(previous);
            restorePool(previousPool);
        }
    }
    /**
     * The wait may not outlast the replay window of the {@code write()} it runs inside: this loop
     * runs within one attempt of that one, so a refusal waited out past the window reaches it with
     * the window already spent and is thrown unreplayed - the retry would cost the caller the replay
     * it had before there was a retry here at all. The shorter of the two bounds is the deadline of
     * the wait; the bound of one attempt is not taken from it, and this case pins that too.
     */
    @Test
    public void testTheCatalogConnectDoesNotOutlastTheReplayWindowOfItsCaller() throws Exception {
        final String previous = System.getProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        final String previousPool = System.getProperty(CachedConnection.POOL_TIMEOUT_PROPERTY);
        System.clearProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        // a minute, which is the default and six times the window of a write: the property is what
        // this connect would wait out if the window did not reach it
        System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "60");
        probeDriver.refusal = new SQLException("the database system is starting up", "57P03");
        probeDriver.refusalsLeft.set(Integer.MAX_VALUE);
        final long startedAt = System.currentTimeMillis();
        try {
            storageFor(ProbeDriver.URL).newCatalogConnection(startedAt + 300);
            fail("a connect refused past the replay window of its caller was not given up on");
        } catch (SQLTimeoutException expected) {
            final long waited = System.currentTimeMillis() - startedAt;
            assertTrue(waited < 30_000,
                "the connect waited " + waited + " ms, which is the pool timeout rather than the window above it");
            assertTrue(probeDriver.attempts.get() > 1,
                "the window was spent without the connect having been retried at all");
            assertEquals(expected.getSQLState(), "57P03",
                "the failure the window ended carried another state than the driver's own");
            // the line has to send an operator to the right knob: raising the pool timeout moves
            // nothing where the window of the write is the shorter bound
            assertTrue(expected.getMessage().contains("replay window"),
                "the timeout did not say which of the two bounds ended it: " + expected.getMessage());
            // and one attempt keeps the bound the operator configured for a login of this database:
            // the window decides how long it is worth retrying, not how long a login may take, and
            // an attempt cut to what is left of the window is the connect dying where the pooled one
            // beside it succeeds - the backend that stops opening
            assertBoundedAt(probeDriver.lastProperties, CachedConnection.DEFAULT_CONNECT_TIMEOUT_SECONDS);
        } finally {
            restore(previous);
            restorePool(previousPool);
        }
    }
    /**
     * A thread asked to stop is not answered by sleeping out the rest of a pool timeout: the flag is
     * put back - every frame above this one reads it to decide whether to unwind - and the driver's
     * own refusal is what the caller is told, the interrupt riding along with it so that a connect
     * cut short by a shutdown is not read off the log as a database refusing connections.
     */
    @Test
    public void testTheCatalogConnectReportsAnInterruptRatherThanSleepingPastIt() throws Exception {
        final String previous = System.getProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        final String previousPool = System.getProperty(CachedConnection.POOL_TIMEOUT_PROPERTY);
        System.clearProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "60");
        probeDriver.refusal = new SQLException("the database system is starting up", "57P03");
        probeDriver.refusalsLeft.set(Integer.MAX_VALUE);
        // raised inside the attempt rather than by another thread racing this one: the first backoff
        // is a millisecond, and a sleep entered with the flag already up throws at once
        probeDriver.interruptOnAttempt = true;
        try {
            storageFor(ProbeDriver.URL).newCatalogConnection(NO_REPLAY_WINDOW);
            fail("a connect interrupted while it waited was not reported at all");
        } catch (SQLException expected) {
            assertFalse(expected instanceof SQLTimeoutException,
                "an interrupt was reported as the deadline of the wait running out");
            assertEquals(expected.getSQLState(), "57P03",
                "the interrupt replaced the driver's own failure instead of riding along with it");
            assertEquals(probeDriver.attempts.get(), 1, "the wait went on past the interrupt");
            boolean carried = false;
            for (final Throwable suppressed : expected.getSuppressed()) {
                carried |= suppressed instanceof InterruptedException;
            }
            assertTrue(carried, "the interrupt was dropped rather than carried on the failure reported");
            assertTrue(Thread.currentThread().isInterrupted(),
                "the flag Thread.sleep() cleared was not put back, so nothing above can read it");
        } finally {
            Thread.interrupted(); // cleared here, or every case running after this one on this thread meets it
            probeDriver.interruptOnAttempt = false;
            restore(previous);
            restorePool(previousPool);
        }
    }
    /**
     * A connect of this backend that stalls says so in the log, and is throttled apart from the
     * borrows of the pool that address the same database: the two stall for the same reason, and a
     * borrow that reported a moment ago would otherwise silence the connect that is about to fail -
     * which is the one of the two an operator has no other line about.
     * <p>
     * The stall is a real one rather than a call of the formatter: the guard of the throttle passes
     * only where a connect has been retrying for a second, so nothing under it is reached by a case
     * whose refusals come back at once.
     */
    @Test
    public void testAStallOfTheCatalogConnectIsThrottledApartFromTheBorrowsOfThePool() throws Exception {
        final String previous = System.getProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        final String previousPool = System.getProperty(CachedConnection.POOL_TIMEOUT_PROPERTY);
        System.clearProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "60");
        // one refusal, slow enough that the connect has been retrying for longer than the guard of
        // the throttle, and then a connection: what is asserted is the warning, not the failure
        probeDriver.refusal = new SQLException("the database system is starting up", "57P03");
        probeDriver.refusalsLeft.set(1);
        probeDriver.refusalDelayMs = CachedConnection.STALL_WARNING_AFTER_MS + 100;
        try {
            // a url of its own: the throttle is keyed by url, and a case sharing one with another
            // would read that one's stamp instead of its own
            storageFor(ProbeDriver.STALL_URL).newCatalogConnection(NO_REPLAY_WINDOW).close();
            final long now = System.currentTimeMillis();
            final long longEnoughAgo = now - 2 * CachedConnection.STALL_WARNING_AFTER_MS;
            // the connect reported its stall: the moment is filed, so the next one inside the interval
            // is not due. Nothing else of this suite touches this url
            assertFalse(CachedConnection.stallWarningDue(ProbeDriver.STALL_URL, "|tree catalog", longEnoughAgo, now),
                "the connect stalled for longer than the guard and reported nothing");
            // and a borrow of the pool on that very url is still due one of its own, which is the half
            // of the throttle key that keeps the two waits from silencing each other
            assertTrue(CachedConnection.stallWarningDue(ProbeDriver.STALL_URL, "", longEnoughAgo, now),
                "a stall of this connect silenced the borrows of the pool addressing the same database");
        } finally {
            probeDriver.refusalDelayMs = 0;
            restore(previous);
            restorePool(previousPool);
        }
    }
    /**
     * Nothing configured is the default of the pool, which is what this connect used to take always.
     * The deadline is pinned rather than left to its own default: the attempt takes the shorter of
     * the two, so a pool timeout set anywhere - the surefire configuration, the environment, another
     * suite - would otherwise decide what this case asserts.
     */
    @Test
    public void testTheCatalogConnectTakesTheDefaultWhereNothingIsConfigured() throws Exception {
        final String previous = System.getProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        final String previousPool = System.getProperty(CachedConnection.POOL_TIMEOUT_PROPERTY);
        System.clearProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "0");
        try {
            probeDriver.lastProperties = null;
            storageFor(ProbeDriver.URL).newCatalogConnection(NO_REPLAY_WINDOW).close();
            assertBoundedAt(probeDriver.lastProperties, CachedConnection.DEFAULT_CONNECT_TIMEOUT_SECONDS);
        } finally {
            restore(previous);
            restorePool(previousPool);
        }
    }
    /**
     * A property of 0 is the operator asking for no bound at all - the pool reads it that way - and a
     * connect that bounded itself anyway would be answering a setting with the opposite of it. Nothing
     * is handed to the driver then, and there is no read bound to lift once the login is through.
     * <p>
     * Both properties, which is what the pool itself says leaves a connect unbounded: the deadline of
     * the retry bounds the attempt where there is one, so turning the per-attempt bound off alone
     * leaves the attempt bounded by what is left of that deadline - the case above.
     */
    @Test
    public void testTheCatalogConnectIsUnboundedWhereTheOperatorTurnedTheBoundOff() throws Exception {
        final String previous = System.getProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        final String previousPool = System.getProperty(CachedConnection.POOL_TIMEOUT_PROPERTY);
        System.setProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY, "0");
        System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "0");
        try {
            probeDriver.lastProperties = null;
            final Connection con = storageFor(ProbeDriver.URL).newCatalogConnection(NO_REPLAY_WINDOW);
            con.close();
            assertNotNull(probeDriver.lastProperties, "no properties were handed to the driver at all");
            assertTrue(probeDriver.lastProperties.isEmpty(),
                "a connect the operator asked for no bound on was bounded anyway: " + probeDriver.lastProperties);
            verify(con, never()).setNetworkTimeout(any(), anyInt());
        } finally {
            restore(previous);
            restorePool(previousPool);
        }
    }
    /**
     * What the connection is handed back for: rows of its own, committed where they are written. The
     * read bound of the login is lifted as soon as the login is through (#872) - it is a bound of the
     * connect and not of the statements of the catalog - and the isolation is the pool's, a repeatable
     * read gap-locking a catalog two transactions enrol into.
     */
    @Test
    public void testTheCatalogConnectionIsSetUpForItsRows() throws Exception {
        final String previous = System.getProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        final String previousPool = System.getProperty(CachedConnection.POOL_TIMEOUT_PROPERTY);
        System.clearProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        // the read bound is lifted only where the attempt was given one, and the attempt takes the
        // shorter of the two properties: a deadline of 0 elsewhere would leave nothing to lift here
        System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "0");
        try {
            final Connection con = storageFor(ProbeDriver.URL).newCatalogConnection(NO_REPLAY_WINDOW);
            con.close();
            verify(con).setAutoCommit(false);
            verify(con).setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
            verify(con).setNetworkTimeout(any(), eq(0));
        } finally {
            restore(previous);
            restorePool(previousPool);
        }
    }
    /**
     * A driver that will not take the read bound of the login back does not cost the backend its
     * catalog: the pool meets the same failure and hands the connection to the borrower waiting for
     * it, and a connect failing where the pooled one beside it succeeds is a backend that stops
     * opening on an installation which opened before this connection existed. Reported, and kept.
     * <p>
     * A guard rather than a regression test, like the one above: the head before this round already
     * caught that failure and returned the connection, so nothing of the round makes this case pass.
     * It is here because the answer was reached for twice - once as "fail the connect", which this
     * round took back out - and a third attempt at it would go unnoticed otherwise.
     */
    @Test
    public void testTheCatalogConnectionIsKeptWhereTheReadBoundWillNotComeOff() throws Exception {
        final String previous = System.getProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        final String previousPool = System.getProperty(CachedConnection.POOL_TIMEOUT_PROPERTY);
        System.clearProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "0");
        final Connection keeping = mock(Connection.class);
        doThrow(new SQLFeatureNotSupportedException("no network timeout here"))
            .when(keeping).setNetworkTimeout(any(), anyInt());
        probeDriver.answer = keeping;
        try {
            final Connection con = storageFor(ProbeDriver.URL).newCatalogConnection(NO_REPLAY_WINDOW);
            assertNotNull(con, "a connection whose read bound would not come off was not handed back");
            verify(con).setAutoCommit(false);
            verify(con, never()).close();
        } finally {
            restore(previous);
            restorePool(previousPool);
        }
    }
    /**
     * A connection whose set-up failed is held by nobody - the caller is answered with the failure -
     * so it is closed here or it leaks for the life of the process, one per open of a storage.
     */
    @Test
    public void testAConnectionWhoseSetUpFailsIsClosed() throws Exception {
        final String previous = System.getProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        final String previousPool = System.getProperty(CachedConnection.POOL_TIMEOUT_PROPERTY);
        // pinned like every other case of this class: both are read from the system properties on every
        // connect, so an ambient value would have this case exercise another path than the one it names
        System.clearProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "0");
        final Connection failing = mock(Connection.class);
        doThrow(new SQLException("no transaction here", "08006")).when(failing).setAutoCommit(false);
        probeDriver.answer = failing;
        try {
            storageFor(ProbeDriver.URL).newCatalogConnection(NO_REPLAY_WINDOW);
            fail("a connection this backend could not set up was handed to the catalog");
        } catch (SQLException expected) {
            // the failure of the set-up itself, reported to the caller rather than swallowed
        } finally {
            probeDriver.answer = null;
            restore(previous);
            restorePool(previousPool);
        }
        verify(failing).close();
    }
    /** What the dialect of this url declares, at the given number of seconds. */
    private static void assertBoundedAt(Properties handed, long seconds) {
        assertNotNull(handed, "no properties were handed to the driver at all");
        final CachedConnection.ConnectDialect dialect = CachedConnection.ConnectDialect.of(ProbeDriver.URL);
        assertNotNull(dialect, "the url of this test is read as an engine of nobody's, so it is bounded by nothing");
        for (final String property : dialect.connectProperties) {
            assertEquals(handed.getProperty(property), Long.toString(seconds * dialect.connectUnitsPerSecond),
                property + " did not reach the driver at the configured bound");
        }
        assertEquals(handed.getProperty(dialect.readProperties[0]),
            Long.toString(seconds * dialect.readUnitsPerSecond),
            dialect.readProperties[0] + " did not reach the driver at the configured bound");
    }
    /** The same where the value is what is left of a deadline, which no case may pin to the millisecond. */
    private static void assertBoundedWithin(Properties handed, long atLeastSeconds, long atMostSeconds) {
        assertNotNull(handed, "no properties were handed to the driver at all");
        final CachedConnection.ConnectDialect dialect = CachedConnection.ConnectDialect.of(ProbeDriver.URL);
        assertNotNull(dialect, "the url of this test is read as an engine of nobody's, so it is bounded by nothing");
        final String property = dialect.connectProperties[0];
        final String handedValue = handed.getProperty(property);
        assertNotNull(handedValue, property + " did not reach the driver at all");
        final long seconds = Long.parseLong(handedValue) / dialect.connectUnitsPerSecond;
        assertTrue(seconds >= atLeastSeconds && seconds <= atMostSeconds,
            property + " reached the driver at " + seconds + "s, outside the deadline it is taken from ("
                + atLeastSeconds + ".." + atMostSeconds + "s)");
    }
    private static void restore(String previous) {
        if (previous == null) {
            System.clearProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
        } else {
            System.setProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY, previous);
        }
    }
    private static void restorePool(String previous) {
        if (previous == null) {
            System.clearProperty(CachedConnection.POOL_TIMEOUT_PROPERTY);
        } else {
            System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, previous);
        }
    }
    /** Records the properties a catalog connection hands its driver, and connects to nothing. */
    private static final class ProbeDriver implements Driver {
        /**
         * A postgresql url with a port that is not a number: pgjdbc cannot parse it and answers the
         * DriverManager with null - or with a failure, which it records and walks past all the same -
         * so this probe is the driver that ends up answering, while ConnectDialect still reads the
         * prefix as postgres.
         */
        static final String URL = "jdbc:postgresql://catalog-probe:not-a-port/db";
        /**
         * The same for the one case about the stall warning, which reads the throttle this connect
         * files its report in: that throttle is keyed by url, so a case sharing one with any other
         * would be asserting on whichever of them ran first.
         */
        static final String STALL_URL = "jdbc:postgresql://catalog-probe-stall:not-a-port/db";
        volatile Properties lastProperties;
        /** The connection to answer with, for a test about what is done with it; a fresh mock otherwise. */
        volatile Connection answer;
        /** How many attempts to refuse before answering, and with what; for the cases about the retry. */
        final AtomicInteger refusalsLeft = new AtomicInteger();
        volatile SQLException refusal;
        /** How long a refused attempt takes, for the one case that needs a wait the throttle counts. */
        volatile long refusalDelayMs;
        /**
         * Whether an attempt raises the interrupt flag of the thread asking for it, so that the case
         * about an interrupted wait does not have to race a backoff of one millisecond from outside.
         */
        volatile boolean interruptOnAttempt;
        /** Every attempt this driver was asked to make, refused ones included. */
        final AtomicInteger attempts = new AtomicInteger();
        @Override
        public Connection connect(String url, Properties info) throws SQLException {
            if (!acceptsURL(url)) {
                return null; // not ours: DriverManager goes on to the next driver
            }
            attempts.incrementAndGet();
            lastProperties = info;
            if (refusal != null && refusalsLeft.getAndDecrement() > 0) {
                if (refusalDelayMs > 0) {
                    try {
                        Thread.sleep(refusalDelayMs);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
                if (interruptOnAttempt) {
                    Thread.currentThread().interrupt();
                }
                throw refusal;
            }
            return answer != null ? answer : mock(Connection.class);
        }
        @Override
        public boolean acceptsURL(String url) {
            return url != null && (url.startsWith(URL) || url.startsWith(STALL_URL));
        }
        @Override
        public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) {
            return new DriverPropertyInfo[0];
        }
        @Override
        public int getMajorVersion() {
            return 1;
        }
        @Override
        public int getMinorVersion() {
            return 0;
        }
        @Override
        public boolean jdbcCompliant() {
            return false;
        }
        @Override
        public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException {
            throw new SQLFeatureNotSupportedException();
        }
    }
}
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.
     */
opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/JDBCStorageRetryTest.java
@@ -41,6 +41,8 @@
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import java.util.logging.Logger;
import static org.forgerock.i18n.LocalizableMessage.raw;
@@ -49,6 +51,7 @@
import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
@@ -97,6 +100,13 @@
  private Connection engineConnection;
  /**
   * The connection the tree catalog of a storage of this test is written on, so that a test can assert what was
   * written there. Nothing else can: it is opened straight through the driver rather than borrowed from the pool,
   * and the rows it carries are the ones {@code statements} is asserted never to have carried.
   */
  private Connection catalogConnection;
  /**
   * Connections whose class names carry the engine the way the drivers' own do - pgjdbc's
   * {@code org.postgresql.jdbc.PgConnection}, Connector/J's {@code com.mysql.cj.jdbc.ConnectionImpl}. That name
   * is what {@code driverNameOf()} matches an engine on, and the name of a mock is derived from the type it
@@ -444,6 +454,45 @@
  }
  /**
   * The first read-write open of a backend upgraded from a version keeping no catalog creates the catalog and
   * writes one row per tree, and creates no table of its own: every tree it opens is already there. None of that
   * may commit anything of the caller's - {@code RootContainer.open()} opens every tree of every base DN in a
   * single write, and a commit anywhere inside it takes the whole open out of the replay for the life of that
   * attempt, so a deadlock at the twentieth tree would fail the backend start-up that master replayed. The rows
   * still have to be committed, since nothing else of this open would carry them: a connection of the catalog's
   * own is what makes the two compatible.
   */
  @Test
  public void testFillingTheCatalogOfAnUpgradedBackendLeavesTheAttemptReplayable() throws Exception
  {
    // every table of this backend is there except the one the catalog is kept in, which is the shape of an
    // installation whose tables predate the catalog
    final AtomicReference<String> catalogTable = new AtomicReference<>();
    final JDBCStorage storage = storageOverTablesThatAre(tableName -> !tableName.equals(catalogTable.get()));
    catalogTable.set(storage.getTableName(storage.getCatalogTree()));
    final AtomicInteger attempts = new AtomicInteger();
    storage.write(txn -> {
      txn.openTree(TREE, true);
      if (attempts.incrementAndGet() == 1)
      {
        throw new StorageRuntimeException(sql(0, "40001"));
      }
    });
    assertEquals(attempts.get(), 2, "the write that created and filled the catalog was not replayed");
    verify(statements, never()).executeUpdate();
    // and the catalog was filled, on the connection of its own: without this every assertion above holds of a
    // storage that enrolled nothing at all - an attempt issuing no statement is replayed the same way, and the
    // caller's connection is exactly as untouched. The row is the ANSI upsert of a plain mock, which tries an
    // update before an insert; the create is the table this fixture is missing
    verify(catalogConnection, atLeastOnce()).prepareStatement(startsWith("create table " + catalogTable.get()));
    verify(catalogConnection, atLeastOnce()).prepareStatement(startsWith("update " + catalogTable.get()));
    // committed where it is written, which is what keeps a row of an attempt that failed afterwards recorded
    verify(catalogConnection, atLeastOnce()).commit();
  }
  /**
   * A tree that had to be created did commit - the create table commits, and mysql and oracle commit before a DDL
   * statement of their own accord - so the attempt is out of the replay whatever the failure says: a
   * {@link WriteOperation} is only idempotent in the database, and {@code RootContainer.open()} replayed after the
@@ -745,24 +794,38 @@
  }
  /**
   * A storage whose pool hands out one connection of this test, over a catalog that either holds the table of
   * {@link #TREE} or does not. The connection is a mock of no recognized driver, which is how the engines that
   * guard their create index - and mssql, which has none - reach {@code openTree}.
   * A storage whose pool hands out one connection of this test, over a database that either holds the tables
   * this backend asks about or holds none of them - the table of {@link #TREE} and the table of the tree
   * catalog alike. The connection is a mock of no recognized driver, which is how the engines that guard their
   * create index - and mssql, which has none - reach {@code openTree}.
   */
  private JDBCStorage storageOverACatalogHolding(boolean theTable) throws Exception
  {
    return storageOverTablesThatAre(tableName -> theTable);
  }
  /**
   * The same, over a database holding exactly the tables the given rule accepts, and with a second connection
   * behind the pooled one: the tree catalog is written on a connection of its own, so that its rows commit
   * nothing of the caller's - which is the very thing {@code statements} is asserted on below.
   */
  private JDBCStorage storageOverTablesThatAre(Predicate<String> present) throws Exception
  {
    final Connection con = mock(Connection.class);
    final JDBCStorage storage = storageOver(con);
    final JDBCStorage storage = storageOver(con, catalogConnection());
    statements = mock(PreparedStatement.class);
    final String tableName = storage.getTableName(TREE);
    final DatabaseMetaData metaData = mock(DatabaseMetaData.class);
    // a result set of its own per call: the catalog is asked once per attempt, and a replayed attempt
    // reading a result set the previous one had already walked to its end would find no table there
    when(metaData.getTables(any(), any(), any(), any())).thenAnswer(invocation -> {
      // the name asked about and not the one table of a fixture: openTree() asks about the table of the tree
      // and about the table of the catalog, and answering the second with the name of the first would have
      // the catalog created over again on every attempt
      final String asked = (String) invocation.getArguments()[2];
      final ResultSet tables = mock(ResultSet.class);
      when(tables.next()).thenReturn(theTable, false);
      when(tables.getString("TABLE_NAME")).thenReturn(tableName);
      when(tables.next()).thenReturn(present.test(asked), false);
      when(tables.getString("TABLE_NAME")).thenReturn(asked);
      return tables;
    });
@@ -772,9 +835,37 @@
    return storage;
  }
  /** A connection of no rows at all, for a query this fixture has nothing to answer with. */
  private static ResultSet noRows() throws SQLException
  {
    final ResultSet rs = mock(ResultSet.class);
    when(rs.next()).thenReturn(false);
    return rs;
  }
  /**
   * A storage whose pool hands out one connection of the given engine, over a catalog holding the table of
   * {@link #TREE} and either holding its {@code k_} index or not. The index guard and the statement behind it
   * The connection the tree catalog of a storage of this test is written on: it opens one straight through the
   * driver, for the reason a stamp opens one of its own - the caller of openTree() is holding a pooled
   * connection already.
   */
  private Connection catalogConnection() throws Exception
  {
    final Connection con = mock(Connection.class);
    final PreparedStatement onIt = mock(PreparedStatement.class);
    final ResultSet empty = noRows(); // the read of what the catalog records: nothing was ever enrolled
    when(onIt.executeQuery()).thenReturn(empty);
    when(con.prepareStatement(anyString())).thenReturn(onIt);
    // the stamp of a tree name opens a connection of its own too, and a fixture that let it have this one
    // would have it issue the session statement of its dialect here
    when(con.createStatement()).thenThrow(new SQLException("no session statement in this test", "42000"));
    catalogConnection = con;
    return con;
  }
  /**
   * A storage whose pool hands out one connection of the given engine, over a database holding every table
   * this backend asks about - that of {@link #TREE} and that of its tree catalog - and either holding the
   * {@code k_} index of the first or not. The index guard and the statement behind it
   * are the branches {@code openTree()} takes per engine, and a mock of plain {@link Connection} reaches none
   * of them - so the name the mock ends up with is asserted here rather than assumed.
   */
@@ -787,11 +878,15 @@
        "a mock of " + engine.getSimpleName() + " reaches no " + engineName + " branch: "
            + JDBCStorage.driverNameOf(con));
    engineConnection = con;
    // the connections behind it answer the connects the pool does not make: the stamp of a tree name opens one
    // of its own, straight through the driver, since the caller of openTree() is holding a pooled connection
    final Connection[] answers = new Connection[behind.length + 1];
    // the connections behind it answer the connects the pool does not make: the tree catalog is read and
    // written on one of its own, straight through the driver, since the caller of openTree() is holding a
    // pooled connection already - and the stamp of a tree name opens one for the same reason. The catalog
    // comes first because openTree() opens the catalog before anything else and stamping its table is the
    // last thing that does, so a test naming a connection of its own names the one behind it
    final Connection[] answers = new Connection[behind.length + 2];
    answers[0] = con;
    System.arraycopy(behind, 0, answers, 1, behind.length);
    answers[1] = catalogConnection();
    System.arraycopy(behind, 0, answers, 2, behind.length);
    final JDBCStorage storage = storageOver(answers);
    statements = mock(PreparedStatement.class);
@@ -802,7 +897,8 @@
    when(metaData.getTables(any(), any(), any(), any())).thenAnswer(invocation -> {
      final ResultSet tables = mock(ResultSet.class);
      when(tables.next()).thenReturn(true, false);
      when(tables.getString("TABLE_NAME")).thenReturn(tableName);
      // the name asked about: openTree() asks about the table of the tree and about the table of the catalog
      when(tables.getString("TABLE_NAME")).thenReturn((String) invocation.getArguments()[2]);
      return tables;
    });
    when(metaData.getIndexInfo(any(), any(), any(), anyBoolean(), anyBoolean())).thenAnswer(invocation -> {
@@ -829,7 +925,11 @@
  private JDBCStorage storageOver(Connection... connections) throws Exception
  {
    final JDBCBackendCfg cfg = mock(JDBCBackendCfg.class);
    when(cfg.getDBDirectory()).thenReturn(StubDriver.PREFIX + pools.incrementAndGet());
    final int pool = pools.incrementAndGet();
    when(cfg.getDBDirectory()).thenReturn(StubDriver.PREFIX + pool);
    // the tree catalog of a backend is named after its id: a mock answering null for it would name every
    // storage of this class the same catalog, and the tables of these fixtures are named after that name
    when(cfg.getBackendId()).thenReturn("retry" + pool);
    final JDBCStorage storage = new JDBCStorage(cfg, null);
    storage.accessMode = AccessMode.READ_WRITE;
    stub.answerWith(connections);
opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/PgSqlTestCase.java
@@ -11,17 +11,41 @@
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2025 3A Systems, LLC.
 * Copyright 2025-2026 3A Systems, LLC.
 */
package org.opends.server.backends.jdbc;
import org.opends.server.backends.pluggable.spi.AccessMode;
import org.opends.server.backends.pluggable.spi.TreeName;
import org.opends.server.backends.pluggable.spi.WriteOperation;
import org.opends.server.backends.pluggable.spi.WriteableTransaction;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testng.annotations.Test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotEquals;
import static org.testng.Assert.assertTrue;
//docker run --rm -it -p 5432:5432 -e POSTGRES_PASSWORD=password --name postgres postgres
@Test
/**
 * The class-level annotation governs the cases declared here, and it has to carry
 * {@code sequential = true} of its own: {@code TestListener.enforceTestClassTypeAndAnnotations()}
 * looks it up on the class declaring the case rather than on the one running it, so the
 * {@code @Test(groups = ..., sequential = true)} of {@code PluggableBackendImplTestCase} answers for
 * the inherited cases alone and a bare {@code @Test} here fails every case this class declares.
 * {@code OracleTestCase} carries it for the same reason.
 */
@Test(sequential = true)
public class PgSqlTestCase extends TestCase {
    @Override
@@ -48,4 +72,120 @@
        return "jdbc:postgresql://localhost:"+ ((container==null)?"5432":container.getMappedPort(5432))+"/database_name?user=postgres&password=password";
    }
    /** The schema put ahead of the one this suite's tables are in, for the case below and for nothing else. */
    private static final String AHEAD_ON_THE_PATH = "opendj_ahead";
    /**
     * A table of this backend is found where an unqualified statement of the same connection reaches it, and
     * not only in the schema that connection happens to work in.
     * <p>
     * PostgreSQL resolves an unqualified reference across the whole {@code search_path} while an unqualified
     * {@code create} lands in {@code current_schema()} alone, so the two are the same schema only as long as
     * nothing was put in front of the one the tables were made in. Adding a schema of its own to a role is the
     * standard remedy since PG15 took {@code CREATE} off {@code public}, and it makes them differ on an
     * installation whose tables are already there: the backend goes on reading and writing them unqualified,
     * and a lookup asking only about {@code current_schema()} would report every one of them absent. What that
     * would cost is this issue over again - the clear would drop nothing and say nothing, which is #888 - and
     * one thing worse besides: the next open would create a second, empty set of tables in the schema ahead,
     * and from that commit on they would shadow the populated ones for every later unqualified reference.
     * <p>
     * The connection string carries the path rather than a role being altered, because the pools of this
     * backend are keyed by it: a storage of another url is a storage of connections of its own, where an
     * {@code ALTER ROLE} would leave every connection already pooled resolving the way it always did.
     */
    @Test
    public void testAClearFindsATableOfAnotherSchemaOfTheSearchPath() throws Exception {
        final TreeName tree = new TreeName("testSearchPath", "tree");
        final String backendId = getBackendId() + "_searchPath";
        // the tables of an installation made before anything was put in front of the schema they are in
        final JDBCStorage created = new JDBCStorage(createBackendCfg(backendId), null);
        final String tableName = created.getTableName(tree);
        // the same backend, over connections resolving in a schema of its own first and in the one the
        // tables are in behind it: what they reach unqualified is unchanged, what they create is not
        final String aheadOfThem = getJdbcUrl() + "&currentSchema=" + AHEAD_ON_THE_PATH + ",public";
        final JDBCStorage storage = new JDBCStorage(createBackendCfg(backendId, aheadOfThem), null);
        try {
            try {
                created.open(AccessMode.READ_WRITE);
                created.write(new WriteOperation() {
                    @Override
                    public void run(WriteableTransaction txn) throws Exception {
                        txn.openTree(tree, true);
                    }
                });
            } finally {
                created.close();
            }
            assertTrue(isExistsTable(tableName), "the case did not make the table it is about");
            try (final Connection con = DriverManager.getConnection(getJdbcUrl());
                 final Statement st = con.createStatement()) {
                st.execute("create schema if not exists " + AHEAD_ON_THE_PATH);
            }
            storage.open(AccessMode.READ_WRITE);
            try (final Connection con = DriverManager.getConnection(aheadOfThem)) {
                // the fixture is the whole of the case: without this the two schemas are the same one and
                // the assertions below hold of the version this case is about as well
                assertEquals(con.getSchema(), AHEAD_ON_THE_PATH,
                    "the connections of this storage do not work in the schema put ahead of the tables");
                assertNotEquals(con.getSchema(), "public", "the tables of this case are not in public after all");
            }
            assertTrue(storage.listTrees().contains(tree),
                "a tree whose table this connection reads unqualified was named by none of them");
            // the other half of what the narrowing decides, and the destructive one: openTree() creates a
            // table where its lookup answers that there is none, and an unqualified "create table" lands in
            // current_schema() - the schema ahead of the tables. A lookup asking about that schema alone
            // would answer no here and leave the populated table in public orphaned behind a second, empty
            // one, from this commit on. The clear below drops what the catalog names and would go on
            // passing while it happened, which is why this is asserted here rather than left to it
            storage.write(new WriteOperation() {
                @Override
                public void run(WriteableTransaction txn) throws Exception {
                    txn.openTree(tree, true);
                }
            });
            assertFalse(isExistsTableInSchema(AHEAD_ON_THE_PATH, tableName),
                "the open created a second table in the schema ahead of the tables, shadowing the populated one");
            assertTrue(isExistsTableInSchema("public", tableName),
                "the open did not leave the populated table where it is");
            storage.removeStorageFiles();
            assertFalse(isExistsTable(tableName),
                "the clear left a table it reaches unqualified standing, for living in another schema of the search path");
        } finally {
            // the same backend id, so this clears what either half of the case created - including the
            // run where the clear under test drops nothing and the tables would otherwise be left for
            // whatever case of this class runs next
            clearQuietly(storage);
            clearQuietly(new JDBCStorage(createBackendCfg(backendId), null));
            try (final Connection con = DriverManager.getConnection(getJdbcUrl());
                 final Statement st = con.createStatement()) {
                st.execute("drop schema if exists " + AHEAD_ON_THE_PATH + " cascade");
            }
        }
    }
    /**
     * Whether the table is in that one schema, which is the question the case above asks and the one
     * {@code TestCase.isExistsTable} cannot answer: it walks every schema the connection can see, so a
     * table created in the wrong one of the two reads there exactly like a table created in the right
     * one. Asked of {@code information_schema} with the schema and the name bound rather than through
     * {@code getTables()}, whose schema is a pattern - {@code opendj_ahead} would match a schema named
     * {@code opendjXahead} as readily, {@code _} being a single-character wildcard there.
     */
    private boolean isExistsTableInSchema(String schema, String tableName) throws SQLException {
        try (final Connection con = DriverManager.getConnection(getJdbcUrl());
             final PreparedStatement st = con.prepareStatement(
                 "select 1 from information_schema.tables where table_schema=? and lower(table_name)=lower(?)")) {
            st.setString(1, schema);
            st.setString(2, tableName);
            try (final ResultSet rs = st.executeQuery()) {
                return rs.next();
            }
        }
    }
}
opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/TestCase.java
@@ -15,8 +15,10 @@
 */
package org.opends.server.backends.jdbc;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ByteStringBuilder;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.server.config.server.JDBCBackendCfg;
import org.opends.server.backends.pluggable.PluggableBackendImplTestCase;
import org.opends.server.backends.pluggable.spi.AccessMode;
@@ -41,12 +43,16 @@
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -121,12 +127,68 @@
    @Override
    protected JDBCBackendCfg createBackendCfg() {
        return createBackendCfg(getBackendId());
    }
    /**
     * A configuration of another backend on the database of this suite: backends sharing one database
     * URL is a configuration nothing forbids, and what one of them clears must be its own tables.
     */
    protected JDBCBackendCfg createBackendCfg(String backendId) {
        JDBCBackendCfg backendCfg = mockCfg(JDBCBackendCfg.class);
        when(backendCfg.getBackendId()).thenReturn(getBackendId());
        when(backendCfg.getBackendId()).thenReturn(backendId);
        when(backendCfg.getDBDirectory()).thenReturn(getJdbcUrl());
        return backendCfg;
    }
    /**
     * The same, reached over a connection string of the caller's own: the pools of this backend are
     * keyed by it, so a case wanting connections established differently - in another schema of the
     * search path, say - asks for them by asking for another url.
     */
    protected JDBCBackendCfg createBackendCfg(String backendId, String jdbcUrl) {
        final JDBCBackendCfg backendCfg = createBackendCfg(backendId);
        when(backendCfg.getDBDirectory()).thenReturn(jdbcUrl);
        return backendCfg;
    }
    /**
     * The same, serving the given base DN: what a clear compares the tree stamp of a table against
     * when it says whether the table is this backend's own or another's (#866).
     */
    protected JDBCBackendCfg createBackendCfg(String backendId, DN baseDN) {
        final JDBCBackendCfg backendCfg = createBackendCfg(backendId);
        final TreeSet<DN> baseDNs = new TreeSet<>();
        baseDNs.add(baseDN);
        when(backendCfg.getBaseDN()).thenReturn(baseDNs);
        return backendCfg;
    }
    /** Asked of the database itself, by listing its tables, so that no folding rule of the backend is trusted here. */
    protected boolean isExistsTable(String tableName) throws SQLException {
        try (final Connection con = DriverManager.getConnection(getJdbcUrl())) {
            return isExistingTable(con, tableName);
        }
    }
    /** Drops a table behind the back of the storage that owns it, which no code path of the backend does. */
    private void dropTableBehindTheBackend(String tableName) throws SQLException {
        try (final Connection con = DriverManager.getConnection(getJdbcUrl());
             final Statement st = con.createStatement()) {
            st.execute("drop table " + tableName);
        }
    }
    /** Clears a backend of a test without letting the failure of the clear replace the failure being reported. */
    protected static void clearQuietly(JDBCStorage storage) {
        try {
            storage.removeStorageFiles();
        } catch (Exception ignored) {
        } finally {
            storage.close();
        }
    }
    @AfterClass
    @Override
    public void cleanUp() throws Exception {
@@ -178,6 +240,33 @@
        }
    }
    /**
     * And the bound that replaces it reaches the socket of the driver: set with setNetworkTimeout
     * once the login is through, it is read back off the connection the pool hands out (#885). The
     * unit tests pin which value is set, on a mock that can only answer that it was asked; this is
     * the driver of a real engine answering that it took it.
     */
    @Test(timeOut = 120000)
    public void testTheStandingReadBoundReachesTheSocket() throws Exception {
        final String url = createBackendCfg().getDBDirectory();
        final int configured = CachedConnection.readTimeoutMillis;
        CachedConnection.readTimeoutMillis = 5000;
        try {
            assertEquals(CachedConnection.standingReadBoundMillis(url), 5000,
                "the url of this container carries a read bound of its own, so no bound of ours is set on it");
            // a pooled connection would be handed back without being established again
            CachedConnection.poolOf(url).drainIdle();
            try (final Connection con = CachedConnection.getConnection(url)) {
                assertEquals(con.getNetworkTimeout(), 5000,
                    "the read bound of this backend did not reach the socket of this driver");
            }
        } finally {
            CachedConnection.readTimeoutMillis = configured;
            // and nothing carrying the bound of this test goes back to the pool the suite goes on using
            CachedConnection.poolOf(url).drainIdle();
        }
    }
    private static ByteString key(int i) {
        return ByteString.valueOfUtf8(String.format("key%02d", i));
    }
@@ -314,11 +403,16 @@
    }
    /**
     * Reading a tree must not enrol it in the storage's tree map: removeStorageFiles() drops every
     * table that map names, and the compressed schema reads the tree its definitions used to be
     * shared under - which on a shared database is another backend's to keep (#873). Asking whether
     * the tree is there is only the first of those reads: the migration counts it and copies it out
     * too, so one guarded statement would not be enough.
     * Reading a tree must not put it up for removal: a clear drops what the catalog of the backend
     * names (#888), and the compressed schema reads the tree its definitions used to be shared under
     * - which on a shared database is another backend's to keep (#873). Asking whether the tree is
     * there is only the first of those reads: the migration counts it and copies it out too, so one
     * guarded statement would not be enough.
     * <p>
     * The two storages are two backends and not one addressing the same database, which is what the
     * case is about: what a backend owns is recorded in a catalog named after its backend id and
     * outlives the process that opened the tree, so a second storage of the same id would be shown
     * the tree its own earlier open had enrolled - and would be right to be.
     */
    @Test
    public void testProbingATreeDoesNotPutItUpForRemoval() throws Exception {
@@ -334,9 +428,9 @@
        });
        owner.close();
        // a second storage on the same database, which never opened that tree - the shape of two
        // a second backend on the same database, which never opened that tree - the shape of two
        // backends addressing one database
        final JDBCStorage other = new JDBCStorage(createBackendCfg(), null);
        final JDBCStorage other = new JDBCStorage(createBackendCfg(getBackendId() + "_probe"), null);
        try {
            other.open(AccessMode.READ_WRITE);
            other.read(new ReadOperation<Void>() {
@@ -399,7 +493,11 @@
        });
        owner.close();
        // a storage that never opened that tree, so nothing but the delete can enrol it
        // a second storage of the same backend, which never opened that tree itself: the delete takes
        // the enrolling name and the table it writes to is the one the tree names. What puts a tree up
        // for removal is the row its backend's catalog holds (#888) - written by the openTree above and
        // outliving the storage that made it - so this asserts the listing of a backend and not a
        // side effect of the statement, which is what a listing of a catalog can assert
        final JDBCStorage other = new JDBCStorage(createBackendCfg(), null);
        try {
            other.open(AccessMode.READ_WRITE);
@@ -1109,7 +1207,10 @@
            storage.write(new WriteOperation() {
                @Override
                public void run(WriteableTransaction txn) throws Exception {
                    txn.put(written, key(1), value(1)); // pending in this transaction...
                    // pending in this transaction, and it has to still be pending when the stamp is
                    // attempted: both trees were enrolled by the storage above, so the openTree below
                    // records nothing in the catalog and commits nothing of what is written here
                    txn.put(written, key(1), value(1));
                    txn.openTree(stamped, true); // ...while the comment machinery fails
                }
            });
@@ -1120,9 +1221,12 @@
                    return null;
                }
            });
            // the failure is remembered: an unstampable table is not asked again while this backend is open
            // the failure is remembered: an unstampable table is not asked again while this backend is open.
            // Counted from what the open itself attempted rather than from one: the open stamps the tree and
            // the catalog of the backend, and how many tables an open has to stamp is not what this is about
            final int attemptsOfTheOpen = stampAttempts.get();
            assertEquals(storage.commentTable(stamped, dialect()), JDBCStorage.CommentResult.FAILED);
            assertEquals(stampAttempts.get(), 1, "a failed stamp was reissued");
            assertEquals(stampAttempts.get(), attemptsOfTheOpen, "a failed stamp was reissued");
        } finally {
            try {
                storage.write(new WriteOperation() {
@@ -1832,4 +1936,930 @@
            storage.close();
        }
    }
    /**
     * removeStorageFiles() has to clear a backend this process has never opened: offline import-ldif
     * configures the backend and calls it before anything opens the root container, so answering from
     * the trees this process happens to have touched dropped nothing at all - an offline
     * "import-ldif --clearBackend" cleared a JDBC backend of nothing (#888).
     */
    @Test
    public void testABackendIsClearedByAProcessThatNeverOpenedIt() throws Exception {
        final TreeName tree = new TreeName("testOfflineClear", "tree");
        // the neighbour serves a base DN of its own, so that its table is one it reports as its own:
        // what this case asserts of the clear next door is then an absence and not a vacuity
        final DN neighbourBaseDN = DN.valueOf("dc=offline-clear-neighbour,dc=com");
        final TreeName neighbourTree = new TreeName(neighbourBaseDN.toNormalizedUrlSafeString(), "tree");
        final JDBCStorage storage = new JDBCStorage(createBackendCfg(getBackendId() + "_cleared"), null);
        final JDBCStorage neighbour =
            new JDBCStorage(createBackendCfg(getBackendId() + "_neighbour", neighbourBaseDN), null);
        try {
            storage.open(AccessMode.READ_WRITE);
            storage.write(new WriteOperation() {
                @Override
                public void run(WriteableTransaction txn) throws Exception {
                    txn.openTree(tree, true);
                    txn.put(tree, key(1), value(1));
                }
            });
            neighbour.open(AccessMode.READ_WRITE);
            neighbour.write(new WriteOperation() {
                @Override
                public void run(WriteableTransaction txn) throws Exception {
                    txn.openTree(neighbourTree, true);
                    txn.put(neighbourTree, key(1), value(1));
                }
            });
        } catch (Exception e) {
            // the clears of the case below are reached by no failure of this half, and nothing but
            // @BeforeClass ever drops what it leaves behind
            clearQuietly(storage);
            clearQuietly(neighbour);
            throw e;
        } finally {
            storage.close();
            neighbour.close();
        }
        // configured and never opened, nothing touched: what BackendImpl.importLDIF holds offline
        final JDBCStorage offline = new JDBCStorage(createBackendCfg(getBackendId() + "_cleared"), null);
        try {
            assertTrue(offline.listTrees().contains(tree),
                "the tree of a backend this process never opened has to be named by its catalog");
            offline.removeStorageFiles();
            assertFalse(isExistsTable(offline.getTableName(tree)), "the table of the tree survived the clear");
            assertFalse(isExistsTable(offline.getTableName(offline.getCatalogTree())), "the catalog survived the clear");
            final Set<TreeName> cleared = offline.listTrees();
            assertFalse(cleared.contains(tree), "a cleared backend still names its tree");
            assertFalse(cleared.contains(offline.getCatalogTree()), "a cleared backend still names its catalog");
            // the neighbour is named by a catalog of its own: what one backend clears is never another's
            assertTrue(isExistsTable(neighbour.getTableName(neighbourTree)),
                "the clear of one backend dropped the table of another backend of the same database");
            // nor does it report another backend's tables as tables of its own: a table is named after
            // the hash of its tree name and says nothing about whose it is, but it is stamped with that
            // tree name (#866), and the neighbour's trees are trees of no base DN this backend serves
            assertReportsNothingOf(offline, neighbour, neighbourTree);
        } finally {
            // in a finally of their own: a failed assertion above must not leave the tables of either
            // backend behind for the rest of the class, which nothing but @BeforeClass ever drops
            clearQuietly(neighbour);
            clearQuietly(offline);
        }
    }
    /**
     * A dropped tree has to leave the catalog together with its table: a row outliving its table
     * would make backendstat name a tree that is not there, and would put a table that is already
     * gone up for removal (#888).
     */
    @Test
    public void testADeletedTreeIsNoLongerNamedByTheCatalog() throws Exception {
        final TreeName kept = new TreeName("testCatalogDelete", "kept");
        final TreeName dropped = new TreeName("testCatalogDelete", "dropped");
        final JDBCStorage storage = new JDBCStorage(createBackendCfg(getBackendId() + "_deleted"), null);
        try {
            storage.open(AccessMode.READ_WRITE);
            storage.write(new WriteOperation() {
                @Override
                public void run(WriteableTransaction txn) throws Exception {
                    txn.openTree(kept, true);
                    txn.openTree(dropped, true);
                }
            });
            final Set<TreeName> opened = storage.listTrees();
            assertTrue(opened.contains(kept) && opened.contains(dropped), "an opened tree is not named by the catalog");
            storage.write(new WriteOperation() {
                @Override
                public void run(WriteableTransaction txn) throws Exception {
                    txn.deleteTree(dropped);
                }
            });
            final Set<TreeName> remaining = storage.listTrees();
            assertTrue(remaining.contains(kept), "the catalog forgot a tree that is still there");
            assertFalse(remaining.contains(dropped), "the catalog still names a tree that was deleted");
            // and the removal that follows must not stumble over the tree it no longer names
            storage.removeStorageFiles();
            assertFalse(isExistsTable(storage.getTableName(kept)), "the table of the tree survived the clear");
        } finally {
            clearQuietly(storage);
        }
    }
    /**
     * A row of the catalog whose table is not there any more must not fail the clear, and must not
     * stop it dropping the rest. Nothing of the backend leaves such a row behind - deleteTree() takes
     * it out in the commit that drops the table - but a table dropped by hand, or a catalog restored
     * from a backup older than the database, leaves exactly this (#888).
     */
    @Test
    public void testAClearSkipsACatalogRowWhoseTableIsGone() throws Exception {
        final TreeName kept = new TreeName("testStaleCatalogRow", "kept");
        final TreeName vanished = new TreeName("testStaleCatalogRow", "vanished");
        final JDBCStorage storage = new JDBCStorage(createBackendCfg(getBackendId() + "_stale"), null);
        try {
            storage.open(AccessMode.READ_WRITE);
            storage.write(new WriteOperation() {
                @Override
                public void run(WriteableTransaction txn) throws Exception {
                    txn.openTree(kept, true);
                    txn.openTree(vanished, true);
                }
            });
            dropTableBehindTheBackend(storage.getTableName(vanished));
            assertTrue(storage.listTrees().contains(vanished),
                "the catalog was expected to go on naming the tree whose table was dropped behind its back");
            storage.removeStorageFiles();
            assertFalse(isExistsTable(storage.getTableName(kept)),
                "a row of the catalog whose table is gone stopped the clear dropping the rest");
            assertFalse(isExistsTable(storage.getTableName(storage.getCatalogTree())), "the catalog survived the clear");
        } finally {
            clearQuietly(storage);
        }
    }
    /**
     * Naming a tree in order to read it must never put it up for removal: the tree read may be held
     * by another backend of the same database, which nothing forbids (#873). Only
     * openTree(createOnDemand) enrols.
     */
    @Test
    public void testReadingATreeDoesNotPutItUpForRemoval() throws Exception {
        final TreeName owned = new TreeName("testReadDoesNotEnrol", "owned");
        final TreeName foreign = new TreeName("testReadDoesNotEnrolForeign", "tree");
        final JDBCStorage storage = new JDBCStorage(createBackendCfg(getBackendId() + "_reader"), null);
        final JDBCStorage owner = new JDBCStorage(createBackendCfg(getBackendId() + "_owner"), null);
        try {
            owner.open(AccessMode.READ_WRITE);
            owner.write(new WriteOperation() {
                @Override
                public void run(WriteableTransaction txn) throws Exception {
                    txn.openTree(foreign, true);
                    txn.put(foreign, key(1), value(1));
                }
            });
            storage.open(AccessMode.READ_WRITE);
            storage.write(new WriteOperation() {
                @Override
                public void run(WriteableTransaction txn) throws Exception {
                    txn.openTree(owned, true); // the catalog of this backend comes into being here
                    txn.openTree(foreign, false); // read, not owned
                }
            });
            assertEquals(storage.read(new ReadOperation<ByteString>() {
                @Override
                public ByteString run(ReadableTransaction txn) throws Exception {
                    return txn.read(foreign, key(1));
                }
            }), value(1), "the tree of the other backend could not be read");
            assertFalse(storage.listTrees().contains(foreign), "reading a tree enrolled it in the catalog");
            storage.removeStorageFiles();
            assertTrue(isExistsTable(owner.getTableName(foreign)),
                "the clear dropped a tree this backend had only read");
            assertFalse(isExistsTable(storage.getTableName(owned)), "the table of the backend's own tree survived the clear");
        } finally {
            clearQuietly(owner);
            clearQuietly(storage);
        }
    }
    /**
     * The compressed schema trees named from a literal carry no backend qualifier, so on a database
     * addressed by several backends they are the same pair for all of them: a clear must leave them
     * where they lie (#881). A tool asking a backend what trees it holds has to be shown them all the
     * same, which is what keeps them out of the catalog and inside listTrees().
     */
    @Test
    public void testTheSharedCompressedSchemaTreesAreNamedButNeverCleared() throws Exception {
        final TreeName owned = new TreeName("testSharedCompressedSchema", "owned");
        final JDBCStorage storage = new JDBCStorage(createBackendCfg(getBackendId() + "_schema"), null);
        try {
            storage.open(AccessMode.READ_WRITE);
            storage.write(new WriteOperation() {
                @Override
                public void run(WriteableTransaction txn) throws Exception {
                    txn.openTree(owned, true);
                    // opened, never written to: since #881 no backend of this class makes the literal-named
                    // pair, so this openTree is what creates these two tables - and the finally below is what
                    // removes them again, a clear being required to leave them exactly where they lie
                    for (final TreeName shared : JDBCStorage.SHARED_COMPRESSED_SCHEMA_TREES) {
                        txn.openTree(shared, true);
                    }
                }
            });
            // both of them: the pair is a hand-copy of two privates of PersistentCompressedSchema, and
            // a literal naming a tree that does not exist would go unseen if one of them were never asked
            // for - the tree it names would be neither shown by listTrees() nor spared by a clear
            final Set<TreeName> named = storage.listTrees();
            for (final TreeName shared : JDBCStorage.SHARED_COMPRESSED_SCHEMA_TREES) {
                assertTrue(named.contains(shared),
                    "a tool asking this backend for its trees was not shown " + shared);
            }
            storage.removeStorageFiles();
            for (final TreeName shared : JDBCStorage.SHARED_COMPRESSED_SCHEMA_TREES) {
                assertTrue(isExistsTable(JDBCStorage.toTableName(shared)),
                    "the clear dropped " + shared + ", which another backend of this database may be the only owner of");
            }
            assertFalse(isExistsTable(storage.getTableName(owned)), "the table of the backend's own tree survived the clear");
        } finally {
            // the pair is dropped by hand here, and by nothing of the backend: a clear must leave it
            // where it lies, which is the whole of what this case asserts. It is this case's to remove
            // because it is this case that made it - since #881 each backend keeps its definitions in a
            // pair of its own, so the literal-named pair belongs to no backend of this class any more
            // and the openTree above is what created these two tables. Left standing they would be a
            // legacy pair this database does not have, which
            // testCompressedSchemaTableIsQualifiedByBackendId asserts about and TestNG may run after
            // this case as easily as before it
            clearQuietly(storage);
            for (final TreeName shared : JDBCStorage.SHARED_COMPRESSED_SCHEMA_TREES) {
                try {
                    dropTableBehindTheBackend(JDBCStorage.toTableName(shared));
                } catch (SQLException ignored) { // a case that failed before it made them leaves none to drop
                }
            }
        }
    }
    /**
     * The row of a deleted tree must not be left to the enclosing transaction: a terminal failure
     * later in it - write() replays a class 40 conflict and rethrows everything else - would roll the
     * row back over a table that is already gone, and nothing would put it right, a deleted tree not
     * being opened again (#888).
     */
    @Test
    public void testADeletedTreeStaysOutOfTheCatalogWhenItsTransactionFails() throws Exception {
        final TreeName kept = new TreeName("testCatalogDeleteRollback", "kept");
        final TreeName deleted = new TreeName("testCatalogDeleteRollback", "deleted");
        final JDBCStorage storage = new JDBCStorage(createBackendCfg(getBackendId() + "_rollback"), null);
        try {
            storage.open(AccessMode.READ_WRITE);
            storage.write(new WriteOperation() {
                @Override
                public void run(WriteableTransaction txn) throws Exception {
                    txn.openTree(kept, true);
                    txn.openTree(deleted, true);
                }
            });
            try {
                storage.write(new WriteOperation() {
                    @Override
                    public void run(WriteableTransaction txn) throws Exception {
                        txn.deleteTree(deleted);
                        // terminal, and no conflict for write() to replay: everything this transaction
                        // still owes goes back, and the row of the deleted tree must not be part of it
                        throw new IllegalStateException("the transaction of a deleteTree failed");
                    }
                });
                fail("the write was expected to fail");
            } catch (Exception expected) {
                // what the case is about is what the failure left behind
            }
            assertFalse(isExistsTable(storage.getTableName(deleted)), "the failed transaction brought a dropped table back");
            final Set<TreeName> remaining = storage.listTrees();
            assertFalse(remaining.contains(deleted),
                "the catalog names a tree whose table the failed transaction left dropped");
            assertTrue(remaining.contains(kept), "the catalog forgot a tree that is still there");
        } finally {
            clearQuietly(storage);
        }
    }
    /**
     * The same, for the branch a deleteTree takes when the table is not there any more: nothing is
     * dropped, so there is no commit of a drop for the row to be carried out of the catalog by, and
     * the commit the delete is given on the catalog's own connection is the whole of what takes it
     * out. Left to the enclosing transaction, the row would go back with it and the catalog would name
     * a tree with no table for good - the state a clear can only skip and report, never repair (#888).
     */
    @Test
    public void testADeletedTreeStaysOutOfTheCatalogWhenItsTableIsAlreadyGone() throws Exception {
        final TreeName kept = new TreeName("testCatalogDeleteNoTable", "kept");
        final TreeName deleted = new TreeName("testCatalogDeleteNoTable", "deleted");
        final JDBCStorage storage = new JDBCStorage(createBackendCfg(getBackendId() + "_noTable"), null);
        try {
            storage.open(AccessMode.READ_WRITE);
            storage.write(new WriteOperation() {
                @Override
                public void run(WriteableTransaction txn) throws Exception {
                    txn.openTree(kept, true);
                    txn.openTree(deleted, true);
                }
            });
            // what an interrupted change of an earlier run leaves: a row of the catalog naming a table
            // that is not there any more. The deleteTree below therefore drops nothing at all
            dropTableBehindTheBackend(storage.getTableName(deleted));
            try {
                storage.write(new WriteOperation() {
                    @Override
                    public void run(WriteableTransaction txn) throws Exception {
                        txn.deleteTree(deleted);
                        // terminal, and no conflict for write() to replay: everything this transaction
                        // still owes goes back, and the row of the deleted tree must not be part of it
                        throw new IllegalStateException("the transaction of a deleteTree failed");
                    }
                });
                fail("the write was expected to fail");
            } catch (Exception expected) {
                // what the case is about is what the failure left behind
            }
            final Set<TreeName> remaining = storage.listTrees();
            assertFalse(remaining.contains(deleted),
                "the catalog names a tree whose table was already gone when it was deleted");
            assertTrue(remaining.contains(kept), "the catalog forgot a tree that is still there");
        } finally {
            clearQuietly(storage);
        }
    }
    /**
     * The row of a tree whose table is already standing must not be left to the enclosing transaction
     * either. That is the open which fills the catalog of a backend upgraded from a version keeping
     * none: it creates no table, so nothing else of openTree() commits anything, and a transaction
     * failing after the enrolment would take the whole of it back - leaving a backend whose tables
     * are named by no catalog and whose next clear therefore drops nothing at all (#888).
     * <p>
     * The row is written and committed on a connection of the catalog's own, so this holds on every
     * engine for the same reason: nothing the caller's transaction does - or fails to do - reaches it.
     * On the branch before this one the row rode the caller's connection, and the case was green on
     * postgres for a reason of that engine alone (openTree() asks there for the cursor index of every
     * tree on every open and commits that, carrying the row with it) while the other three lost it.
     */
    @Test
    public void testAReopenedTreeStaysInTheCatalogWhenItsTransactionFails() throws Exception {
        final TreeName tree = new TreeName("testCatalogEnrolRollback", "tree");
        final JDBCStorage setUp = new JDBCStorage(createBackendCfg(getBackendId() + "_enrol"), null);
        try { // the tables of the backend, made by a storage that then goes away
            setUp.open(AccessMode.READ_WRITE);
            setUp.write(new WriteOperation() {
                @Override
                public void run(WriteableTransaction txn) throws Exception {
                    txn.openTree(tree, true);
                }
            });
        } finally {
            setUp.close();
        }
        // and the rest of what an installation upgraded to a version keeping a catalog holds: a
        // catalog naming none of those tables
        emptyTheCatalog(setUp.getTableName(setUp.getCatalogTree()));
        final JDBCStorage storage = new JDBCStorage(createBackendCfg(getBackendId() + "_enrol"), null);
        try {
            storage.open(AccessMode.READ_WRITE);
            assertFalse(storage.listTrees().contains(tree), "the catalog of the case was not emptied");
            try {
                storage.write(new WriteOperation() {
                    @Override
                    public void run(WriteableTransaction txn) throws Exception {
                        // the table is there, so this open creates none: the enrolment is the only thing
                        // this transaction has written when it fails
                        txn.openTree(tree, true);
                        // terminal, and no conflict for write() to replay: everything this transaction
                        // still owes goes back, and the row naming a standing table must not be part of it
                        throw new IllegalStateException("the transaction of an openTree failed");
                    }
                });
                fail("the write was expected to fail");
            } catch (Exception expected) {
                // what the case is about is what the failure left behind
            }
            assertTrue(storage.listTrees().contains(tree),
                "the catalog forgot a tree whose table is standing: a clear of this backend would drop nothing");
        } finally {
            clearQuietly(storage);
        }
    }
    /**
     * A tree the catalog already records at the table this version records it at is not enrolled
     * again by an open: the row would be the row that is already there. A row recording any other
     * table is, though - a removal drops the table the row records, so a row naming one this backend
     * would not create leaves the real table standing, named by nothing and dropped by no clear ever
     * after. Which of the two a row is has to be decided by what it records and not by its presence.
     */
    @Test
    public void testARowRecordingAnotherTableIsEnrolledAgain() throws Exception {
        final TreeName tree = new TreeName("testCatalogStaleRow", "tree");
        final JDBCStorage setUp = new JDBCStorage(createBackendCfg(getBackendId() + "_staleRow"), null);
        try { // the table and its row, by a storage that then goes away
            setUp.open(AccessMode.READ_WRITE);
            setUp.write(new WriteOperation() {
                @Override
                public void run(WriteableTransaction txn) throws Exception {
                    txn.openTree(tree, true);
                }
            });
        } finally {
            setUp.close();
        }
        final String catalogTable = setUp.getTableName(setUp.getCatalogTree());
        // what a version naming its tables otherwise would have left: a row of the right tree
        // recording a table this one would never create
        recordAnotherTable(catalogTable, "opendj_00000000000000000000000000000000000000000000000000000000");
        final JDBCStorage storage = new JDBCStorage(createBackendCfg(getBackendId() + "_staleRow"), null);
        try {
            storage.open(AccessMode.READ_WRITE);
            storage.write(new WriteOperation() {
                @Override
                public void run(WriteableTransaction txn) throws Exception {
                    txn.openTree(tree, true);
                }
            });
            try (final Connection con = DriverManager.getConnection(getJdbcUrl())) {
                assertEquals(
                    storage.catalogTables(con, JDBCStorage.TableScope.of(storage, con)).get(tree),
                    storage.getTableName(tree),
                    "a row recording a table this backend does not hold was left as it was: its tree is named at a table no clear can drop");
            }
        } finally {
            clearQuietly(storage);
        }
    }
    /**
     * A clear which removed no tree of its backend says why, and says it where the one table it did
     * drop was its own catalog: a catalog standing over rows that name nothing - the state a backup
     * restored beside older tables leaves - is one drop and no tree removed, which is the outcome of
     * #888 exactly and not a clear that did something.
     * <p>
     * Decided on the drops of trees and not on every drop for that reason. Counted the other way the
     * line is silent here, since dropping the catalog makes the count one.
     */
    @Test
    public void testAClearWhichRemovedNoTreeSaysWhyEvenWhereItDroppedItsCatalog() throws Exception {
        final DN baseDN = DN.valueOf("dc=clear-catalog-only,dc=com");
        final TreeName owned = new TreeName(baseDN.toNormalizedUrlSafeString(), "id2entry");
        final ReportingStorage storage =
            new ReportingStorage(createBackendCfg(getBackendId() + "_catalogOnly", baseDN));
        final String catalogTable = storage.getTableName(storage.getCatalogTree());
        try {
            storage.open(AccessMode.READ_WRITE);
            storage.write(new WriteOperation() {
                @Override
                public void run(WriteableTransaction txn) throws Exception {
                    txn.openTree(owned, true);
                }
            });
            // the catalog table is there and names nothing, so the clear below has exactly one table to
            // drop - its own - and leaves the tree standing, named by nothing
            emptyTheCatalog(catalogTable);
            storage.close();
            storage.removeStorageFiles();
            assertFalse(isExistsTable(catalogTable), "the clear left its own catalog table standing");
            assertTrue(isExistsTable(storage.getTableName(owned)),
                "a table named by no catalog was dropped: nothing may be dropped that cannot be attributed");
            storage.assertReported("a clear which dropped its catalog and removed no tree of the backend"
                    + " said nothing about why, which is the silence of #888",
                "removed no tree of this backend", "has to be started once");
        } finally {
            clearQuietly(storage);
            // left standing on purpose above: its catalog is gone, so no clear of this backend names it
            dropTableIfExists(storage.getTableName(owned));
        }
    }
    /**
     * A row recording a name outside the namespace this backend names its tables in is passed over
     * rather than reaching a {@code drop table} built from a value read back out of a table - and the
     * clear accounts for it, no other line of its report being able to: what such a row records is
     * outside the {@code opendj} names the scan of what a clear left standing walks, and is dropped
     * by nothing. The row is not there to be read again either - the catalog names itself last, so
     * the clear drops that table with the row still in it - which is why the line is asserted here
     * along with the drop: it is the only surviving copy of what the row said.
     * <p>
     * Nothing this version writes makes such a row, which is why the case makes one by hand.
     */
    @Test
    public void testAClearAccountsForACatalogRowItCannotActOn() throws Exception {
        final TreeName tree = new TreeName("testCatalogForeignRow", "tree");
        final ReportingStorage storage = new ReportingStorage(createBackendCfg(getBackendId() + "_foreignRow"));
        final String tableName = storage.getTableName(tree);
        try {
            storage.open(AccessMode.READ_WRITE);
            storage.write(new WriteOperation() {
                @Override
                public void run(WriteableTransaction txn) throws Exception {
                    txn.openTree(tree, true);
                }
            });
            final String catalogTable = storage.getTableName(storage.getCatalogTree());
            recordAnotherTable(catalogTable, "a_table_of_something_else");
            try (final Connection con = DriverManager.getConnection(getJdbcUrl())) {
                final List<String> skipped = new ArrayList<>();
                assertFalse(storage.readCatalogRows(con, catalogTable, skipped).containsKey(tree),
                    "a row recording a name no table of this backend goes by was read as a tree to drop");
                assertEquals(skipped.size(), 1, "the row the read passed over was not described to its caller: " + skipped);
                assertTrue(skipped.get(0).contains("a_table_of_something_else"),
                    "what the row records is named by nothing the clear could report: " + skipped);
            }
            // the clear still drops what it can: the catalog itself, which it names last
            storage.removeStorageFiles();
            assertTrue(isExistsTable(tableName),
                "the clear dropped the table of a tree its catalog names at another name than that table's");
            assertFalse(isExistsTable(catalogTable),
                "the clear left its own catalog table standing, so the row it passed over is still readable"
                    + " and the line reporting it is not the last copy of what it said");
            // the report itself and not the read behind it: reportSkippedRows() writes to nothing else,
            // so both of its call sites could be deleted and every assertion above would still hold
            storage.assertReported("the row the clear could not act on was reported by no line of it",
                "a_table_of_something_else", "passed over");
        } finally {
            clearQuietly(storage);
            // left standing on purpose above, so this case removes it rather than the next one meeting it
            dropTableIfExists(tableName);
        }
    }
    /**
     * A clear drops the table its catalog records for a tree, not one it derives again from the tree
     * name, so that a removal drops what was enrolled even if the naming of tables were ever to
     * change. A row recording no table at all - all a version recording the name alone would have
     * left - falls back to the derived name rather than naming nothing.
     */
    @Test
    public void testAClearDropsTheTableTheCatalogRecords() throws Exception {
        final TreeName tree = new TreeName("testCatalogValue", "tree");
        final JDBCStorage storage = new JDBCStorage(createBackendCfg(getBackendId() + "_value"), null);
        try {
            storage.open(AccessMode.READ_WRITE);
            storage.write(new WriteOperation() {
                @Override
                public void run(WriteableTransaction txn) throws Exception {
                    txn.openTree(tree, true);
                }
            });
            try (final Connection con = DriverManager.getConnection(getJdbcUrl())) {
                // asked the way a clear asks it, narrowed to where an unqualified name of the connection
                // resolves: what the removal reads is this and not a lookup of a shape of its own
                final Map<TreeName, String> recorded =
                    storage.catalogTables(con, JDBCStorage.TableScope.of(storage, con));
                assertEquals(recorded.get(tree), storage.getTableName(tree),
                    "the catalog does not record the table holding the tree its row names");
                emptyTheRecordedTableNames(storage.getTableName(storage.getCatalogTree()));
                assertEquals(storage.catalogTables(con, JDBCStorage.TableScope.of(storage, con)).get(tree),
                    storage.getTableName(tree),
                    "a row recording no table name did not fall back to the name derived from the tree");
            }
            storage.removeStorageFiles();
            assertFalse(isExistsTable(storage.getTableName(tree)), "the table the catalog named survived the clear");
        } finally {
            clearQuietly(storage);
        }
    }
    /**
     * A clear drops the catalog last, after every tree it names: what names the trees has to outlive
     * them. Dropping a table is DDL, which mysql and oracle commit as they go, so a clear that fails
     * halfway leaves a catalog still naming what is left - and the next attempt finishes it - where one
     * that had dropped the catalog first would leave tables nothing names any more and no clear could
     * ever reach.
     * <p>
     * Taken from the drops themselves and not from the map the loop walks: the map is built with the
     * catalog put last by hand, so an assertion on it would hold of any loop at all - one that sorted
     * the keys, or copied them into a HashSet, included.
     */
    @Test
    public void testAClearDropsTheCatalogAfterEveryTreeItNames() throws Exception {
        final TreeName first = new TreeName("testCatalogDropOrder", "first");
        final TreeName second = new TreeName("testCatalogDropOrder", "second");
        final List<String> order = new ArrayList<>();
        final JDBCStorage storage = new JDBCStorage(createBackendCfg(getBackendId() + "_order"), null) {
            @Override
            void dropTable(Connection con, String tableName) throws SQLException {
                order.add(tableName);
                super.dropTable(con, tableName);
            }
        };
        try {
            storage.open(AccessMode.READ_WRITE);
            storage.write(new WriteOperation() {
                @Override
                public void run(WriteableTransaction txn) throws Exception {
                    txn.openTree(first, true);
                    txn.openTree(second, true);
                }
            });
            storage.removeStorageFiles();
            final String catalogTable = storage.getTableName(storage.getCatalogTree());
            assertTrue(order.contains(storage.getTableName(first)) && order.contains(storage.getTableName(second)),
                "the clear did not drop the tables of the trees its catalog names: " + order);
            assertEquals(order.get(order.size() - 1), catalogTable,
                "the clear dropped the catalog before a tree it names, which no later clear could reach: " + order);
            assertEquals(order.indexOf(catalogTable), order.size() - 1,
                "the catalog was dropped more than once: " + order);
        } finally {
            clearQuietly(storage);
        }
    }
    /**
     * What a clear leaves standing it reports, and it reports it as what it is: a table stamped with a
     * tree of a base DN this backend serves is its own and can be removed by hand, while a table of a
     * backend sharing this database (#873) is that backend's business and no part of this outcome.
     * Told apart by the stamp of #866 and by nothing else - a table name is a bare hash.
     */
    @Test
    public void testAClearReportsTheTablesItCanAttributeToThisBackend() throws Exception {
        final DN baseDN = DN.valueOf("dc=clear-report,dc=com");
        final TreeName owned = new TreeName(baseDN.toNormalizedUrlSafeString(), "id2entry");
        // a base DN of its own, so that the neighbour is a backend that reports this table as its own:
        // what this case asserts about the clear of the other one is then an absence and not a vacuity
        final DN neighbourBaseDN = DN.valueOf("dc=clear-report-neighbour,dc=com");
        final TreeName neighbourTree = new TreeName(neighbourBaseDN.toNormalizedUrlSafeString(), "id2entry");
        final JDBCStorage storage = new JDBCStorage(createBackendCfg(getBackendId() + "_reported", baseDN), null);
        final JDBCStorage neighbour =
            new JDBCStorage(createBackendCfg(getBackendId() + "_reportedNeighbour", neighbourBaseDN), null);
        try {
            storage.open(AccessMode.READ_WRITE);
            storage.write(new WriteOperation() {
                @Override
                public void run(WriteableTransaction txn) throws Exception {
                    txn.openTree(owned, true);
                }
            });
            neighbour.open(AccessMode.READ_WRITE);
            neighbour.write(new WriteOperation() {
                @Override
                public void run(WriteableTransaction txn) throws Exception {
                    txn.openTree(neighbourTree, true);
                }
            });
            // the state of a backend upgraded from a version keeping no catalog: its tables are there
            // and nothing names them, so the clear that follows drops nothing at all
            dropTableBehindTheBackend(storage.getTableName(storage.getCatalogTree()));
            storage.close();
            storage.removeStorageFiles();
            assertTrue(isExistsTable(storage.getTableName(owned)),
                "a table named by no catalog was dropped: nothing may be dropped that cannot be attributed");
            try (final Connection con = DriverManager.getConnection(getJdbcUrl())) {
                // asked the way a clear asks it, through the same normalisation: a driver naming its
                // catalog with an empty string names no catalog, and a metadata pattern reads that as
                // "the tables belonging to no catalog at all", which would answer nothing
                final JDBCStorage.ClearLeftovers leftovers =
                    storage.leftoverTables(con, JDBCStorage.TableScope.of(storage, con));
                assertNotNull(leftovers, "the database would not say which tables the clear left standing");
                assertTrue(leftovers.ours.toString().toLowerCase().contains(storage.getTableName(owned).toLowerCase()),
                    "a table of a base DN this backend serves was not reported as its own: " + leftovers.ours);
                assertFalse(leftovers.unattributed.toString().toLowerCase().contains(storage.getTableName(owned).toLowerCase()),
                    "a table this backend can name was reported as attributable to nobody: " + leftovers.unattributed);
                assertTrue(leftovers.unreadable.isEmpty(),
                    "the stamp of a table this database does give up was reported as unreadable: " + leftovers.unreadable);
            }
            assertReportsNothingOf(storage, neighbour, neighbourTree);
        } finally {
            clearQuietly(neighbour);
            // the catalog of this one is gone, so its clear names nothing: the table it left standing on
            // purpose is dropped here by hand, as the report says such a table has to be
            clearQuietly(storage);
            dropTableIfExists(storage.getTableName(owned));
        }
    }
    /**
     * Asserts that the clear of one backend says nothing whatsoever about the tables of another - and
     * that the silence is one about tables the scan does reach: the backend those tables belong to is
     * asked the same question and reports them as its own, so an absence here is a decision and not a
     * scan that enumerated nothing.
     */
    private void assertReportsNothingOf(JDBCStorage cleared, JDBCStorage other, TreeName otherTree) throws SQLException {
        try (final Connection con = DriverManager.getConnection(getJdbcUrl())) {
            final JDBCStorage.ClearLeftovers leftovers =
                cleared.leftoverTables(con, JDBCStorage.TableScope.of(cleared, con));
            assertNotNull(leftovers, "the database would not say which tables the clear left standing");
            final String reported =
                (leftovers.ours + " " + leftovers.unattributed + " " + leftovers.unreadable).toLowerCase();
            assertFalse(reported.contains(other.getTableName(otherTree).toLowerCase()),
                "the clear of one backend reported the table of another: " + reported);
            assertFalse(reported.contains(other.getTableName(other.getCatalogTree()).toLowerCase()),
                "the clear of one backend reported the catalog of another: " + reported);
            final JDBCStorage.ClearLeftovers theirs =
                other.leftoverTables(con, JDBCStorage.TableScope.of(other, con));
            assertNotNull(theirs, "the database would not say which tables the neighbour is holding");
            assertTrue(theirs.ours.toString().toLowerCase().contains(other.getTableName(otherTree).toLowerCase()),
                "the table left unreported is one the scan does not reach at all: " + theirs.ours);
        }
    }
    /**
     * A clear which dropped nothing at all says why, where the only thing it had to say is a row it
     * could not act on: the catalog table went between the read of its rows and the loop that drops
     * what they named - an offline tool clearing the same backend, and the one state in which a clear
     * passes a row over and still drops nothing - so the account of it has no drop, no tree that had
     * lost its table and no leftover of this backend to be decided by. The row is what is left, and
     * the line reporting it is the only copy of what that row said.
     * <p>
     * What the case pins is one term of that condition. It pins it on a database holding nothing of
     * anybody else that the scan cannot attribute - the fragments asserted are the counts this case
     * owns, and a neighbour of another suite leaving an unstamped table would make the line fire for
     * a reason of its own rather than fail this.
     */
    @Test
    public void testAClearWhichDroppedNothingSaysWhyWhereARowItPassedOverIsAllItHad() throws Exception {
        final DN baseDN = DN.valueOf("dc=clear-catalog-race,dc=com");
        final TreeName owned = new TreeName(baseDN.toNormalizedUrlSafeString(), "id2entry");
        final ReportingStorage storage =
            new ReportingStorage(createBackendCfg(getBackendId() + "_catalogRace", baseDN));
        final String catalogTable = storage.getTableName(storage.getCatalogTree());
        final String ownedTable = storage.getTableName(owned);
        try {
            storage.open(AccessMode.READ_WRITE);
            storage.write(new WriteOperation() {
                @Override
                public void run(WriteableTransaction txn) throws Exception {
                    txn.openTree(owned, true);
                }
            });
            // the one row of the catalog now records a name outside the namespace this backend names
            // its tables in, so the read passes it over and the catalog names itself alone
            recordAnotherTable(catalogTable, "a_table_of_something_else");
            // and nothing of this backend is left standing for the scan to attribute to it
            dropTableBehindTheBackend(ownedTable);
            storage.close();
            // the table goes while the clear is running, which is what leaves the clear with nothing
            // dropped: an offline tool clearing the same backend a moment earlier. At the second
            // lookup and not the first, so that the rows are read before the table goes - the first
            // is catalogTables() asking whether there is a catalog at all
            storage.takeAwayAtLookupNumber(catalogTable, 2);
            storage.removeStorageFiles();
            assertFalse(isExistsTable(catalogTable), "the catalog table this case takes away was still there");
            storage.assertReported("a clear which dropped nothing at all and passed a row over said nothing"
                    + " about why, which is the silence of #888",
                "the clear removed no tree of this backend", "it dropped 0 table(s) in all",
                "0 of the trees its catalog names had lost their table already",
                "and 0 table(s) of this backend were named by no catalog");
            storage.assertReported("the row the clear could not act on was named by no line of it",
                "a_table_of_something_else", "passed over");
        } finally {
            clearQuietly(storage);
            dropTableIfExists(ownedTable);
            dropTableIfExists(catalogTable);
        }
    }
    /**
     * Takes every row out of a catalog, leaving the tables it named standing: what a backend upgraded
     * from a version keeping no catalog holds before its first read-write open fills one in.
     */
    private void emptyTheCatalog(String catalogTable) throws SQLException {
        try (final Connection con = DriverManager.getConnection(getJdbcUrl());
             final Statement st = con.createStatement()) {
            st.executeUpdate("delete from " + catalogTable);
        }
    }
    /**
     * Records the given table for every row of a catalog, as a version naming its tables otherwise
     * would have left them: the row names the right tree and a table this version never creates.
     */
    private void recordAnotherTable(String catalogTable, String tableName) throws SQLException {
        try (final Connection con = DriverManager.getConnection(getJdbcUrl());
             final PreparedStatement statement = con.prepareStatement("update " + catalogTable + " set v=?")) {
            statement.setBytes(1, tableName.getBytes(StandardCharsets.UTF_8));
            statement.executeUpdate();
        }
    }
    /** Empties the recorded table name of every row of a catalog, as a version recording none would have left it. */
    private void emptyTheRecordedTableNames(String catalogTable) throws SQLException {
        try (final Connection con = DriverManager.getConnection(getJdbcUrl());
             final PreparedStatement statement = con.prepareStatement("update " + catalogTable + " set v=?")) {
            statement.setBytes(1, new byte[0]);
            statement.executeUpdate();
        }
    }
    /**
     * Drops a table a clear left standing on purpose, so that it is not left behind for the rest of
     * the class. A failure here is swallowed rather than replacing the failure of the case it cleans
     * up after: what it leaves is dropped by the dropStaleTrees() of the next run of the class.
     */
    private void dropTableIfExists(String tableName) {
        try {
            if (isExistsTable(tableName)) {
                dropTableBehindTheBackend(tableName);
            }
        } catch (SQLException ignored) {
        }
    }
    /**
     * A storage which keeps the lines every clear it runs reports, so that a case can hold that
     * account to what it says.
     * <p>
     * Those lines change no state whatsoever, so a case asserting on the database a clear leaves
     * behind passes just as well with all of them deleted - which is how this report came to be
     * changed in three rounds of review with nothing able to fail. Here rather than in the case that
     * needed it first, for the same reason: the next line of the report wants an assertion too, and a
     * helper per case is what got the report where it was. See {@link JDBCStorage#reportClearLine}.
     */
    protected static final class ReportingStorage extends JDBCStorage {
        private final List<String> lines = Collections.synchronizedList(new ArrayList<String>());
        private volatile String tableToTakeAway;
        private final AtomicInteger lookupsToLetPass = new AtomicInteger();
        ReportingStorage(JDBCBackendCfg cfg) {
            super(cfg, null);
        }
        @Override
        void reportClearLine(LocalizableMessage line) {
            lines.add(line.toString());
            super.reportClearLine(line); // and on to the log, which is where an operator meets it
        }
        /**
         * Takes the named table away just before the given lookup of it, counting from the next one,
         * so that the lookup answers as another process taking the table a moment earlier would have
         * made it answer. What it models is the one state a clear cannot be put into from outside: a
         * table going between the read of the catalog and the loop that drops what that read named.
         * <p>
         * Which lookup matters, and the count is not decoration: a clear asks about its catalog table
         * twice - once in {@code catalogTables()} to decide whether there is a catalog to read at all,
         * and once in the drop loop, per entry. Taken away before the first, the clear reads no row,
         * passes none over and reports nothing, which is a different case from this one.
         * <p>
         * Dropped on the very connection the lookup is made on, and not on one of the test's own: the
         * clear holds its read of the catalog table until it commits, so a {@code drop table} issued
         * from a second session would queue behind the transaction that is waiting for this call to
         * return. Inside that transaction the drop takes no lock it does not already hold, and it is
         * committed with the loop - or, on the two engines committing DDL as they go, at once.
         */
        void takeAwayAtLookupNumber(String tableName, int nth) {
            lookupsToLetPass.set(nth - 1);
            tableToTakeAway = tableName;
        }
        @Override
        boolean isExistsTable(Connection con, JDBCStorage.TableScope scope, String tableName) {
            final String taking = tableToTakeAway;
            if (taking != null && taking.equalsIgnoreCase(tableName)
                    && lookupsToLetPass.getAndDecrement() <= 0) {
                tableToTakeAway = null; // once: every later lookup is answered by the database alone
                try (final PreparedStatement statement = con.prepareStatement("drop table " + taking)) {
                    statement.execute();
                } catch (SQLException e) {
                    throw new IllegalStateException("the table this case takes away could not be dropped", e);
                }
            }
            return super.isExistsTable(con, scope, tableName);
        }
        /** Every line reported so far, in the order the clears that reported them ran. */
        List<String> reported() {
            synchronized (lines) {
                return new ArrayList<>(lines);
            }
        }
        /**
         * Fails unless one reported line holds every one of the fragments. By fragments and not by the
         * whole line: what a case is entitled to pin is the thing the line is about, and a report
         * asserted word for word is a report nobody may improve the wording of.
         */
        void assertReported(String whatWentUnsaid, String... fragments) {
            for (final String line : reported()) {
                boolean holdsAll = true;
                for (final String fragment : fragments) {
                    holdsAll &= line.contains(fragment);
                }
                if (holdsAll) {
                    return;
                }
            }
            fail(whatWentUnsaid + "; the clear reported: " + reported());
        }
    }
}
opendj-server-legacy/src/test/java/org/opends/server/backends/pdb/PDBStorageTest.java
@@ -12,6 +12,7 @@
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.backends.pdb;
@@ -21,11 +22,17 @@
import static org.opends.server.util.StaticUtils.*;
import static org.forgerock.opendj.ldap.ByteString.*;
import java.util.concurrent.atomic.AtomicInteger;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.ByteString;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.forgerock.opendj.server.config.server.PDBBackendCfg;
import org.opends.server.backends.pluggable.spi.AccessMode;
import org.opends.server.backends.pluggable.spi.ReadOperation;
import org.opends.server.backends.pluggable.spi.ReadableTransaction;
import org.opends.server.backends.pluggable.spi.StorageRuntimeException;
import org.opends.server.backends.pluggable.spi.TreeName;
import org.opends.server.backends.pluggable.spi.WriteOperation;
import org.opends.server.backends.pluggable.spi.WriteableTransaction;
@@ -38,10 +45,19 @@
import org.testng.annotations.Test;
import com.persistit.Exchange;
import com.persistit.exception.RollbackException;
public class PDBStorageTest extends DirectoryServerTestCase
{
  /** A window no run of replays can spend, so that a test of the attempt cap is only ever ended by the cap. */
  private static final long UNREACHABLE_RETRY_WINDOW_NANOS = 300L * 1000L * 1000L * 1000L; //5 min
  /** A window a single attempt outlasts, so that a test of the window reaches it without seconds of build time. */
  private static final long SHORT_RETRY_WINDOW_NANOS = 200L * 1000L * 1000L; //200 ms
  /** An attempt long enough to outlast {@link #SHORT_RETRY_WINDOW_NANOS} on its own, in milliseconds. */
  private static final long ATTEMPT_LONGER_THAN_SHORT_WINDOW_MS = 300;
  private final TreeName treeName = new TreeName("dc=test", "test");
  private ServerContext serverContext;
  private PDBStorage storage;
  @BeforeClass
@@ -53,18 +69,72 @@
  @BeforeMethod
  public void setUp() throws ConfigException
  {
    ServerContext serverContext = mock(ServerContext.class);
    serverContext = mock(ServerContext.class);
    when(serverContext.getMemoryQuota()).thenReturn(new MemoryQuota());
    when(serverContext.getDiskSpaceMonitor()).thenReturn(mock(DiskSpaceMonitor.class));
    storage = new PDBStorage(createBackendCfg(), serverContext);
    // the volume is removed on the way in as well as on the way out: a build whose JVM died never ran tearDown(),
    // and this class shares a fixed db-directory across methods and across builds, so what that run left behind
    // would still be here to answer this method's reads
    storage.removeStorageFiles();
    storage.open(AccessMode.READ_WRITE);
  }
  @AfterMethod
  public void tearDown()
  {
    storage.close();
    closeAndRemove(storage);
  }
  /**
   * Closes the storage and removes its volume, keeping whichever of the two failed first. Removing it from a
   * finally would let a removal failure replace the close() failure (JLS 14.20.2) - and a close() that throws is
   * exactly the case the removal is here for.
   */
  private static void closeAndRemove(PDBStorage storage)
  {
    RuntimeException failure = null;
    try
    {
      storage.close();
    }
    catch (RuntimeException e)
    {
      failure = e;
    }
    try
    {
      storage.removeStorageFiles();
    }
    catch (RuntimeException e)
    {
      if (failure == null)
      {
        failure = e;
      }
      else
      {
        failure.addSuppressed(e);
      }
    }
    if (failure != null)
    {
      throw failure;
    }
  }
  /**
   * Replaces the storage under test with one bounded by the given values, so that the bound a test is about is
   * the one that ends its replays. With the shipped values the two race: the nine backoffs of a full ladder draw
   * from 50+100+200+400+800+1000x4, so an attempt cap test can be ended by the ten second window instead, and a
   * window test has to make every attempt outlast seconds of that window to reach it.
   */
  private void reopenWithReplayBounds(int maxRetries, long retryWindowNanos) throws Exception
  {
    closeAndRemove(storage);
    storage = new PDBStorage(createBackendCfg(), serverContext, maxRetries, retryWindowNanos);
    storage.open(AccessMode.READ_WRITE);
  }
  @Test
@@ -122,6 +192,260 @@
    assertThat(storage.getNewExchange(treeName, true)).isNotSameAs(initial);
  }
  @Test
  public void testWriteGivesUpAfterTheAttemptCap() throws Exception
  {
    // the shipped cap, against a window the ladder of backoffs cannot reach: on the shipped window those nine
    // backoffs draw from up to 5550 ms, so a loaded machine ends this loop on the window and the cap goes untested
    reopenWithReplayBounds(PDBStorage.MAX_RETRIES, UNREACHABLE_RETRY_WINDOW_NANOS);
    createTree();
    final RollbackException conflict = new RollbackException();
    final AtomicInteger attempts = new AtomicInteger();
    try
    {
      storage.write(new WriteOperation()
      {
        @Override
        public void run(WriteableTransaction txn) throws Exception
        {
          attempts.incrementAndGet();
          txn.put(treeName, valueOfUtf8("abandoned"), valueOfUtf8("value"));
          throw conflict;
        }
      });
      failBecauseExceptionWasNotThrown(StorageRuntimeException.class);
    }
    catch (StorageRuntimeException e)
    {
      assertThat(e.getSuppressed()).contains(conflict);
    }
    assertThat(attempts.get()).isEqualTo(PDBStorage.MAX_RETRIES);
    assertThat(read("abandoned")).isNull();
  }
  @Test
  public void testWriteIsReplayedUntilTheConflictClears() throws Exception
  {
    createTree();
    final AtomicInteger attempts = new AtomicInteger();
    storage.write(new WriteOperation()
    {
      @Override
      public void run(WriteableTransaction txn) throws Exception
      {
        if (attempts.incrementAndGet() <= 3)
        {
          throw new RollbackException();
        }
        txn.put(treeName, valueOfUtf8("applied"), valueOfUtf8("value"));
      }
    });
    assertThat(attempts.get()).isEqualTo(4);
    assertThat(read("applied")).isEqualTo(valueOfUtf8("value"));
  }
  /**
   * PersistIt reports a write-write conflict only once it has waited on it - up to
   * {@code SharedResource.DEFAULT_MAX_WAIT_TIME}, a minute, which this backend never lowers - so a single attempt
   * can outlast the whole window. Giving up on the window alone would then replay nothing, in the very case where
   * the replay is likeliest to succeed: the transaction that was blocking this one has just finished.
   */
  @Test
  public void testWriteIsReplayedOnceWhenTheFirstAttemptOutlastsTheWindow() throws Exception
  {
    reopenWithReplayBounds(PDBStorage.MAX_RETRIES, SHORT_RETRY_WINDOW_NANOS);
    createTree();
    final AtomicInteger attempts = new AtomicInteger();
    storage.write(new WriteOperation()
    {
      @Override
      public void run(WriteableTransaction txn) throws Exception
      {
        if (attempts.incrementAndGet() == 1)
        {
          Thread.sleep(ATTEMPT_LONGER_THAN_SHORT_WINDOW_MS);
          throw new RollbackException();
        }
        txn.put(treeName, valueOfUtf8("outlasted"), valueOfUtf8("written"));
      }
    });
    assertThat(attempts.get()).isEqualTo(2);
    assertThat(read("outlasted")).isEqualTo(valueOfUtf8("written"));
  }
  @Test
  public void testExhaustedWriteNamesTheAttemptsItSpent() throws Exception
  {
    // the message is the same at any cap, so this one is spent in two backoffs rather than in the shipped ladder
    final int maxRetries = 3;
    reopenWithReplayBounds(maxRetries, UNREACHABLE_RETRY_WINDOW_NANOS);
    createTree();
    try
    {
      storage.write(new WriteOperation()
      {
        @Override
        public void run(WriteableTransaction txn) throws Exception
        {
          throw new RollbackException();
        }
      });
      failBecauseExceptionWasNotThrown(StorageRuntimeException.class);
    }
    catch (StorageRuntimeException e)
    {
      assertThat(e.getMessage()).contains("PDBStorageTest").contains(maxRetries + " attempts");
      // and which of the two bounds ran out, since the attempt count alone does not say
      assertThat(e.getMessage()).contains("attempt cap");
      // write() unwraps a StorageRuntimeException that carries a cause, which would replace this message with
      // the bare RollbackException, and it is the message the config change paths report
      assertThat(e.getCause()).isNull();
    }
  }
  @Test
  public void testWriteGivesUpOnTheWindowWhenAttemptsAreSlow() throws Exception
  {
    reopenWithReplayBounds(PDBStorage.MAX_RETRIES, SHORT_RETRY_WINDOW_NANOS);
    createTree();
    final AtomicInteger attempts = new AtomicInteger();
    try
    {
      storage.write(new WriteOperation()
      {
        @Override
        public void run(WriteableTransaction txn) throws Exception
        {
          attempts.incrementAndGet();
          // a conflict this slow to report spends the wall clock window long before the attempt cap
          Thread.sleep(ATTEMPT_LONGER_THAN_SHORT_WINDOW_MS);
          throw new RollbackException();
        }
      });
      failBecauseExceptionWasNotThrown(StorageRuntimeException.class);
    }
    catch (StorageRuntimeException e)
    {
      // the window is what ended it, and it says so: an assertion on the attempt count alone would also pass for
      // a give up on attempt 1, which is the regression the attempt > 1 exemption exists to prevent
      assertThat(e.getMessage()).contains("retry window");
    }
    // one attempt beyond the first: the first spends the window, the exemption grants the replay, and the check
    // after that replay is the one that gives up
    assertThat(attempts.get()).isEqualTo(2);
  }
  @Test
  public void testInterruptedWriteReportsTheConflictItWasReplaying() throws Exception
  {
    createTree();
    final RollbackException conflict = new RollbackException();
    final AtomicInteger attempts = new AtomicInteger();
    final boolean interruptedAfterwards;
    try
    {
      storage.write(new WriteOperation()
      {
        @Override
        public void run(WriteableTransaction txn) throws Exception
        {
          attempts.incrementAndGet();
          // interrupted here rather than before the write, where the transaction this attempt begins would
          // report the interrupt itself and the loop would never reach the backoff being tested
          Thread.currentThread().interrupt();
          throw conflict;
        }
      });
      failBecauseExceptionWasNotThrown(StorageRuntimeException.class);
      return;
    }
    catch (StorageRuntimeException e)
    {
      interruptedAfterwards = Thread.interrupted();
      // the conflict, not the interrupt, is what the caller is told about - but through the same shape the
      // exhausted loop uses, since a bare RollbackException reaches every caller as its own class name
      assertThat(e.getMessage()).contains("PDBStorageTest").contains("interrupted");
      assertThat(e.getSuppressed()).contains(conflict).hasAtLeastOneElementOfType(InterruptedException.class);
      assertThat(e.getCause()).isNull();
    }
    finally
    {
      Thread.interrupted();
    }
    // sleep() cleared the flag, so the caller only learns of the interrupt if the loop restores it
    assertThat(interruptedAfterwards).isTrue();
    // one attempt even though the first backoff is a random 0-49 ms and so is sometimes 0: Thread.sleep() checks
    // the interrupt flag before it checks for a zero duration, so the replay is never reached
    assertThat(attempts.get()).isEqualTo(1);
  }
  /**
   * The delay grows with the attempt and stays under the cap, so that a contention the first delays did not
   * outlast still has a chance to clear without the replays overrunning the window on sleep alone.
   */
  @Test
  public void testRetryDelayGrowsAndStaysBounded()
  {
    long previousBound = 0;
    for (int attempt = 1; attempt <= PDBStorage.MAX_RETRIES; attempt++)
    {
      long bound = 0;
      for (int i = 0; i < 100; i++)
      {
        final long delay = PDBStorage.retryDelayMillis(attempt);
        assertThat(delay).as("attempt %d", attempt).isGreaterThanOrEqualTo(0).isLessThan(1000);
        bound = Math.max(bound, delay);
      }
      if (attempt == 1)
      {
        // the flat sleep this loop took before it was bounded, unchanged: only the later attempts back off
        assertThat(bound).as("attempt 1 delays past the sleep this loop always took").isLessThan(50);
      }
      assertThat(bound).as("attempt %d did not grow past attempt %d", attempt, attempt - 1)
          .isGreaterThanOrEqualTo(previousBound / 2);
      previousBound = bound;
    }
    // and the growth is real rather than a delay that never leaves the first tier
    long grown = 0;
    for (int i = 0; i < 100; i++)
    {
      grown = Math.max(grown, PDBStorage.retryDelayMillis(PDBStorage.MAX_RETRIES));
    }
    assertThat(grown).as("the last attempts still sleep within the first attempt's bound").isGreaterThan(500);
  }
  private void createTree() throws Exception
  {
    storage.write(new WriteOperation()
    {
      @Override
      public void run(WriteableTransaction txn) throws Exception
      {
        txn.openTree(treeName, true);
      }
    });
  }
  private ByteString read(final String key) throws Exception
  {
    return storage.read(new ReadOperation<ByteString>()
    {
      @Override
      public ByteString run(ReadableTransaction txn) throws Exception
      {
        return txn.read(treeName, valueOfUtf8(key));
      }
    });
  }
  protected PDBBackendCfg createBackendCfg()
  {
    PDBBackendCfg backendCfg = mockCfg(PDBBackendCfg.class);
opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/ReplayedConfigChangeTest.java
New file
@@ -0,0 +1,967 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.backends.pluggable;
import static org.assertj.core.api.Assertions.assertThat;
import static org.opends.messages.BackendMessages.ERR_BACKEND_BASEDN_NO_LONGER_HELD;
import static org.opends.messages.BackendMessages.ERR_BACKEND_CANNOT_LIST_TREES_AFTER_BASEDN_CHANGE;
import static org.opends.messages.BackendMessages.ERR_BACKEND_CANNOT_REGISTER_BASEDN;
import static org.forgerock.opendj.config.ConfigurationMock.mockCfg;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.opends.server.backends.pluggable.State.IndexFlag.TRUSTED;
import static org.opends.server.backends.pluggable.SuffixContainer.STATE_INDEX_NAME;
import static org.opends.server.util.CollectionUtils.newTreeSet;
import static org.forgerock.util.Utils.closeSilently;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.ByteSequence;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.schema.AttributeType;
import org.forgerock.opendj.server.config.meta.BackendIndexCfgDefn.IndexType;
import org.forgerock.opendj.server.config.server.BackendIndexCfg;
import org.forgerock.opendj.server.config.server.PDBBackendCfg;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.opends.server.backends.pdb.PDBStorage;
import org.opends.server.backends.pluggable.State.IndexFlag;
import org.opends.server.backends.pluggable.spi.AccessMode;
import org.opends.server.backends.pluggable.spi.Cursor;
import org.opends.server.backends.pluggable.spi.Importer;
import org.opends.server.backends.pluggable.spi.ReadOperation;
import org.opends.server.backends.pluggable.spi.Storage;
import org.opends.server.backends.pluggable.spi.StorageRuntimeException;
import org.opends.server.backends.pluggable.spi.StorageStatus;
import org.opends.server.backends.pluggable.spi.TreeName;
import org.opends.server.backends.pluggable.spi.UpdateFunction;
import org.opends.server.backends.pluggable.spi.WriteOperation;
import org.opends.server.backends.pluggable.spi.WriteableTransaction;
import org.opends.server.core.ServerContext;
import org.opends.server.types.BackupConfig;
import org.opends.server.types.BackupDirectory;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.RestoreConfig;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import com.persistit.exception.RollbackException;
/**
 * Tests that {@link BackendImpl#applyConfigurationChange} survives a replay of its
 * {@link WriteOperation}. {@link Storage#write(WriteOperation)} may replay the operation after a
 * transaction conflict, so every side effect it performs must either be transactional or be
 * idempotent - see OpenDJ issue #907.
 * <p>
 * The conflict is raised from inside the operation as the {@link RollbackException} PersistIt
 * itself raises, so that the replay is driven by {@code PDBStorage.write}'s own retry loop rather
 * than by a second call to it. That loop keeps one storage implementation - and with it its cache
 * of PersistIt exchanges - across every attempt, which a second call would not.
 */
@SuppressWarnings("javadoc")
@Test(groups = { "precommit", "pluggablebackend" }, sequential = true)
public class ReplayedConfigChangeTest extends DirectoryServerTestCase
{
  private static final String BACKEND_ID = "ReplayedConfigChangeTest";
  private static final DN KEPT = DN.valueOf("dc=b907a,dc=com");
  private static final DN REMOVED = DN.valueOf("dc=b907b,dc=com");
  private static final DN ADDED = DN.valueOf("dc=b907c,dc=com");
  /** Hierarchically related to {@link #KEPT}, which one backend is not allowed to serve as well. */
  private static final DN UNREGISTRABLE = DN.valueOf("dc=b907d,dc=b907a,dc=com");
  private ServerContext serverContext;
  private AttributeType cnType;
  @BeforeClass
  public void startServer() throws Exception
  {
    TestCaseUtils.startServer();
    serverContext = TestCaseUtils.getServerContext();
    cnType = serverContext.getSchema().getAttributeType("cn");
  }
  /**
   * These tests are designed to fail, and a failing one can leave a base DN behind in the server
   * wide registry, where it would outlive the test and break the next one to use that DN.
   */
  @AfterMethod
  public void deregisterLeftoverBaseDNs()
  {
    for (DN baseDN : new DN[] { KEPT, REMOVED, ADDED, UNREGISTRABLE })
    {
      try
      {
        serverContext.getBackendConfigManager().deregisterBaseDN(baseDN);
      }
      catch (Exception alreadyGone)
      {
        // Which is what the test should have left behind.
      }
    }
  }
  /**
   * A base DN removal whose transaction conflicts before it touches the storage must be replayed
   * without reporting a failure against the base DN it has already deregistered.
   */
  @Test
  public void removalIsReplayableWhenTheTransactionConflictsBeforeAnyStorageAccess() throws Exception
  {
    final ReplayingBackend backend = openBackend(newTreeSet(KEPT, REMOVED));
    try
    {
      final RootContainer rootContainer = backend.getRootContainer();
      assertThat(rootContainer.getBaseDNs()).contains(REMOVED);
      backend.storage.conflictAtFirstStorageAccess(1);
      final ConfigChangeResult ccr = backend.applyConfigurationChange(backendCfg(newTreeSet(KEPT)));
      assertThat(backend.storage.attempts()).isEqualTo(2);
      assertThat(ccr.getMessages()).isEmpty();
      assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
      assertThat(rootContainer.getBaseDNs()).doesNotContain(REMOVED);
      assertThat(backend.getBaseDNs()).doesNotContain(REMOVED);
      assertThat(serverContext.getBackendConfigManager().getLocalBackendWithBaseDN(REMOVED)).isNull();
    }
    finally
    {
      backend.finalizeBackend();
    }
  }
  /**
   * A base DN addition whose transaction conflicts at commit time must be replayed, so that what
   * the entry container it opens writes ends up committed rather than discarded by the rollback.
   */
  @Test
  public void additionIsReplayableWhenTheTransactionConflictsAtCommitTime() throws Exception
  {
    final ReplayingBackend backend = openBackend(newTreeSet(KEPT));
    try
    {
      final RootContainer rootContainer = backend.getRootContainer();
      assertThat(rootContainer.getBaseDNs()).doesNotContain(ADDED);
      backend.storage.conflictAtCommit(1);
      final ConfigChangeResult ccr = backend.applyConfigurationChange(backendCfg(newTreeSet(KEPT, ADDED)));
      assertThat(backend.storage.attempts()).isEqualTo(2);
      assertThat(ccr.getMessages()).isEmpty();
      assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
      assertThat(rootContainer.getBaseDNs()).contains(ADDED);
      assertThat(backend.getBaseDNs()).contains(ADDED);
      assertThat(serverContext.getBackendConfigManager().getLocalBackendWithBaseDN(ADDED)).isSameAs(backend);
      // Everything the newly opened entry container wrote belongs to the rolled back transaction,
      // so the storage has to be asked, not the entry container which remembers writing it.
      final EntryContainer ec = rootContainer.getEntryContainer(ADDED);
      final TreeName cnIndex = ec.getAttributeIndex(cnType).getNameToIndexes().values().iterator().next().getName();
      assertThat(rootContainer.getStorage().listTrees()).contains(cnIndex);
      assertThat(persistedFlags(rootContainer, ec, cnIndex)).contains(TRUSTED);
      // The entry container the rolled back attempt opened registered five configuration listeners,
      // which only its close() takes back, so the replay has to give it up before opening another.
      verify(backend.configuredWith, times(1)).removePluggableChangeListener(any());
    }
    finally
    {
      backend.finalizeBackend();
    }
  }
  /**
   * The trees of a removed base DN are deleted by the operation itself, so a replay deletes trees a
   * rolled back attempt had already deleted. This is the case which reaches the storage, and it
   * removes and adds a base DN at once because that is what an operator editing the configuration
   * does.
   */
  @Test
  public void aRemovalAndAnAdditionInOneChangeSurviveRepeatedReplay() throws Exception
  {
    final ReplayingBackend backend = openBackend(newTreeSet(KEPT, REMOVED));
    try
    {
      final RootContainer rootContainer = backend.getRootContainer();
      final Set<TreeName> removedTrees = treesOf(rootContainer.getEntryContainer(REMOVED));
      assertThat(rootContainer.getStorage().listTrees()).containsAll(removedTrees);
      // More than one conflict, because the contract is that the operation is replayed until it
      // succeeds rather than that it survives a single replay.
      backend.storage.conflictAtCommit(2);
      final ConfigChangeResult ccr = backend.applyConfigurationChange(backendCfg(newTreeSet(KEPT, ADDED)));
      assertThat(backend.storage.attempts()).isEqualTo(3);
      assertThat(ccr.getMessages()).isEmpty();
      assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
      assertThat(rootContainer.getBaseDNs()).contains(KEPT, ADDED).doesNotContain(REMOVED);
      assertThat(backend.getBaseDNs()).contains(KEPT, ADDED).doesNotContain(REMOVED);
      final Set<TreeName> storedTrees = rootContainer.getStorage().listTrees();
      assertThat(storedTrees).doesNotContainAnyElementsOf(removedTrees);
      assertThat(storedTrees).containsAll(treesOf(rootContainer.getEntryContainer(ADDED)));
      assertThat(storedTrees).containsAll(treesOf(rootContainer.getEntryContainer(KEPT)));
    }
    finally
    {
      backend.finalizeBackend();
    }
  }
  /**
   * A failure the storage engine does not replay must leave the backend as it was and say which
   * base DNs the change was about, since the failure itself never names them.
   */
  @Test
  public void aFailureWhichIsNotReplayedAppliesNothingAndNamesTheBaseDNs() throws Exception
  {
    final ReplayingBackend backend = openBackend(newTreeSet(KEPT, REMOVED));
    try
    {
      final RootContainer rootContainer = backend.getRootContainer();
      final Set<TreeName> removedTrees = treesOf(rootContainer.getEntryContainer(REMOVED));
      backend.storage.failWithoutReplay();
      final ConfigChangeResult ccr = backend.applyConfigurationChange(backendCfg(newTreeSet(KEPT, ADDED)));
      assertThat(ccr.getResultCode()).isNotEqualTo(ResultCode.SUCCESS);
      assertThat(ccr.getMessages().toString()).contains(REMOVED.toString()).contains(ADDED.toString());
      // Nothing was registered, nothing was deregistered, and the rollback put the trees back.
      assertThat(rootContainer.getBaseDNs()).contains(REMOVED).doesNotContain(ADDED);
      assertThat(backend.getBaseDNs()).contains(REMOVED).doesNotContain(ADDED);
      assertThat(serverContext.getBackendConfigManager().getLocalBackendWithBaseDN(REMOVED)).isSameAs(backend);
      assertThat(serverContext.getBackendConfigManager().getLocalBackendWithBaseDN(ADDED)).isNull();
      assertThat(rootContainer.getStorage().listTrees()).containsAll(removedTrees);
    }
    finally
    {
      backend.finalizeBackend();
    }
  }
  /**
   * A failure which the storage engine neither replays nor rolls back - the DDL of mysql and oracle
   * commits of its own accord, and cassandra has no transaction at all - leaves the trees of a
   * removed base DN gone. That base DN has to stop being reachable, or every operation against it
   * meets a storage error rather than the "no such entry" its removal was meant to leave.
   */
  @Test
  public void aFailureWhichIsNotRolledBackGivesUpTheBaseDNsWhoseTreesAreGone() throws Exception
  {
    final ReplayingBackend backend = openBackend(newTreeSet(KEPT, REMOVED));
    try
    {
      final RootContainer rootContainer = backend.getRootContainer();
      final Set<TreeName> removedTrees = treesOf(rootContainer.getEntryContainer(REMOVED));
      backend.storage.failAfterCommit();
      final ConfigChangeResult ccr = backend.applyConfigurationChange(backendCfg(newTreeSet(KEPT, ADDED)));
      assertThat(ccr.getResultCode()).isNotEqualTo(ResultCode.SUCCESS);
      assertThat(ccr.adminActionRequired()).isTrue();
      assertThat(ccr.getMessages().toString()).contains(REMOVED.toString()).contains(ADDED.toString());
      // The trees are gone, so the base DN is given up rather than left routed at them.
      assertThat(rootContainer.getStorage().listTrees()).doesNotContainAnyElementsOf(removedTrees);
      assertThat(rootContainer.getBaseDNs()).doesNotContain(REMOVED);
      assertThat(backend.getBaseDNs()).doesNotContain(REMOVED);
      assertThat(serverContext.getBackendConfigManager().getLocalBackendWithBaseDN(REMOVED)).isNull();
      // The added base DN is not registered, since the change it belongs to failed.
      assertThat(rootContainer.getBaseDNs()).doesNotContain(ADDED);
      assertThat(backend.getBaseDNs()).doesNotContain(ADDED);
      assertThat(serverContext.getBackendConfigManager().getLocalBackendWithBaseDN(ADDED)).isNull();
    }
    finally
    {
      backend.finalizeBackend();
    }
  }
  /**
   * The same failure leaves the trees it created for a base DN which is not being added after all
   * exactly where they are. The configuration which names that base DN was stored before this
   * listener was called - {@code ConfigurationHandler.replaceEntry} writes the entry, and only then
   * notifies - and the failure does not take it back, so the next open of this backend opens that
   * base DN again from it, adopting the trees which survived and creating the ones which did not.
   * Deleting them here would take away the trees of a base DN the stored configuration still asks
   * this backend to serve, and would buy nothing: that open re-creates them empty.
   */
  @Test
  public void aFailureWhichIsNotRolledBackLeavesTheTreesItCreated() throws Exception
  {
    final ReplayingBackend backend = openBackend(newTreeSet(KEPT, REMOVED));
    try
    {
      final RootContainer rootContainer = backend.getRootContainer();
      final Set<TreeName> storedBefore = new HashSet<>(rootContainer.getStorage().listTrees());
      backend.storage.failAfterCommit();
      final ConfigChangeResult ccr = backend.applyConfigurationChange(backendCfg(newTreeSet(KEPT, ADDED)));
      assertThat(ccr.getResultCode()).isNotEqualTo(ResultCode.SUCCESS);
      assertThat(rootContainer.getBaseDNs()).doesNotContain(ADDED);
      // Named by the failure, since nothing in the running server names them any more.
      assertThat(ccr.getMessages().toString()).contains(ADDED.toString());
      final Set<TreeName> left = new HashSet<>(rootContainer.getStorage().listTrees());
      left.removeAll(storedBefore);
      assertThat(left).as("the trees created for the base DN the stored configuration still names")
          .isNotEmpty();
      for (TreeName tree : left)
      {
        assertThat(tree.getBaseDN()).isEqualTo(ADDED.toNormalizedUrlSafeString());
      }
    }
    finally
    {
      backend.finalizeBackend();
    }
  }
  /**
   * Whether anything survived a failure is read from the trees the storage still holds, so a backend
   * which cannot be asked for them reconciles nothing at all. The operator has to be told that,
   * since it is the case where the failure alone says least about what the backend is left serving.
   */
  @Test
  public void aFailureWhoseSurvivingTreesCannotBeListedSaysSo() throws Exception
  {
    final ReplayingBackend backend = openBackend(newTreeSet(KEPT, REMOVED));
    try
    {
      backend.storage.onListTrees(new Runnable()
      {
        @Override
        public void run()
        {
          throw new StorageRuntimeException("the trees cannot be listed");
        }
      });
      backend.storage.failAfterCommit();
      final ConfigChangeResult ccr = backend.applyConfigurationChange(backendCfg(newTreeSet(KEPT)));
      backend.storage.onListTrees(null);
      assertThat(ccr.getResultCode()).isNotEqualTo(ResultCode.SUCCESS);
      assertThat(ccr.adminActionRequired()).isTrue();
      assertThat(ordinalsOf(ccr)).contains(ERR_BACKEND_CANNOT_LIST_TREES_AFTER_BASEDN_CHANGE.ordinal());
    }
    finally
    {
      backend.finalizeBackend();
    }
  }
  /**
   * A base DN this backend no longer holds must fail the change rather than the method: an entry
   * container unregistered while the change was working out what to do leaves the root container
   * with nothing to answer for that base DN, and the administration framework is owed a result
   * whatever happens.
   */
  @Test
  public void aBaseDNTheBackendNoLongerHoldsFailsTheChangeRatherThanTheMethod() throws Exception
  {
    final ReplayingBackend backend = openBackend(newTreeSet(KEPT, REMOVED));
    try
    {
      final RootContainer rootContainer = backend.getRootContainer();
      final PDBBackendCfg newCfg = backendCfg(newTreeSet(KEPT));
      when(newCfg.getBaseDN()).thenReturn(new UnregisteringWhenAsked(rootContainer, REMOVED, newTreeSet(KEPT)));
      final ConfigChangeResult ccr = backend.applyConfigurationChange(newCfg);
      assertThat(ccr.getResultCode()).isNotEqualTo(ResultCode.SUCCESS);
      assertThat(ordinalsOf(ccr)).contains(ERR_BACKEND_BASEDN_NO_LONGER_HELD.ordinal());
      assertThat(ccr.getMessages().toString()).contains(REMOVED.toString());
      // Nothing was applied, so the base DNs this backend serves are the ones it served before.
      assertThat(backend.getBaseDNs()).contains(KEPT);
      assertThat(rootContainer.getStorage().listTrees()).containsAll(treesOf(rootContainer.getEntryContainer(KEPT)));
    }
    finally
    {
      backend.finalizeBackend();
    }
  }
  /**
   * A base DN whose entry container is gone must not be answered with an ancestor's.
   * {@link RootContainer#getEntryContainer} walks up the DN until it finds a container, which is how
   * an entry is routed to the base DN above it; asked for a base DN the root container no longer
   * holds, it hands back the container of the one it does. Deleting the trees of that container is
   * deleting the trees of a base DN this backend is still serving.
   * <p>
   * Two base DNs of one backend are hierarchically related only after a registration the registry
   * refused, which leaves the entry container behind in the root container - see
   * {@link #aBaseDNWhichCannotBeRegisteredReportsWhereItFailed}.
   */
  @Test
  public void anEntryContainerWhichIsGoneIsNotAnsweredWithItsParent() throws Exception
  {
    final ReplayingBackend backend = openBackend(newTreeSet(KEPT));
    try
    {
      final RootContainer rootContainer = backend.getRootContainer();
      // Refused by the registry, and so left in the root container underneath KEPT.
      backend.applyConfigurationChange(backendCfg(newTreeSet(KEPT, UNREGISTRABLE)));
      assertThat(rootContainer.getBaseDNs()).contains(KEPT, UNREGISTRABLE);
      final Set<TreeName> keptTrees = treesOf(rootContainer.getEntryContainer(KEPT));
      final PDBBackendCfg newCfg = backendCfg(newTreeSet(KEPT));
      when(newCfg.getBaseDN()).thenReturn(new UnregisteringWhenAsked(rootContainer, UNREGISTRABLE, newTreeSet(KEPT)));
      final ConfigChangeResult ccr = backend.applyConfigurationChange(newCfg);
      // The base DN above the one which was gone is left alone, trees and routing both.
      assertThat(rootContainer.getStorage().listTrees())
          .as("the trees of the base DN above the one which was gone").containsAll(keptTrees);
      assertThat(rootContainer.getBaseDNs()).contains(KEPT);
      assertThat(serverContext.getBackendConfigManager().getLocalBackendWithBaseDN(KEPT)).isSameAs(backend);
      // And the change says which base DN stopped it.
      assertThat(ccr.getResultCode()).isNotEqualTo(ResultCode.SUCCESS);
      assertThat(ordinalsOf(ccr)).contains(ERR_BACKEND_BASEDN_NO_LONGER_HELD.ordinal());
      assertThat(ccr.getMessages().toString()).contains(UNREGISTRABLE.toString());
    }
    finally
    {
      backend.finalizeBackend();
    }
  }
  /**
   * A base DN the registry refuses is reported with the whole of what refused it. The registry
   * raises the same message for several reasons and from more than one place, so the exception's own
   * text does not say which of them happened; the frames it was raised on do.
   */
  @Test
  public void aBaseDNWhichCannotBeRegisteredReportsWhereItFailed() throws Exception
  {
    final ReplayingBackend backend = openBackend(newTreeSet(KEPT));
    try
    {
      final ConfigChangeResult ccr = backend.applyConfigurationChange(backendCfg(newTreeSet(KEPT, UNREGISTRABLE)));
      assertThat(ccr.getResultCode()).isNotEqualTo(ResultCode.SUCCESS);
      assertThat(ordinalsOf(ccr)).contains(ERR_BACKEND_CANNOT_REGISTER_BASEDN.ordinal());
      assertThat(ccr.getMessages().toString())
          .as("the reported cause never says where it was raised")
          .contains("BackendConfigManager.java:");
    }
    finally
    {
      backend.finalizeBackend();
    }
  }
  /**
   * The entry container locks are held for the write which deletes the trees, and no longer:
   * everything below the write reaches {@code BackendConfigManager}, whose single registry lock the
   * server already takes in the opposite order - {@code shutdownLocalBackends} and a backend being
   * disabled both hold it while closing a root container, which locks every entry container in turn.
   * Holding both in this order would deadlock a base DN change against a shutdown, with no timeout
   * on either side.
   */
  @Test
  public void theRegistryIsNotTouchedWhileAnEntryContainerLockIsHeld() throws Exception
  {
    final ReplayingBackend backend = openBackend(newTreeSet(KEPT, REMOVED));
    try
    {
      final RootContainer rootContainer = backend.getRootContainer();
      final EntryContainer removed = rootContainer.getEntryContainer(REMOVED);
      // Listing the surviving trees is the last thing the failure path does before it deregisters,
      // so it is asked on the very thread, and at the very moment, the deadlock would be reached.
      final boolean[] lockHeld = new boolean[] { false };
      final boolean[] asked = new boolean[] { false };
      backend.storage.onListTrees(new Runnable()
      {
        @Override
        public void run()
        {
          asked[0] = true;
          lockHeld[0] |= ((ReentrantReadWriteLock.WriteLock) removed.exclusiveLock).isHeldByCurrentThread();
        }
      });
      backend.storage.failAfterCommit();
      backend.applyConfigurationChange(backendCfg(newTreeSet(KEPT)));
      backend.storage.onListTrees(null);
      assertThat(asked).as("the failure path never listed the surviving trees").containsExactly(true);
      assertThat(lockHeld).as("the entry container lock was still held").containsExactly(false);
    }
    finally
    {
      backend.finalizeBackend();
    }
  }
  /**
   * A configuration change which leaves the base DNs alone - every change to index-entry-limit,
   * db-cache-percent and the rest - has no storage work to do, so it opens no transaction to
   * commit nothing.
   */
  @Test
  public void aChangeWhichLeavesTheBaseDNsAloneOpensNoTransaction() throws Exception
  {
    final ReplayingBackend backend = openBackend(newTreeSet(KEPT, REMOVED));
    try
    {
      final int writesBefore = backend.storage.writes();
      final ConfigChangeResult ccr = backend.applyConfigurationChange(backendCfg(newTreeSet(KEPT, REMOVED)));
      assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
      assertThat(ccr.getMessages()).isEmpty();
      assertThat(backend.storage.writes()).isEqualTo(writesBefore);
      assertThat(backend.getBaseDNs()).contains(KEPT, REMOVED);
    }
    finally
    {
      backend.finalizeBackend();
    }
  }
  /** The messages a change result carries, by identity rather than by their formatted text. */
  private static Set<Integer> ordinalsOf(ConfigChangeResult ccr)
  {
    final Set<Integer> ordinals = new HashSet<>();
    for (LocalizableMessage message : ccr.getMessages())
    {
      ordinals.add(message.ordinal());
    }
    return ordinals;
  }
  private static Set<TreeName> treesOf(EntryContainer ec)
  {
    final Set<TreeName> names = new HashSet<>();
    for (Tree tree : ec.listTrees())
    {
      names.add(tree.getName());
    }
    return names;
  }
  /** Reads back the flags an index was given when it was opened, as they are stored. */
  private static EnumSet<IndexFlag> persistedFlags(RootContainer rootContainer, EntryContainer ec, TreeName index)
      throws Exception
  {
    final State state = new State(new TreeName(ec.getTreePrefix(), STATE_INDEX_NAME));
    return rootContainer.getStorage().read(txn -> state.getIndexFlags(txn, index));
  }
  private ReplayingBackend openBackend(SortedSet<DN> baseDNs) throws Exception
  {
    final ReplayingBackend backend = new ReplayingBackend();
    backend.setBackendID(BACKEND_ID);
    backend.configuredWith = backendCfg(baseDNs);
    backend.configureBackend(backend.configuredWith, serverContext);
    // Start from a pristine on-disk state so that a previous run cannot mask the defect.
    backend.storage.removeStorageFiles();
    try
    {
      backend.openBackend();
    }
    catch (Exception e)
    {
      // openBackend() opens the root container before it preloads, counts the entries, registers
      // the base DNs and registers the monitor, so a failure in any of those leaves the volume open
      // and the monitor registered. Every following test would then fail in openBackend() too, and
      // the one which actually broke would be lost among them.
      try
      {
        if (backend.getRootContainer() != null)
        {
          backend.finalizeBackend();
        }
        else
        {
          backend.storage.close();
        }
      }
      catch (Exception cleanupFailure)
      {
        // openBackend() registers the root container monitor last of all, and closeBackend()
        // deregisters it without a null check, so cleaning up after a failure before that throws a
        // NullPointerException of its own. The failure being cleaned up after is the one worth
        // reading.
        e.addSuppressed(cleanupFailure);
      }
      throw e;
    }
    return backend;
  }
  private PDBBackendCfg backendCfg(SortedSet<DN> baseDNs) throws ConfigException
  {
    final PDBBackendCfg cfg = mockCfg(PDBBackendCfg.class);
    when(cfg.dn()).thenReturn(DN.valueOf("ds-cfg-backend-id=" + BACKEND_ID + ",cn=Backends,cn=config"));
    when(cfg.getBackendId()).thenReturn(BACKEND_ID);
    when(cfg.getDBDirectory()).thenReturn(BACKEND_ID);
    when(cfg.getDBDirectoryPermissions()).thenReturn("755");
    when(cfg.getDBCacheSize()).thenReturn(0L);
    when(cfg.getDBCachePercent()).thenReturn(20);
    when(cfg.getBaseDN()).thenReturn(baseDNs);
    when(cfg.listBackendIndexes()).thenReturn(new String[] { "cn" });
    when(cfg.listBackendVLVIndexes()).thenReturn(new String[0]);
    final BackendIndexCfg indexCfg = mock(BackendIndexCfg.class);
    when(indexCfg.getIndexType()).thenReturn(newTreeSet(IndexType.EQUALITY));
    when(indexCfg.getAttribute()).thenReturn(cnType);
    when(indexCfg.getIndexEntryLimit()).thenReturn(4000);
    when(indexCfg.getSubstringLength()).thenReturn(6);
    when(cfg.getBackendIndex("cn")).thenReturn(indexCfg);
    return cfg;
  }
  /** A backend whose storage makes the next write operation conflict, and so be replayed. */
  private static final class ReplayingBackend extends BackendImpl<PDBBackendCfg>
  {
    private ReplayingStorage storage;
    /** The configuration the entry containers register their listeners with. */
    private PDBBackendCfg configuredWith;
    @Override
    protected Storage configureStorage(PDBBackendCfg cfg, ServerContext serverContext) throws ConfigException
    {
      storage = new ReplayingStorage(new PDBStorage(cfg, serverContext));
      return storage;
    }
  }
  /**
   * The base DNs a change asks for, which unregisters an entry container the first time it is asked
   * whether it holds one. {@link BackendImpl#applyConfigurationChange} copies the base DNs the root
   * container holds and then looks each of their entry containers up, and asks this set in between;
   * an importLDIF, a rebuildBackend, an exportLDIF or the backend being disabled closes the root
   * container in that window and unregisters every one of them. Done here rather than raced for, so
   * that the window is closed on the same thread every time.
   */
  private static final class UnregisteringWhenAsked extends TreeSet<DN>
  {
    private static final long serialVersionUID = 1L;
    private final transient RootContainer rootContainer;
    private final DN toUnregister;
    private boolean unregistered;
    UnregisteringWhenAsked(RootContainer rootContainer, DN toUnregister, SortedSet<DN> baseDNs)
    {
      super(baseDNs);
      this.rootContainer = rootContainer;
      this.toUnregister = toUnregister;
    }
    @Override
    public boolean contains(Object baseDN)
    {
      if (!unregistered)
      {
        unregistered = true;
        // Closed here because nothing else will: the root container closes the containers it
        // holds, and this one has just been taken out of it, with its configuration listeners
        // still registered.
        closeSilently(rootContainer.unregisterEntryContainer(toUnregister));
      }
      return super.contains(baseDN);
    }
  }
  /** A failure which no storage engine replays, unlike {@link RollbackException}. */
  private static final class UnreplayableFailure extends Exception
  {
    private static final long serialVersionUID = 1L;
  }
  /**
   * Decorates a {@link Storage} so that the next {@link Storage#write(WriteOperation)} conflicts a
   * given number of times before it is let through. The conflict is raised from within the single
   * {@code write} the delegate is asked for, so the delegate's own retry loop performs the replay.
   */
  private static final class ReplayingStorage implements Storage
  {
    /** Where the conflict is raised, which decides how much of the operation has run. */
    private enum ConflictPoint
    {
      /** As soon as the operation first touches the transaction, before it has changed anything. */
      FIRST_STORAGE_ACCESS,
      /** Once the operation has run to completion, as a conflict reported by {@code commit()}. */
      COMMIT,
      /** Once the operation has run to completion, as a failure which is not replayed at all. */
      NO_REPLAY,
      /**
       * Once the operation has committed, as a failure which is not replayed either: what an engine
       * whose tree deletions do not belong to the transaction leaves behind.
       */
      NO_REPLAY_AFTER_COMMIT
    }
    private final Storage delegate;
    private Runnable onListTrees;
    private ConflictPoint conflictPoint;
    private int conflictsLeft;
    private int attempts;
    private int writes;
    ReplayingStorage(Storage delegate)
    {
      this.delegate = delegate;
    }
    void conflictAtFirstStorageAccess(int conflicts)
    {
      arm(ConflictPoint.FIRST_STORAGE_ACCESS, conflicts);
    }
    void conflictAtCommit(int conflicts)
    {
      arm(ConflictPoint.COMMIT, conflicts);
    }
    void failWithoutReplay()
    {
      arm(ConflictPoint.NO_REPLAY, 1);
    }
    void failAfterCommit()
    {
      arm(ConflictPoint.NO_REPLAY_AFTER_COMMIT, 1);
    }
    private void arm(ConflictPoint where, int conflicts)
    {
      conflictPoint = where;
      conflictsLeft = conflicts;
      attempts = 0;
    }
    /** How many times the armed operation was run, the first attempt included. */
    int attempts()
    {
      return attempts;
    }
    /** How many write operations this storage was asked for, armed or not. */
    int writes()
    {
      return writes;
    }
    @Override
    public void write(final WriteOperation writeOperation) throws Exception
    {
      writes++;
      final ConflictPoint armed = conflictPoint;
      if (armed == null)
      {
        delegate.write(writeOperation);
        return;
      }
      conflictPoint = null;
      if (armed == ConflictPoint.NO_REPLAY_AFTER_COMMIT)
      {
        // Committed, then reported as a failure: the operation's work outlives the failure, as it
        // does where the storage engine does not roll a tree deletion back.
        delegate.write(new WriteOperation()
        {
          @Override
          public void run(WriteableTransaction txn) throws Exception
          {
            attempts++;
            writeOperation.run(txn);
          }
        });
        throw new UnreplayableFailure();
      }
      // A single call, so that the replay is the delegate's own and keeps whatever the delegate
      // holds for the duration of a write, rather than starting afresh as a second call would.
      delegate.write(new WriteOperation()
      {
        @Override
        public void run(WriteableTransaction txn) throws Exception
        {
          attempts++;
          if (conflictsLeft-- <= 0)
          {
            writeOperation.run(txn);
            return;
          }
          if (armed == ConflictPoint.FIRST_STORAGE_ACCESS)
          {
            writeOperation.run(new ConflictingTransaction());
            return;
          }
          writeOperation.run(txn);
          if (armed == ConflictPoint.NO_REPLAY)
          {
            throw new UnreplayableFailure();
          }
          throw new RollbackException();
        }
      });
    }
    @Override
    public Importer startImport() throws ConfigException
    {
      return delegate.startImport();
    }
    @Override
    public void open(AccessMode accessMode) throws Exception
    {
      delegate.open(accessMode);
    }
    @Override
    public <T> T read(ReadOperation<T> readOperation) throws Exception
    {
      return delegate.read(readOperation);
    }
    @Override
    public void removeStorageFiles()
    {
      delegate.removeStorageFiles();
    }
    @Override
    public StorageStatus getStorageStatus()
    {
      return delegate.getStorageStatus();
    }
    @Override
    public boolean supportsBackupAndRestore()
    {
      return delegate.supportsBackupAndRestore();
    }
    @Override
    public void createBackup(BackupConfig backupConfig) throws DirectoryException
    {
      delegate.createBackup(backupConfig);
    }
    @Override
    public void removeBackup(BackupDirectory backupDirectory, String backupID) throws DirectoryException
    {
      delegate.removeBackup(backupDirectory, backupID);
    }
    @Override
    public void restoreBackup(RestoreConfig restoreConfig) throws DirectoryException
    {
      delegate.restoreBackup(restoreConfig);
    }
    /** Run whenever the trees which survived a failure are listed, and only then. */
    void onListTrees(Runnable probe)
    {
      this.onListTrees = probe;
    }
    @Override
    public Set<TreeName> listTrees()
    {
      if (onListTrees != null)
      {
        onListTrees.run();
      }
      return delegate.listTrees();
    }
    @Override
    public void close()
    {
      delegate.close();
    }
  }
  /** A transaction which conflicts as soon as it is used, without ever reaching the storage. */
  private static final class ConflictingTransaction implements WriteableTransaction
  {
    private static RollbackException conflict()
    {
      return new RollbackException();
    }
    @Override
    public void openTree(TreeName name, boolean createOnDemand)
    {
      throw conflict();
    }
    @Override
    public void deleteTree(TreeName name)
    {
      throw conflict();
    }
    @Override
    public void put(TreeName treeName, ByteSequence key, ByteSequence value)
    {
      throw conflict();
    }
    @Override
    public boolean update(TreeName treeName, ByteSequence key, UpdateFunction f)
    {
      throw conflict();
    }
    @Override
    public boolean delete(TreeName treeName, ByteSequence key)
    {
      throw conflict();
    }
    @Override
    public ByteString read(TreeName treeName, ByteSequence key)
    {
      throw conflict();
    }
    @Override
    public Cursor<ByteString, ByteString> openCursor(TreeName treeName)
    {
      throw conflict();
    }
    @Override
    public long getRecordCount(TreeName treeName)
    {
      throw conflict();
    }
    @Override
    public boolean treeExists(TreeName treeName)
    {
      throw conflict();
    }
  }
}
opendj-server-legacy/src/test/java/org/opends/server/plugins/ShortCircuitPlugin.java
@@ -23,10 +23,14 @@
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.server.ConfigException;
@@ -230,6 +234,24 @@
  /** {@inheritDoc} */
  @Override
  public void finalizePlugin()
  {
    /*
     * A park which outlives the test which took it holds a replay thread of this server,
     * and every replayed operation queued behind it, for as long as this plugin is
     * loaded: the map is static and nothing but the test itself removes an entry from it.
     */
    for (ParkedReplay park : parks.values())
    {
      park.deregister();
    }
    parks.clear();
  }
  /** {@inheritDoc} */
  @Override
  public PluginResult.PreParse
         doPreParse(PreParseAbandonOperation abandonOperation)
  {
@@ -616,7 +638,7 @@
    }
    // Check for registered short circuits.
    final String key = operation.getOperationType() + "/" + section.toLowerCase();
    final String key = keyFor(operation.getOperationType(), section);
    Integer resultCode = shortCircuits.get(key);
    if (resultCode != null)
    {
@@ -630,6 +652,26 @@
      // operations are let through, which is how a transient failure is simulated.
    }
    /*
     * A parked replay is held here, which is inside the run() of the operation and before
     * anything of the backend was taken: the thread which is replaying a change sits on
     * this monitor while it still owns that change, which is what lets a test act on the
     * thread rather than race it. It is consulted last, so that a park never takes an
     * operation away from a control or from a registered short circuit.
     */
    if (operation.isSynchronizationOperation())
    {
      final ParkedReplay park = parks.get(key);
      if (park != null && park.parks(operation))
      {
        final int parkResultCode = park.hold();
        if (parkResultCode >= 0)
        {
          return parkResultCode;
        }
      }
    }
    // If we've gotten here, then we shouldn't short-circuit the operation
    // processing.
    return -1;
@@ -688,7 +730,7 @@
   */
  public static int getShortCircuitCount(OperationType operation, String section)
  {
    final AtomicInteger count = shortCircuitCounts.get(operation + "/" + section.toLowerCase());
    final AtomicInteger count = shortCircuitCounts.get(keyFor(operation, section));
    return count != null ? count.get() : 0;
  }
@@ -701,7 +743,7 @@
   */
  public static void registerShortCircuit(OperationType operation, String section, int resultCode)
  {
    final String key = operation + "/" + section.toLowerCase();
    final String key = keyFor(operation, section);
    // This registration applies to every operation, and it counts from zero: a limit or
    // a count left behind by a previous registration is not part of it.
    shortCircuitCounts.remove(key);
@@ -720,7 +762,7 @@
   */
  public static void registerShortCircuit(OperationType operation, String section, int resultCode, int maxTimes)
  {
    final String key = operation + "/" + section.toLowerCase();
    final String key = keyFor(operation, section);
    shortCircuitCounts.remove(key);
    shortCircuitLimits.put(key, maxTimes);
    shortCircuits.put(key, resultCode);
@@ -733,11 +775,310 @@
   */
  public static void deregisterShortCircuit(OperationType operation, String section)
  {
    final String key = operation + "/" + section.toLowerCase();
    final String key = keyFor(operation, section);
    shortCircuits.remove(key);
    shortCircuitLimits.remove(key);
    // The count belongs to the registration which is being removed: a test which counts
    // the operations it short circuits must not inherit the count of the previous one.
    shortCircuitCounts.remove(key);
  }
  /** Registered parks for the replayed operations, keyed like the short circuits. */
  private static final Map<String, ParkedReplay> parks = new ConcurrentHashMap<>();
  /**
   * Holds the replayed operations of one type where they are, one at a time, until the
   * test lets each of them go.
   * <p>
   * The hold is taken at a plugin point which runs inside {@code op.run()}, so the thread
   * which is replaying a change is stopped while it still owns that change: a test can
   * then do something to that thread - stop it, disable its domain - and know the change
   * is in flight rather than hope it is. Nothing of the backend has been taken at that
   * point, so a parked operation blocks the replay and nothing else.
   */
  public static final class ParkedReplay
  {
    /**
     * The value which lets the operation run rather than short circuit it.
     * <p>
     * {@code ResultCode.UNDEFINED} is registered on {@code -1} as well, so
     * {@code release(ResultCode.UNDEFINED.intValue())} lets the operation run instead of
     * making it report that code - the same hole {@code registerShortCircuit(-1)} has.
     * No caller has a use for it, and a park releases with a real result code or with
     * none at all.
     */
    private static final int LET_THROUGH = -1;
    /**
     * How long an operation is held before this park gives up on the test which took it.
     * <p>
     * It is far longer than any release a test waits for - the fixture itself waits a
     * minute for a park - and it exists for the test which never releases at all: a park
     * leaked by a method killed on a timeout would otherwise hold a replay thread of this
     * server, and every replayed operation queued behind it, for the life of the JVM.
     */
    private static final long MAX_HOLD_IN_MS = TimeUnit.MINUTES.toMillis(5);
    private final String key;
    /** Which of the replayed operations of that type this park is for. */
    private final Predicate<PluginOperation> parked;
    private final Object lock = new Object();
    /** Whether an operation is parked right now. */
    private boolean occupied;
    /**
     * The thread of the operation which parked last. It is never cleared, so that a test
     * which waited for a park is handed the thread of that park even when the operation
     * has left the park since - a park which is let go of by {@link #deregister()}, or by
     * the thread it holds being interrupted, would otherwise hand out no thread at all
     * and have an assertion on which thread replays the change pass without asserting it.
     */
    private Thread lastParkedThread;
    /** How many operations were parked, which is what tells one park from the next. */
    private int parkedOperations;
    /** How many of them the test has waited for already. */
    private int awaitedOperations;
    private boolean released;
    private int releasedResultCode;
    private boolean deregistered;
    private ParkedReplay(String key, Predicate<PluginOperation> parked)
    {
      this.key = key;
      this.parked = parked;
    }
    /** Returns whether the provided operation is one this park is for. */
    private boolean parks(PluginOperation operation)
    {
      return parked.test(operation);
    }
    /**
     * Parks the calling operation until the test releases it. Runs on the thread which is
     * replaying the change.
     *
     * @return the result code the operation must be short circuited with, or a negative
     *         value to let it run
     */
    private int hold()
    {
      final long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(MAX_HOLD_IN_MS);
      synchronized (lock)
      {
        // One operation at a time, so that a release belongs to the operation the test
        // waited for rather than to whichever of them the scheduler let in first.
        while (occupied && !deregistered)
        {
          if (!waitOnLock(deadline))
          {
            return LET_THROUGH;
          }
        }
        if (deregistered)
        {
          return LET_THROUGH;
        }
        occupied = true;
        lastParkedThread = Thread.currentThread();
        parkedOperations++;
        released = false;
        lock.notifyAll();
        try
        {
          while (!released && !deregistered)
          {
            if (!waitOnLock(deadline))
            {
              return LET_THROUGH;
            }
          }
          return released ? releasedResultCode : LET_THROUGH;
        }
        finally
        {
          occupied = false;
          lock.notifyAll();
        }
      }
    }
    /**
     * Waits on the monitor until the provided deadline, reporting whether waiting can go
     * on. A deadline which has passed gives up on this park altogether rather than only
     * on the operation which reached it: the operations behind it would each pay the
     * whole wait again otherwise.
     */
    private boolean waitOnLock(long deadlineInNanos)
    {
      final long leftInNanos = deadlineInNanos - System.nanoTime();
      if (leftInNanos <= 0)
      {
        giveUpOnTheTest();
        return false;
      }
      try
      {
        // Rounded up, so that a budget shorter than a millisecond is still waited out
        // rather than truncated to a wait with no timeout at all.
        lock.wait(TimeUnit.NANOSECONDS.toMillis(leftInNanos + 999999L));
        return true;
      }
      catch (InterruptedException e)
      {
        // Whatever wants this thread to stop wins over the park: let the operation run
        // rather than hold a thread which is being taken down.
        Thread.currentThread().interrupt();
        return false;
      }
    }
    /** Stops parking anything and says so, after a test held an operation for too long. */
    private void giveUpOnTheTest()
    {
      System.err.println("***** ERROR:  a replayed operation was parked on " + key
          + " for " + MAX_HOLD_IN_MS + " ms and was never released:  the test which took"
          + " this park left it behind.  Letting the operation run and parking no more.");
      deregister();
    }
    /**
     * Waits for a replayed operation which was not waited for yet to be parked, and
     * reports which thread is replaying it. The operations are parked one at a time, so
     * that thread is the one which was parked when this returns; the thread of the last
     * park is reported when several of them were let go of without being waited for.
     *
     * @param timeout how long to wait for it
     * @param unit the unit of the timeout
     * @return the thread which is replaying the parked operation
     * @throws InterruptedException if this thread is interrupted while waiting
     * @throws TimeoutException if no operation was parked in time
     * @throws IllegalStateException if this park is gone, so that nothing can be parked
     *           on it any more
     */
    public Thread awaitParked(long timeout, TimeUnit unit)
        throws InterruptedException, TimeoutException
    {
      final long deadline = System.nanoTime() + unit.toNanos(timeout);
      synchronized (lock)
      {
        while (parkedOperations <= awaitedOperations)
        {
          if (deregistered)
          {
            // Waiting out the budget here would report a timeout naming the operations
            // which never parked, rather than the park which cannot park them any more.
            throw new IllegalStateException("the park on " + key + " is gone - it was"
                + " deregistered, or displaced by another park of the same operations -"
                + " so no replayed operation will be parked on it again");
          }
          final long leftInNanos = deadline - System.nanoTime();
          if (leftInNanos <= 0)
          {
            throw new TimeoutException("no replayed operation was parked on " + key
                + " within " + timeout + " " + unit);
          }
          // Rounded up, so that a budget shorter than a millisecond is still waited out
          // rather than truncated to a wait with no timeout at all.
          lock.wait(TimeUnit.NANOSECONDS.toMillis(leftInNanos + 999999L));
        }
        awaitedOperations = parkedOperations;
        return lastParkedThread;
      }
    }
    /**
     * Lets the parked operation run. Valid once {@link #awaitParked} has reported that
     * operation: see there for what a release which arrives before it costs.
     */
    public void release()
    {
      release(LET_THROUGH);
    }
    /**
     * Lets the parked operation go, short circuiting it with the provided result code.
     * <p>
     * Valid once {@link #awaitParked} has reported the operation being released. A
     * release which arrives before an operation is parked is wiped by the park it was
     * meant for - a park starts out unreleased - and that operation then waits for a
     * release which has already been spent.
     *
     * @param resultCode the result code the operation must report
     */
    public void release(int resultCode)
    {
      synchronized (lock)
      {
        if (!occupied)
        {
          throw new IllegalStateException("nothing is parked on " + key + " to release:"
              + " a release is spent by the park it arrives before, and the operation"
              + " which parks next then waits for one which has already been given");
        }
        released = true;
        releasedResultCode = resultCode;
        lock.notifyAll();
      }
    }
    /**
     * Stops parking the replayed operations and lets go of the one which is parked, if
     * any. A test must call this however it ends, or it leaves a replay thread of this
     * server parked for good.
     */
    public void deregister()
    {
      parks.remove(key, this);
      synchronized (lock)
      {
        deregistered = true;
        lock.notifyAll();
      }
    }
  }
  /**
   * Parks the replayed operations of the given type at the given plugin point, until the
   * test releases each of them.
   *
   * @param operation the type of operation to park
   * @param section the plugin point to park them at, which can only be {@code PreParse}
   * @param parked which of them to park - the change a test acts on rather than whatever
   *          of that type reaches this point first, which is somebody else's change as
   *          soon as more than one of them is in flight
   * @return the park, which the test must {@link ParkedReplay#deregister()} when it is
   *         done with it
   * @throws IllegalArgumentException if asked for any plugin point but {@code PreParse}
   */
  public static ParkedReplay parkReplayedOperations(
      OperationType operation, String section, Predicate<PluginOperation> parked)
  {
    if (!"PreParse".equalsIgnoreCase(section))
    {
      /*
       * The pre-operation plugins are not invoked for synchronization operations at all,
       * so a park anywhere else is never reached: the test which took it would wait out
       * its whole budget for an operation which cannot park, and be told that none did
       * rather than that none could.
       */
      throw new IllegalArgumentException("replayed operations can only be parked at"
          + " PreParse, which is the only plugin point they reach, not at " + section);
    }
    final String key = keyFor(operation, section);
    final ParkedReplay park = new ParkedReplay(key, parked);
    final ParkedReplay previous = parks.put(key, park);
    if (previous != null)
    {
      // A park a test left behind holds a replay thread of this server for good once the
      // map stops pointing at it: let go of it rather than lose the last reference to it.
      previous.deregister();
    }
    return park;
  }
  /** Returns the key a short circuit or a park of the given operations is kept under. */
  private static String keyFor(OperationType operation, String section)
  {
    return operation + "/" + section.toLowerCase(Locale.ROOT);
  }
}
opendj-server-legacy/src/test/java/org/opends/server/replication/ReplicationTestCase.java
@@ -102,6 +102,16 @@
  /** Generation id for a fully empty domain. */
  public static final long EMPTY_DN_GENID = GenerationIdChecksum.EMPTY_BACKEND_GENERATION_ID;
  /** The group a replication server and a replication domain are in unless told otherwise. */
  protected static final int DEFAULT_GROUP_ID = 1;
  /**
   * The group of a broker which is in none. Assured replication does not cross group ids,
   * so such a broker is never waited for and never waits: it is all a broker which only
   * publishes and reads updates needs.
   */
  private static final int NO_GROUP_ID = -1;
  /** How many times {@link #assertMonitorAttrValueStays} reads a value by default. */
  private static final int MONITOR_ATTR_SAMPLES = 5;
@@ -241,7 +251,42 @@
      int serverId, int windowSize, int port, int timeout,
      long generationId) throws Exception
  {
    final DomainFakeCfg config = newFakeCfg(baseDN, serverId, port);
    return openReplicationSession(
        newFakeCfg(baseDN, serverId, port), windowSize, timeout, generationId);
  }
  /**
   * Open a session to the local ReplicationServer which takes part in assured replication.
   * <p>
   * Assured replication does not cross group ids, so a broker whose updates are to be
   * acknowledged by the replicas of this server has to be in the group of the replication
   * server: an update published by a broker of another group is acknowledged on the spot,
   * by the replication server itself, and says nothing about what any replica did with it.
   * <p>
   * The group cuts both ways, and this broker does not acknowledge anything: the
   * replication server expects an ack from every replica of its group whatever that
   * replica is configured for, so a SAFE_READ update published by anyone else while this
   * broker is connected waits out the {@code assured-timeout} of the server. Publish the
   * assured updates from this broker, and open only one of them.
   *
   * @param baseDN the suffix the session is opened for
   * @param serverId the id this broker takes
   * @param windowSize the window size of the session
   * @param port the port of the local replication server
   * @param timeout the read timeout of the session, or 0 for none
   * @return the connected broker
   * @throws Exception if the session could not be opened
   */
  protected ReplicationBroker openAssuredReplicationSession(final DN baseDN,
      int serverId, int windowSize, int port, int timeout) throws Exception
  {
    return openReplicationSession(newFakeCfg(baseDN, serverId, port, DEFAULT_GROUP_ID),
        windowSize, timeout, getGenerationId(baseDN));
  }
  private ReplicationBroker openReplicationSession(final DomainFakeCfg config,
      int windowSize, int timeout, long generationId) throws Exception
  {
    config.setWindowSize(windowSize);
    final ReplicationBroker broker = new ReplicationBroker(
@@ -253,7 +298,13 @@
  protected DomainFakeCfg newFakeCfg(final DN baseDN, int serverId, int port)
  {
    DomainFakeCfg fakeCfg = new DomainFakeCfg(baseDN, serverId, newTreeSet("127.0.0.1:" + port));
    return newFakeCfg(baseDN, serverId, port, NO_GROUP_ID);
  }
  protected DomainFakeCfg newFakeCfg(final DN baseDN, int serverId, int port, int groupId)
  {
    DomainFakeCfg fakeCfg =
        new DomainFakeCfg(baseDN, serverId, newTreeSet("127.0.0.1:" + port), groupId);
    fakeCfg.setHeartbeatInterval(100000);
    fakeCfg.setChangetimeHeartbeatInterval(500);
    return fakeCfg;
opendj-server-legacy/src/test/java/org/opends/server/replication/UpdateOperationTest.java
@@ -22,6 +22,7 @@
import static org.forgerock.opendj.ldap.ModificationType.*;
import static org.forgerock.opendj.ldap.requests.Requests.*;
import static org.forgerock.opendj.ldap.schema.CoreSchema.*;
import static org.mockito.Mockito.*;
import static org.opends.server.TestCaseUtils.*;
import static org.opends.server.protocols.internal.InternalClientConnection.*;
import static org.opends.server.replication.plugin.LDAPReplicationDomain.*;
@@ -32,6 +33,8 @@
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import org.assertj.core.api.Assertions;
import org.forgerock.i18n.LocalizableMessage;
@@ -44,6 +47,7 @@
import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.forgerock.opendj.ldap.schema.AttributeType;
import org.forgerock.opendj.server.config.server.ReplicationSynchronizationProviderCfg;
import org.opends.server.TestCaseUtils;
import org.opends.server.core.AddOperation;
import org.opends.server.core.DeleteOperation;
@@ -52,11 +56,14 @@
import org.opends.server.core.ModifyOperationBasis;
import org.opends.server.extensions.DummyAlertHandler;
import org.opends.server.plugins.ShortCircuitPlugin;
import org.opends.server.plugins.ShortCircuitPlugin.ParkedReplay;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.replication.common.AssuredMode;
import org.opends.server.replication.common.CSN;
import org.opends.server.replication.common.CSNGenerator;
import org.opends.server.replication.plugin.LDAPReplicationDomain;
import org.opends.server.replication.plugin.MultimasterReplication;
import org.opends.server.replication.protocol.AckMsg;
import org.opends.server.replication.protocol.AddMsg;
import org.opends.server.replication.protocol.DeleteMsg;
import org.opends.server.replication.protocol.HeartbeatThread;
@@ -157,7 +164,15 @@
        + "cn: Replication Server\n"
        + "ds-cfg-replication-port: " + replServerPort + "\n"
        + "ds-cfg-replication-db-directory: UpdateOperationTest\n"
        + "ds-cfg-replication-server-id: 107\n";
        + "ds-cfg-replication-server-id: 107\n"
        /*
         * Long enough for a delivery which a test stops on its way through the replay:
         * the acks of an assured update are waited for from the moment it is published,
         * and the default second is spent long before a test which parks that delivery
         * has let go of it. Nothing waits it out - no test here leaves an assured update
         * unacknowledged - so it only bounds a failure.
         */
        + "ds-cfg-assured-timeout: 120000ms\n";
    // suffix synchronized
    String testName = "updateOperationTest";
@@ -1516,6 +1531,447 @@
  }
  /**
   * Test case for [Issue 909]: a replay thread which is stopped while it holds a change -
   * the number of replay threads is changed on a live server - must hand that change back
   * to the replication server instead of leaving it listed as owned by a thread which is
   * gone.
   * <p>
   * The change is parked inside the operation it is replayed by, so the thread is caught
   * while it still owns it rather than raced for: a change which is released by the
   * ordinary recovery instead ends the same way - delivered again and applied - so a test
   * which only watched the end state would pass whether or not the hand-back happened.
   * <p>
   * What tells them apart is which thread replays the change next. The thread which held
   * it is gone, and the change is replayed by one of the threads which replaced it, so
   * the delivery it is replayed from can only be a new one: an attempt which the same
   * thread made again would be the retry in place, and a change nobody handed back is
   * never delivered again at all - it stays listed as owned by a thread which is gone,
   * with the ServerState of this domain stopped behind it for good.
   */
  @Test
  public void aChangeAStoppedReplayThreadHeldIsGivenBackAndDeliveredAgain() throws Exception
  {
    testSetUp("aChangeAStoppedReplayThreadHeldIsGivenBackAndDeliveredAgain");
    logger.error(LocalizableMessage.raw(
        "Starting replication test : aChangeAStoppedReplayThreadHeldIsGivenBackAndDeliveredAgain"));
    final int serverId = 14;
    /*
     * In the group of the replication server, so that the delete published below is one
     * this domain has to acknowledge: an assured update from a broker of another group is
     * acknowledged by the replication server itself, and says nothing about the replay.
     */
    ReplicationBroker broker =
        openAssuredReplicationSession(baseDN, serverId, 100, replServerPort, 1000);
    try
    {
      CSNGenerator gen = new CSNGenerator(serverId, 0);
      Entry tmp = TestCaseUtils.addEntry(
          "dn: uid=user.909," + baseDN,
          "objectClass: top",
          "objectClass: person",
          "objectClass: organizationalPerson",
          "objectClass: inetOrgPerson",
          "uid: user.909",
          "cn: Aaccf Amar",
          "sn: Amar");
      String uuid = getEntry(tmp.getName(), 1, true).parseAttribute("entryuuid").asString();
      final LDAPReplicationDomain domain = MultimasterReplication.findDomain(baseDN, null);
      domain.resetUnreplayedChangeAlertThrottle();
      final int initialAlerts = DummyAlertHandler.getAlertCount(ALERT_TYPE_REPLICATION_UNREPLAYED_CHANGE);
      /*
       * Only the counters of the replay are read: a session restart takes this domain
       * through NOT_CONNECTED, which resets every monitoring counter of the replication
       * service - the updates it received and processed, and the assured acks it sent -
       * and handing a change back is a session restart.
       */
      final long initialApplied = getMonitorAttrValue(baseDN, "replayed-updates-ok");
      final long initialFailures = getMonitorAttrValue(baseDN, "replayed-updates-failed");
      /*
       * Hold the replayed deletes where they are. The park is taken at the pre-parse
       * plugin point, which runs inside op.run() and before anything of the backend was
       * taken: the replay thread stops there while it still owns the change, and the
       * pre-operation plugins are not invoked for synchronization operations at all.
       */
      final CSN csn = gen.newCSN();
      final ParkedReplay parked = ShortCircuitPlugin.parkReplayedOperations(
          OperationType.DELETE, "PreParse", op -> csn.equals(OperationContext.getCSN(op)));
      final AtomicReference<Throwable> reconfigurationFailure = new AtomicReference<>();
      Thread reconfiguration = null;
      boolean reconfigurationFinished = true;
      try
      {
        final DeleteMsg delete = new DeleteMsg(tmp.getName(), csn, uuid);
        /*
         * Published assured in SAFE_READ mode, so that the delivery which is abandoned
         * has to say what it did. The ack is the one thing the counters cannot report
         * afterwards - the session restart the hand-back performs resets every one of
         * them - so it is read off this broker rather than counted.
         */
        delete.setAssured(true);
        delete.setAssuredMode(AssuredMode.SAFE_READ_MODE);
        broker.publish(delete);
        // A replay thread now owns the change and is stopped inside its operation.
        final Thread abandoningThread = parked.awaitParked(60, SECONDS);
        /*
         * Change the number of replay threads while that thread holds the change. It runs
         * on a thread of its own because stopping the replay threads joins them: it can
         * not return before the parked thread is let go, which is exactly the ordering
         * this test is about.
         */
        reconfiguration = startReplayThreadReconfiguration(2, reconfigurationFailure);
        awaitStoppingTheReplayThreads(reconfiguration, reconfigurationFailure);
        /*
         * The parked thread has been asked to stop by now, so its attempt comes back on a
         * storage which did not serve the operation - which is retried in place - and the
         * attempt which follows is where it finds out that it is going away and gives the
         * change back instead.
         */
        parked.release(ResultCode.UNAVAILABLE.intValue());
        /*
         * The change was given back, so the replication server owns it again and delivers
         * it once more. This second park is where the state of the domain is read: the
         * change is held inside its operation, so nothing can be recording it while the
         * assertions below run.
         */
        final Thread replayingThread =
            awaitParkedOrReportReconfigurationFailure(parked, reconfigurationFailure);
        reconfiguration.join(SECONDS.toMillis(60));
        assertFalse(reconfiguration.isAlive(),
            "the replay threads were reconfigured, but applyConfigurationChange never returned");
        if (reconfigurationFailure.get() != null)
        {
          throw new AssertionError("the replay threads could not be reconfigured",
              reconfigurationFailure.get());
        }
        /*
         * The change is being replayed by another thread, and the thread which held it is
         * gone: it gave the change back on its way out rather than take it with it. An
         * attempt made by the thread which held it would be the retry in place instead,
         * and a change which was never handed back is not replayed by anyone.
         */
        assertFalse(abandoningThread.isAlive(),
            "the replay thread which held the change must have stopped");
        Assertions.assertThat(replayingThread)
            .as("the change must be replayed by a thread which did not hold it")
            .isNotSameAs(abandoningThread);
        assertFalse(domain.getServerState().cover(csn),
            "a change a stopped replay thread never applied must not be in the ServerState");
        assertEquals(getMonitorAttrValue(baseDN, "replayed-updates-failed"), initialFailures,
            "a change which was handed back must not be counted as one this replica gave up on");
        assertNotNull(getEntry(tmp.getName(), 1, true),
            "the entry must not have been deleted by the delivery which was abandoned");
        /*
         * The delivery which was abandoned said what it did: an assured write which is
         * waiting on this replica must not be told that the change is in the data here,
         * because it is a change this replica is still asking for.
         *
         * What is read is the ack, not when it was published. The hand-back gives this
         * domain a new broker and the replication server waits for an ack by CSN rather
         * than by session, so an ack published after the hand-back reaches it all the
         * same: the order of the two is not what this asserts.
         */
        final AckMsg ack = awaitAck(broker, csn);
        assertTrue(ack.hasReplayError(),
            "the ack of the abandoned delivery must report the replay error rather than"
                + " be the plain ack a master would take for a durable write");
        assertFalse(ack.hasTimeout(),
            "the ack must be the one the abandoned delivery published, not the one the"
                + " replication server makes up when it gives up waiting for it");
        Assertions.assertThat(ack.getFailedServers())
            .as("the replica which abandoned the delivery must be the one it names")
            .containsExactly(domainSid);
        /*
         * Let the delivery which took over apply the change, and stop parking: an attempt
         * of that delivery which came back on a lock it could not take would be parked
         * again otherwise, with nothing left to release it.
         */
        parked.deregister();
        assertMonitorAttrValueEventually(baseDN, "replayed-updates-ok", initialApplied + 1,
            "the change must be applied by the delivery which took over from the abandoned one");
        /*
         * A change applied twice goes through the expected count on its way up, so the
         * value has to be seen to stay put rather than to be reached once - and for
         * longer than the session restart which would bring that second delivery, or the
         * assertion stops looking before what it is looking for could arrive.
         */
        assertMonitorAttrValueStays(baseDN, "replayed-updates-ok", initialApplied + 1,
            MONITOR_ATTR_SAMPLES_ACROSS_A_REDELIVERY,
            "the change must be applied exactly once");
        assertNull(DirectoryServer.getEntry(tmp.getName()), "the entry must have been deleted");
        /*
         * Abandoning a change is not giving up on it: the change is applied moments later,
         * so nothing is counted as failed and the administrator is not told that this
         * replica diverges.
         */
        assertMonitorAttrValueStays(baseDN, "replayed-updates-failed", initialFailures,
            MONITOR_ATTR_SAMPLES_ACROSS_A_REDELIVERY,
            "a change which was handed back must not be counted as one this replica gave up on");
        assertEquals(DummyAlertHandler.getAlertCount(ALERT_TYPE_REPLICATION_UNREPLAYED_CHANGE),
            initialAlerts,
            "a change which was handed back must not have this replica report a divergence");
      }
      finally
      {
        /*
         * Whatever happened above: no replay thread of this server may be left parked,
         * and the reconfiguration has to be over before the next one is started. The two
         * would otherwise race for the pool of replay threads, which belongs to the
         * server rather than to this test, and the one which creates its threads last
         * drops the ones the other had just started without ever stopping them.
         */
        parked.deregister();
        if (reconfiguration != null)
        {
          reconfiguration.join(SECONDS.toMillis(60));
          reconfigurationFinished = !reconfiguration.isAlive();
        }
        if (reconfigurationFinished)
        {
          setNumberOfReplayThreads(null);
        }
      }
      /*
       * Read here rather than asserted while cleaning up, where a failure would replace
       * the one the test was reporting. join() with a timeout returns the same whether
       * the thread is over or not, and the pool of replay threads belongs to the server:
       * a reconfiguration still inside stopReplayThreads() holds the monitor of
       * MultimasterReplication, which restoring the pool would wait on for as long as an
       * untimed join() takes, and one caught between stopping and creating orphans the
       * threads the restore had just started. So the pool is left alone and this says so.
       */
      assertTrue(reconfigurationFinished,
          "the replay thread reconfiguration never finished; the pool was left alone");
    }
    finally
    {
      broker.stop();
    }
  }
  /**
   * Starts changing the number of replay threads of this server on a thread of its own.
   * <p>
   * It cannot run on the thread of the test: stopping the replay threads joins them, so it
   * does not return while one of them is parked in a change, which is the whole point of
   * this fixture.
   *
   * @param replayThreads how many replay threads the server must run, or {@code null} for
   *          as many as it computes on its own
   * @param failure where the reconfiguration reports what it ran into, if anything
   * @return the thread which is doing the reconfiguration
   */
  private static Thread startReplayThreadReconfiguration(
      final Integer replayThreads, final AtomicReference<Throwable> failure)
  {
    final Thread reconfiguration = new Thread(new Runnable()
    {
      @Override
      public void run()
      {
        try
        {
          setNumberOfReplayThreads(replayThreads);
        }
        catch (Throwable t)
        {
          failure.set(t);
        }
      }
    }, "replay thread reconfiguration");
    // A reconfiguration which never returns holds the monitor of MultimasterReplication:
    // let the fork end on it rather than have it kept alive by a thread of this test.
    reconfiguration.setDaemon(true);
    reconfiguration.start();
    return reconfiguration;
  }
  /**
   * Reads the acknowledgement of the provided change off the broker it was published on.
   * <p>
   * The messages which come first are discarded: this broker is told about everything the
   * replication server has for it, and the ack of one change is what is being looked for.
   *
   * @param broker the broker the change was published on
   * @param csn the change the ack is expected for
   * @return the ack of that change
   * @throws Exception if it never arrived
   */
  private static AckMsg awaitAck(final ReplicationBroker broker, final CSN csn) throws Exception
  {
    final long deadline = System.nanoTime() + SECONDS.toNanos(60);
    while (deadline - System.nanoTime() > 0)
    {
      final ReplicationMsg msg;
      try
      {
        msg = broker.receive();
      }
      catch (SocketTimeoutException e)
      {
        // The broker reads under a timeout of its own, which is far shorter than the
        // budget here: a quiet second is not an answer.
        continue;
      }
      if (msg == null)
      {
        // The broker stopped rather than timed out: there is nothing left to read from,
        // and reading it again would spin a core for the rest of the budget.
        throw new AssertionError("the session " + csn + " was published on is gone,"
            + " so the ack of that change can no longer arrive");
      }
      if (msg instanceof AckMsg && csn.equals(((AckMsg) msg).getCSN()))
      {
        return (AckMsg) msg;
      }
    }
    throw new AssertionError("the delivery of " + csn + " was never acknowledged");
  }
  /**
   * Waits for the delivery which took over from the abandoned one to be parked, reporting
   * what the reconfiguration ran into when that is why nothing was parked.
   * <p>
   * A reconfiguration which throws once it has stopped the replay threads leaves this
   * server with no replay thread at all: nothing can be parked then, and the timeout of
   * the wait would be reported in place of the failure which brought it about.
   *
   * @param parked the park the delivery is expected to be caught in
   * @param failure where the reconfiguration reports what it ran into, if anything
   * @return the thread which is replaying the parked operation
   * @throws Exception if no operation was parked in time
   */
  private static Thread awaitParkedOrReportReconfigurationFailure(
      final ParkedReplay parked, final AtomicReference<Throwable> failure) throws Exception
  {
    try
    {
      return parked.awaitParked(60, SECONDS);
    }
    catch (TimeoutException e)
    {
      final Throwable cause = failure.get();
      if (cause == null)
      {
        throw e;
      }
      final AssertionError error = new AssertionError(
          "the replay threads could not be reconfigured, so nothing was left to replay"
              + " the change which was handed back", cause);
      error.addSuppressed(e);
      throw error;
    }
  }
  /**
   * Changes the number of replay threads of this server, the way a change of
   * {@code num-update-replay-threads} does on a live server.
   *
   * @param replayThreads how many replay threads the server must run, or {@code null} for
   *          as many as it computes on its own
   */
  private static void setNumberOfReplayThreads(final Integer replayThreads)
  {
    final ReplicationSynchronizationProviderCfg cfg =
        mock(ReplicationSynchronizationProviderCfg.class);
    /*
     * An unstubbed mock hands out 0 replay threads and no connection timeout, and both
     * would outlive this test: the whole server shares one pool of replay threads. The
     * number of them is what this test is changing; the connection timeout is read back
     * from the running server, so that it is restored rather than restated.
     */
    when(cfg.getNumUpdateReplayThreads()).thenReturn(replayThreads);
    when(cfg.getConnectionTimeout())
        .thenReturn((long) MultimasterReplication.getConnectionTimeoutMS());
    multimasterReplication().applyConfigurationChange(cfg);
  }
  /** Returns the replication synchronization provider of the running server. */
  private static MultimasterReplication multimasterReplication()
  {
    // Read as an Object: the provider is declared with the configuration of its own type,
    // which is not the one the registry lists.
    for (Object provider : DirectoryServer.getSynchronizationProviders())
    {
      if (provider instanceof MultimasterReplication)
      {
        return (MultimasterReplication) provider;
      }
    }
    throw new AssertionError("this server runs no replication synchronization provider");
  }
  /**
   * Waits for the provided thread to be inside the {@code join()} of the replay threads it
   * is stopping.
   * <p>
   * Waiting for that, rather than for the thread to be started, is what puts the change in
   * the hands of a thread which has already been asked to stop: every replay thread is
   * asked to stop before the first of them is joined, so a thread which is joining has
   * asked the parked one. Where the thread waits is checked as well as that it waits: the
   * state on its own would be satisfied by any wait at all, including one taken before the
   * replay threads were asked to stop.
   *
   * @param reconfiguration the thread which is changing the number of replay threads
   * @param failure where that thread reports what it ran into, if anything
   * @throws Exception if it never reached the join
   */
  private static void awaitStoppingTheReplayThreads(
      final Thread reconfiguration, final AtomicReference<Throwable> failure) throws Exception
  {
    final long deadline = System.nanoTime() + SECONDS.toNanos(60);
    while (deadline - System.nanoTime() > 0)
    {
      if (reconfiguration.getState() == Thread.State.WAITING
          && isJoiningTheReplayThreads(reconfiguration))
      {
        return;
      }
      if (!reconfiguration.isAlive())
      {
        if (failure.get() != null)
        {
          throw new AssertionError(
              "the replay threads could not be reconfigured", failure.get());
        }
        fail("the replay threads were stopped without joining the one which holds a change");
      }
      // Sampling the stack of a running thread costs a handshake with it, so it is done
      // often enough to open the gate promptly and not so often as to slow the server
      // this test is watching: the thread it waits for is not going anywhere.
      Thread.sleep(10);
    }
    fail("the reconfiguration never reached the join() of the replay threads");
  }
  /** Returns whether the provided thread is waiting on the replay threads it stopped. */
  private static boolean isJoiningTheReplayThreads(final Thread reconfiguration)
  {
    for (final StackTraceElement frame : reconfiguration.getStackTrace())
    {
      if ("stopReplayThreads".equals(frame.getMethodName())
          && MultimasterReplication.class.getName().equals(frame.getClassName()))
      {
        return true;
      }
    }
    return false;
  }
  /**
   * Test case for [Issue 889]: every change which can not be replayed must be given up
   * on, not only the one which fails on its own.
   * <p>
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/IsServerFailureTest.java
New file
@@ -0,0 +1,242 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.replication.plugin;
import static org.assertj.core.api.Assertions.assertThat;
import static org.opends.server.replication.plugin.LDAPReplicationDomain.isServerFailure;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.assertj.core.api.SoftAssertions;
import org.forgerock.opendj.ldap.ResultCode;
import org.opends.server.DirectoryServerTestCase;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
/**
 * Tests which result codes {@link LDAPReplicationDomain} counts as a failure of the server
 * rather than as a change only conflict resolution can apply.
 * <p>
 * {@code ds-cfg-server-error-result-code} is a plain integer which is not validated as a
 * result code, so an administrator can set it to a code {@code solveNamingConflict()} owns.
 * The replay must still leave such a change to conflict resolution: a replica which took
 * every one of them for a failure of the server would retry them in place, hold its
 * ServerState back over them and give up on them once the give-up delay is spent - for
 * conflicts conflict resolution would have solved on the attempt it never got.
 * <p>
 * Only {@code UNWILLING_TO_PERFORM} has an end-to-end test of that rule, because each of the
 * other codes would need a scenario which fails with exactly that code and which nothing but
 * conflict resolution can apply - and {@code OBJECTCLASS_VIOLATION} has none a replayed
 * operation reaches through the server's own paths, the schema checks which raise it being
 * skipped for synchronization operations. Forcing a code with {@code ShortCircuitPlugin} does
 * not stand in for one: the operation applies for real once that budget is spent, so such a
 * test passes with the code removed from the set (#910, #938). So the rule is pinned here on
 * the predicate itself, with no server in it.
 * <p>
 * The codes below are written out rather than read from {@code CONFLICT_RESULT_CODES}: a data
 * provider fed by the set under test agrees with it whatever it holds, which is exactly what
 * leaves the set unguarded today. Every result code which can be configured is then swept
 * through the predicate against that list, so a code put into the set is as visible as one
 * taken out of it, and the set itself stays private - nothing here reads it.
 * <p>
 * What this can not see is the set drifting away from the branches of
 * {@code solveNamingConflict()} it is the union of: a code dropped from both at once leaves
 * the table below still describing the predicate correctly.
 * <p>
 * Nor is the predicate the whole of the rule at the call site: {@code replay()} answers
 * {@code SUCCESS} and {@code NO_OPERATION} before it consults this at all, so a change which
 * came back with one of them never reaches the table below whatever is configured - that is
 * issue #953. {@code BUSY} is answered ahead of it too, but only inside the retry loop: once
 * the in-place attempts are spent, the predicate is asked about it after all.
 */
@SuppressWarnings("javadoc")
public class IsServerFailureTest extends DirectoryServerTestCase
{
  /**
   * The result codes the four {@code solveNamingConflict()} overloads dispatch on. Not
   * everything they resolve: the ModifyDN overload answers {@code NOTHING_TO_DO} from three
   * early returns before it ever looks at the result code.
   */
  private static final List<ResultCode> CONFLICT_CODES = Arrays.asList(
      ResultCode.NO_SUCH_OBJECT,          // 32, all four overloads
      ResultCode.ENTRY_ALREADY_EXISTS,    // 68, the Add and ModifyDN overloads
      ResultCode.NOT_ALLOWED_ON_RDN,      // 67, the Modify overload
      ResultCode.NOT_ALLOWED_ON_NONLEAF,  // 66, the Delete overload
      ResultCode.UNWILLING_TO_PERFORM,    // 53, the ModifyDN overload
      ResultCode.OBJECTCLASS_VIOLATION);  // 65, the ModifyDN overload
  /**
   * Codes conflict resolution does not solve. {@code OTHER} is the default of
   * {@code ds-cfg-server-error-result-code}, and the property is an integer with no upper
   * limit, so a value no result code is named after is a setting like any other.
   * {@code UNAVAILABLE} is not here: it is a failure of the server whatever is configured,
   * which {@link #unavailableIsAServerFailureWhateverIsConfigured()} covers.
   */
  private static final List<ResultCode> CODES_OUTSIDE_THE_CONFLICT_SET = Arrays.asList(
      ResultCode.OTHER,                       // 80, the default
      ResultCode.CONSTRAINT_VIOLATION,        // 19
      ResultCode.INSUFFICIENT_ACCESS_RIGHTS,  // 50
      ResultCode.valueOf(9999));              // a value no result code is named after
  @DataProvider(name = "conflictResultCodes")
  public Object[][] conflictResultCodes()
  {
    return rowsOf(CONFLICT_CODES);
  }
  @DataProvider(name = "codesOutsideTheConflictSet")
  public Object[][] codesOutsideTheConflictSet()
  {
    return rowsOf(CODES_OUTSIDE_THE_CONFLICT_SET);
  }
  /**
   * Every result code an administrator could put in {@code ds-cfg-server-error-result-code}.
   * The property is an integer with {@code lower-limit="0"}, so {@code UNDEFINED} (-1) is not
   * one of them - it is the null object of {@link ResultCode} rather than a setting - and a
   * value no result code is named after is.
   */
  private static List<ResultCode> configurableCodes()
  {
    final List<ResultCode> codes = new ArrayList<>();
    for (ResultCode code : ResultCode.values())
    {
      if (code.intValue() >= 0)
      {
        codes.add(code);
      }
    }
    codes.add(ResultCode.valueOf(9999));
    return codes;
  }
  private static Object[][] rowsOf(List<ResultCode> codes)
  {
    final Object[][] rows = new Object[codes.size()][];
    for (int i = 0; i < codes.size(); i++)
    {
      rows[i] = new Object[] { codes.get(i) };
    }
    return rows;
  }
  /** A result which is neither {@code UNAVAILABLE} nor the configured code. */
  @DataProvider(name = "resultAndAnotherConfiguredCode")
  public Object[][] resultAndAnotherConfiguredCode()
  {
    return new Object[][] {
      // a conflict under the default setting: the carve-out for CONFLICT_RESULT_CODES does
      // not come into it, the result simply is not what this server puts on an internal error
      { ResultCode.NO_SUCH_OBJECT, ResultCode.OTHER },
      { ResultCode.ENTRY_ALREADY_EXISTS, ResultCode.OTHER },
      { ResultCode.NOT_ALLOWED_ON_RDN, ResultCode.OTHER },
      { ResultCode.NOT_ALLOWED_ON_NONLEAF, ResultCode.OTHER },
      { ResultCode.UNWILLING_TO_PERFORM, ResultCode.OTHER },
      { ResultCode.OBJECTCLASS_VIOLATION, ResultCode.OTHER },
      // a conflict code while a different conflict code is configured - the deployment
      // this whole class is written about, meeting a conflict it did not configure
      { ResultCode.NO_SUCH_OBJECT, ResultCode.UNWILLING_TO_PERFORM },
      // and a failure which is not the configured code is not this server's either
      { ResultCode.OTHER, ResultCode.NO_SUCH_OBJECT },
      { ResultCode.CONSTRAINT_VIOLATION, ResultCode.UNWILLING_TO_PERFORM },
      { ResultCode.INSUFFICIENT_ACCESS_RIGHTS, ResultCode.OTHER },
      { ResultCode.valueOf(9999), ResultCode.OTHER },
    };
  }
  /**
   * Sweeps every registered result code through the predicate against the list above. The
   * named tables below say what the rule is on the codes which matter; this says the set
   * behind it holds those and nothing else, so that a code put into
   * {@code CONFLICT_RESULT_CODES} - which stops a storage failure carrying it from being
   * retried - fails here as loudly as a code taken out of it.
   * <p>
   * It speaks for the predicate, not for what the replay hands it: {@code SUCCESS} and
   * {@code NO_OPERATION} never reach the method, and what it answers for them is what the
   * rest of this sweep is written from rather than a rule of its own.
   * <p>
   * Soft assertions so that a drift in both directions at once - a code added and another
   * dropped - is reported in one run rather than one code per run.
   */
  @Test
  public void everyRegisteredCodeIsAServerFailureExactlyWhenConflictResolutionDoesNotOwnIt()
  {
    final SoftAssertions softly = new SoftAssertions();
    for (ResultCode code : configurableCodes())
    {
      softly.assertThat(isServerFailure(code, code))
          .as("%s (%d) set as server-error-result-code is a failure of this server unless "
                  + "conflict resolution is the only thing which can solve it",
              code, code.intValue())
          .isEqualTo(!CONFLICT_CODES.contains(code));
    }
    softly.assertAll();
  }
  @Test(dataProvider = "conflictResultCodes")
  public void conflictCodeConfiguredAsTheServerErrorCodeIsLeftToConflictResolution(ResultCode code)
  {
    assertThat(isServerFailure(code, code))
        .as("%s (%d) set as server-error-result-code must not take a change away from "
                + "solveNamingConflict(), which is the only thing which can solve it",
            code, code.intValue())
        .isFalse();
  }
  @Test(dataProvider = "codesOutsideTheConflictSet")
  public void codeOutsideTheConflictSetConfiguredAsTheServerErrorCodeIsAServerFailure(ResultCode code)
  {
    assertThat(isServerFailure(code, code))
        .as("%s (%d) set as server-error-result-code is this server reporting an internal "
                + "error, and conflict resolution can not solve it",
            code, code.intValue())
        .isTrue();
  }
  /**
   * "Whatever is configured" is every code which can be configured, rather than a handful of
   * them: the predicate reads the configured code only after {@code UNAVAILABLE} has not
   * matched, so a change which stops that short circuit for some code has to be looked for
   * across all of them.
   */
  @Test
  public void unavailableIsAServerFailureWhateverIsConfigured()
  {
    final SoftAssertions softly = new SoftAssertions();
    for (ResultCode serverErrorResultCode : configurableCodes())
    {
      softly.assertThat(isServerFailure(ResultCode.UNAVAILABLE, serverErrorResultCode))
          .as("the backend being offline or rebuilt is a failure of the server while "
                  + "server-error-result-code is %s (%d) just as much as it is by default",
              serverErrorResultCode, serverErrorResultCode.intValue())
          .isTrue();
    }
    softly.assertAll();
  }
  @Test(dataProvider = "resultAndAnotherConfiguredCode")
  public void codeWhichIsNotTheConfiguredOneIsNotAServerFailure(
      ResultCode result, ResultCode serverErrorResultCode)
  {
    assertThat(isServerFailure(result, serverErrorResultCode))
        .as("%s (%d) is not what this server puts on an internal error - it puts %s (%d) - "
                + "so it is the operation which failed rather than the server",
            result, result.intValue(), serverErrorResultCode, serverErrorResultCode.intValue())
        .isFalse();
  }
}
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/PendingChangesTest.java
New file
@@ -0,0 +1,127 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.replication.plugin;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.*;
import org.mockito.ArgumentCaptor;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.replication.common.CSN;
import org.opends.server.replication.common.CSNGenerator;
import org.opends.server.replication.protocol.LDAPUpdateMsg;
import org.opends.server.replication.protocol.ReplicaOfflineMsg;
import org.opends.server.replication.protocol.UpdateMsg;
import org.opends.server.replication.service.ReplicationDomain;
import org.opends.server.types.operation.PluginOperation;
import org.testng.annotations.Test;
/**
 * Tests the bookkeeping a replica does on its own changes: they are published in the order of
 * their CSNs, and the announcement that the replica goes offline is only reported as sent when
 * it really was.
 * <p>
 * These tests need no server: the changes are built by a CSNGenerator, which reads the time
 * service, and the time service is up as soon as its class is loaded.
 */
@SuppressWarnings("javadoc")
@Test(groups = { "precommit", "replication" }, sequential = true)
public class PendingChangesTest extends DirectoryServerTestCase
{
  private static final int SERVER_ID = 42;
  @Test
  public void replicaOfflineMsgIsSentWhenNoChangeIsPending() throws Exception
  {
    final ReplicationDomain domain = mock(ReplicationDomain.class);
    final PendingChanges pendingChanges = newPendingChanges(domain);
    final CSN offlineCSN = pendingChanges.putReplicaOfflineMsg();
    assertNotNull(offlineCSN, "the message was published and must be reported as sent");
    final UpdateMsg published = onlyMsgPublishedBy(domain);
    assertTrue(published instanceof ReplicaOfflineMsg, "published " + published);
    assertEquals(published.getCSN(), offlineCSN);
  }
  /**
   * The message carries the newest CSN of the replica, so a change which is still in flight
   * holds it back - and what was never published must not be reported as sent: the shutdown of
   * a collocated replication server waits out the whole grace period of a message it was told
   * about and which never reaches the wire.
   */
  @Test
  public void replicaOfflineMsgQueuedBehindAnUncommittedChangeIsNotReportedAsSent() throws Exception
  {
    final ReplicationDomain domain = mock(ReplicationDomain.class);
    final PendingChanges pendingChanges = newPendingChanges(domain);
    pendingChanges.putLocalOperation(newLocalOperation());
    assertNull(pendingChanges.putReplicaOfflineMsg(), "nothing was published");
    verify(domain, never()).publish(any(UpdateMsg.class));
  }
  /**
   * A message which could not be published is given up on rather than left queued: the replica
   * which could not announce itself offline is either shutting down, and the message dies with
   * the process, or it is being disabled for an import or a configuration change - and once it
   * comes back, announcing it offline on the session which follows would be a lie.
   */
  @Test
  public void replicaOfflineMsgWhichCouldNotBeSentIsNotPublishedLater() throws Exception
  {
    final ReplicationDomain domain = mock(ReplicationDomain.class);
    final PendingChanges pendingChanges = newPendingChanges(domain);
    final CSN changeCSN = pendingChanges.putLocalOperation(newLocalOperation());
    assertNull(pendingChanges.putReplicaOfflineMsg(), "nothing was published");
    // The change which held the message back completes.
    pendingChanges.commitAndPushCommittedChanges(changeCSN, mock(LDAPUpdateMsg.class));
    final UpdateMsg published = onlyMsgPublishedBy(domain);
    assertTrue(published instanceof LDAPUpdateMsg, "published " + published);
  }
  private PendingChanges newPendingChanges(ReplicationDomain domain)
  {
    return new PendingChanges(new CSNGenerator(SERVER_ID, 0), domain);
  }
  /** A local operation, i.e. one this replica must publish to the other replicas. */
  private PluginOperation newLocalOperation()
  {
    final PluginOperation operation = mock(PluginOperation.class);
    when(operation.isSynchronizationOperation()).thenReturn(false);
    return operation;
  }
  /**
   * The single message the domain was asked to publish, failing the test if it published
   * anything else: a message which is not sent and a message which is sent twice are both the
   * kind of mistake these tests are about.
   */
  private UpdateMsg onlyMsgPublishedBy(ReplicationDomain domain)
  {
    final ArgumentCaptor<UpdateMsg> published = ArgumentCaptor.forClass(UpdateMsg.class);
    verify(domain).publish(published.capture());
    return published.getValue();
  }
}