| | |
| | | import java.sql.*; |
| | | import java.util.*; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import java.util.concurrent.Executor; |
| | | import java.util.concurrent.atomic.AtomicBoolean; |
| | | import java.util.function.Predicate; |
| | | |
| | | import static org.opends.server.backends.pluggable.spi.StorageUtils.addErrorMessage; |
| | |
| | | return ccr; |
| | | } |
| | | |
| | | ResultSet executeResultSet(PreparedStatement statement) throws SQLException { |
| | | /** |
| | | * What a statement of this backend may legitimately take, and the property bounding it. One |
| | | * value cannot serve both: an entry read is a single row of an index, while the count of a |
| | | * tree and the delete that empties one before an import are a scan and a rewrite of a whole |
| | | * table, which take minutes on a populated backend and are not a symptom of anything. |
| | | */ |
| | | enum StatementBound { |
| | | /** one row by primary key, or one batch of a cursor along its index */ |
| | | OPERATION("org.openidentityplatform.opendj.jdbc.query.timeout", 120), |
| | | /** |
| | | * a whole table at once: count(*), the delete of clearTree, the scan behind the highest |
| | | * entry id, create index, drop table, and every batch of a cursor walking a tree whole. |
| | | * This class ships <em>unbounded</em>: what such a statement legitimately takes follows the |
| | | * size of the backend and the speed of its database, neither of which can be guessed here, |
| | | * so the deployment that knows both sets the property - until it does, a create index |
| | | * waiting for a metadata lock still waits for as long as the engine lets it, and so do the |
| | | * walks a backend makes while it opens (the load of the compressed schema, the read that |
| | | * checks id2entry is there) and the export behind the generation ID of a replicated domain. |
| | | * That is what this backend did before any of these bounds existed; bounding them as the |
| | | * work of a client operation, which is the only other value there was to give them, stopped |
| | | * a large backend from opening at all. |
| | | */ |
| | | BULK("org.openidentityplatform.opendj.jdbc.bulk.timeout", 0); |
| | | |
| | | final String property; |
| | | final int defaultSeconds; |
| | | |
| | | StatementBound(String property, int defaultSeconds) { |
| | | this.property = property; |
| | | this.defaultSeconds = defaultSeconds; |
| | | } |
| | | |
| | | /** |
| | | * The bound in seconds, as configured by {@link #property}: 0, or a negative value, leaves |
| | | * the statement unbounded, as it was before this bound existed, while a value that is not a |
| | | * number is ignored in favour of {@link #defaultSeconds} - {@code Integer.getInteger()} |
| | | * falls back to its default rather than reading such a value as a zero. A value above |
| | | * {@link JDBCStorage#MAX_BOUND_SECONDS} is taken down to it, for the reason recorded there. |
| | | */ |
| | | int seconds() { |
| | | return clampSeconds(Integer.getInteger(property, defaultSeconds)); |
| | | } |
| | | } |
| | | |
| | | /** What a caller of {@link #executeResultSet} makes of the rows, while the bound is still armed. */ |
| | | interface RowsHandler<T> { |
| | | T handle(ResultSet rows) throws SQLException; |
| | | } |
| | | |
| | | /** |
| | | * The value of a row that is there. A row whose {@code v} is null is one this backend never |
| | | * wrote - the column is nullable, however it is written - and it must not be answered with the |
| | | * {@code null} a single-row read uses, which is already taken and means "no such key": read that |
| | | * way, a key that exists is reported as absent. Named rather than left to the bare |
| | | * {@code NullPointerException} of {@code ByteString.wrap}, which names neither the fault nor the |
| | | * table it is in, and a {@code RuntimeException} rather than an {@code SQLException}, so that a |
| | | * corrupt row is never weighed against the bound of the statement that read it and reported as a |
| | | * timeout of a property that would have changed nothing. The key is left out of the message for |
| | | * the reason {@link #timedOut} leaves the statement out of its own: it is entry data. |
| | | */ |
| | | static ByteString valueOfRow(ResultSet rows, String tableName) throws SQLException { |
| | | return ByteString.wrap(valueOfRow(rows.getBytes("v"), tableName)); |
| | | } |
| | | |
| | | /** |
| | | * The same check where a batch of a cursor reads the value beside its key, by position. Checked |
| | | * as the rows are taken off the statement rather than as they are handed out one by one: there |
| | | * the failure is inside the bound and inside the {@code catch} of the batch, while a batch |
| | | * buffered whole and unwrapped later fails from {@code advanceFromBuffer()} - outside both, and |
| | | * as the bare {@code NullPointerException} this exists to replace. |
| | | */ |
| | | static byte[] valueOfRow(byte[] value, String tableName) { |
| | | if (value == null) { |
| | | throw new StorageRuntimeException("jdbc: a row of "+tableName+" is present with no value"); |
| | | } |
| | | return value; |
| | | } |
| | | |
| | | <T> T executeResultSet(PreparedStatement statement, RowsHandler<T> rows) throws SQLException { |
| | | return executeResultSet(statement, StatementBound.OPERATION, rows); |
| | | } |
| | | |
| | | /** |
| | | * Runs a query under the bound of its class and hands the rows to {@code rows} while that bound |
| | | * is still armed. They are read there rather than after this method returns because a driver |
| | | * transfers them as they are asked for: read outside, the transfer - up to a whole batch of a |
| | | * cursor - would run with neither layer of the bound covering it, which is exactly where a |
| | | * database that stops answering mid-drain parks the worker thread. {@code setQueryTimeout} |
| | | * covering {@code ResultSet.next()} is optional in the JDBC contract ("drivers <em>may</em> |
| | | * also apply this limit"), and the two drivers of this backend that do not buffer a result |
| | | * whole - oracle prefetches ten rows at a time, mssql buffers adaptively - are the ones that |
| | | * do not. |
| | | */ |
| | | <T> T executeResultSet(PreparedStatement statement, StatementBound bound, RowsHandler<T> rows) throws SQLException { |
| | | if (logger.isTraceEnabled()) { |
| | | logger.trace(LocalizableMessage.raw("jdbc: %s",statement)); |
| | | } |
| | | return statement.executeQuery(); |
| | | return bounded(statement, bound, () -> { |
| | | try (final ResultSet rs=statement.executeQuery()) { |
| | | return rows.handle(rs); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | int execute(PreparedStatement statement) throws SQLException { |
| | | return execute(statement, StatementBound.OPERATION); |
| | | } |
| | | |
| | | int execute(PreparedStatement statement, StatementBound bound) throws SQLException { |
| | | if (logger.isTraceEnabled()) { |
| | | logger.trace(LocalizableMessage.raw("jdbc: %s",statement)); |
| | | } |
| | | return statement.executeUpdate(); |
| | | return bounded(statement, bound, statement::executeUpdate); |
| | | } |
| | | |
| | | // unlike execute(), tolerates statements that return a result set ("analyze table" on mysql) |
| | | interface Execution<T> { |
| | | T run() throws SQLException; |
| | | } |
| | | |
| | | /** |
| | | * Runs a statement under the bound of its class. A statement of a class that carries one has to |
| | | * end: a row locked by an unrelated session, a table waiting for a metadata lock or a database |
| | | * that stops answering mid-query would otherwise park the worker thread that issued it for |
| | | * good. A class configured with no bound - which {@link StatementBound#BULK} ships as - takes |
| | | * neither of the two layers below and waits as this backend waited before they existed. |
| | | * <p> |
| | | * The bound is asked of the driver rather than of the session, because a pooled connection |
| | | * cannot carry a session setting - {@code CachedConnection.close()} only rolls back, so a |
| | | * {@code statement_timeout} of one operation would apply to whoever borrows the connection |
| | | * next - and it is applied in two layers, since the first one is not answered everywhere: |
| | | * {@code setQueryTimeout} cancels the statement and keeps the connection, while the socket read |
| | | * timeout behind it ends the wait even when the cancel is not acted upon. Oracle needs that |
| | | * second layer: a session blocked in a row-lock enqueue does not process the break its driver |
| | | * sends, so the timeout is armed and never arrives (the container suites cover it). That second |
| | | * layer belongs to the connection rather than to the statement, so it is arbitrated between the |
| | | * statements running on one - see {@link Backstop}. |
| | | */ |
| | | private <T> T bounded(PreparedStatement statement, StatementBound bound, Execution<T> execution) throws SQLException { |
| | | final int seconds=bound.seconds(); |
| | | // whether the cancel is in force: a driver is free to refuse the query timeout, and then the |
| | | // socket read timeout behind it is the only layer this statement has - one that arrives later |
| | | final boolean cancelArmed=seconds > 0 && setQueryTimeout(statement, seconds); |
| | | // an unbounded class is announced to the connection all the same: a statement told it may |
| | | // take as long as it needs must not be cut by the socket read timeout of a concurrent one |
| | | return bounded(connectionOf(statement), bound.property, seconds, cancelArmed, execution); |
| | | } |
| | | |
| | | /** |
| | | * Runs the catalog lookups of {@code openTree()} under the bound of their class. They ask |
| | | * {@code DatabaseMetaData}, which takes no query timeout, so the socket read timeout behind the |
| | | * cancel is the only layer they can be given - and they do need one: they run once per tree on |
| | | * every open of a backend, and the catalog is answered by the same engine, behind the same |
| | | * locks, as the {@code create table} they guard. |
| | | * <p> |
| | | * That layer is only as good as what it actually arms, which is not always something: a driver |
| | | * with no network timeout, a connection that failed the call, one already carrying a tighter |
| | | * timeout of a deployment's own, and a statement of an unbounded class running beside this one |
| | | * each leave such a lookup with no bound at all. It is then reported as what it is - see |
| | | * {@link #timedOut} - rather than as a property that bounded nothing. |
| | | */ |
| | | <T> T bounded(Connection con, StatementBound bound, Execution<T> execution) throws SQLException { |
| | | // no cancel to arm: DatabaseMetaData takes no query timeout, so the socket read timeout behind |
| | | // it is the only layer these have, and nothing ends their wait before the margin of that layer |
| | | return bounded(con, bound.property, bound.seconds(), false, execution); |
| | | } |
| | | |
| | | /** |
| | | * Runs a statement under a bound of its own rather than under the bound of a class, for the one |
| | | * statement that has a property of its own: the statistics refresh after an import, which |
| | | * legitimately takes as long as a scan of the table it describes. |
| | | */ |
| | | private <T> T bounded(Connection con, String property, int seconds, boolean cancelArmed, Execution<T> execution) |
| | | throws SQLException { |
| | | final long startedAt=nanoTime(); |
| | | final Backstop backstop=holdBackstop(con, seconds); |
| | | try { |
| | | return execution.run(); |
| | | }catch (SQLException e) { |
| | | // what the second layer carries is read here rather than at the top: it is arbitrated |
| | | // between the statements in flight, so it is the value at the moment of the failure that |
| | | // bounded this statement - and it is read before the release below takes it back off |
| | | throw timedOut(e, property, seconds, cancelArmed, armedMillis(backstop), startedAt); |
| | | }finally { |
| | | releaseBackstop(backstop, con, seconds); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * What the socket read timeout of a connection carries for the statements on it right now, or 0 |
| | | * where this layer is not in force for them at all. It is not enough that a bound was asked for: |
| | | * {@link #applyBackstop} arms nothing on a connection whose driver refused the call or has no |
| | | * network timeout to give, nothing on one already carrying a timeout of a deployment's own that |
| | | * is tighter than ours, and nothing while a statement of an unbounded class runs beside this one. |
| | | */ |
| | | private static int armedMillis(Backstop state) { |
| | | if (state == null) { |
| | | return 0; // no connection to arm it on: the cancel is the whole bound of such a statement |
| | | } |
| | | synchronized (state) { |
| | | return state.armed; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Asks the driver to cancel the statement at the bound. Not every driver has one: the JDBC |
| | | * contract allows {@code SQLFeatureNotSupportedException} and this backend takes whatever URL a |
| | | * deployment configures, so a driver without it degrades to the socket read timeout behind it |
| | | * rather than failing every statement it is given. |
| | | */ |
| | | private boolean setQueryTimeout(PreparedStatement statement, int seconds) { |
| | | try { |
| | | statement.setQueryTimeout(seconds); |
| | | return true; |
| | | }catch (SQLException | RuntimeException e) { |
| | | if (queryTimeoutWarned.compareAndSet(false, true)) { |
| | | logger.warn(LocalizableMessage.raw("jdbc: the driver would not take a query timeout (%s): a statement of this" |
| | | + " backend is left to the socket read timeout behind it", e.getMessage())); |
| | | } |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | private Connection connectionOf(PreparedStatement statement) { |
| | | try { |
| | | return statement.getConnection(); |
| | | }catch (SQLException | RuntimeException e) { |
| | | return null; // nothing to arm the backstop on; the cancel above is the whole bound |
| | | } |
| | | } |
| | | |
| | | /** How long the socket read timeout outlasts the cancel it backs up, giving it room to arrive. */ |
| | | static final int BACKSTOP_MARGIN_SECONDS = 30; |
| | | |
| | | /** How far under its bound a driver may report the cancel, its timer being kept in whole seconds. */ |
| | | static final long CLOCK_SLACK_MILLIS = 250; |
| | | |
| | | /** |
| | | * Ceiling of every bound this backend arms, in seconds - 24.9 days, which is what a socket read |
| | | * timeout can hold at all: {@code setNetworkTimeout} takes milliseconds of an {@code int}, and a |
| | | * bound past this one has no value of that layer to be given. It is <em>not</em> what keeps the |
| | | * arithmetic of {@link #backstopMillis} in range - the {@code long} multiply under the |
| | | * {@code Math.min} there does that on its own, up to the point where adding the margin overflows |
| | | * an {@code int} before the multiply ever runs - so a reader who later takes that {@code Math.min} |
| | | * away must not read this clamp as covering them. |
| | | * <p> |
| | | * Clamped rather than refused, and clamped rather than read as "no bound": a bound this large |
| | | * cancels nothing a database will not have ended first, so taking a nonsensical value down to it |
| | | * costs a deployment nothing, while reading it as an unbound would take a bound away from a |
| | | * deployment that asked for one. A property set to {@code Integer.MAX_VALUE} therefore bounds a |
| | | * statement at 24.9 days rather than leaving it unbounded; {@code 0} is what leaves it unbounded. |
| | | */ |
| | | static final int MAX_BOUND_SECONDS = Integer.MAX_VALUE/1000 - BACKSTOP_MARGIN_SECONDS; |
| | | |
| | | static int clampSeconds(int seconds) { |
| | | return Math.max(0, Math.min(MAX_BOUND_SECONDS, seconds)); |
| | | } |
| | | |
| | | /** |
| | | * What {@link #timedOut} calls the second layer when that layer is the only one a statement ran |
| | | * under, so that a test can tell the two apart in a message: a run where the first layer stopped |
| | | * working degrades to this one by design, silently, and a suite that only measures how long a |
| | | * statement waited would go green with the cancel gone entirely. |
| | | */ |
| | | static final String BACKSTOP_ALONE = "the socket read timeout behind "; |
| | | |
| | | // The clock a bound is measured on, in one place so that a test can drive it: the classification |
| | | // below turns on a few milliseconds either side of the bound, and a mock statement cannot be made |
| | | // to take a real second without the suite taking one too. Monotonic, so that a step of the wall |
| | | // clock can neither lengthen nor shorten what a statement is measured to have taken. |
| | | long nanoTime() { |
| | | return System.nanoTime(); |
| | | } |
| | | |
| | | // setNetworkTimeout() takes the executor its timeout handling runs on; the drivers of this |
| | | // backend only set a socket option in it, so it costs a call rather than a thread. |
| | | private static final Executor DIRECT_EXECUTOR = Runnable::run; |
| | | |
| | | // Set when the driver of this storage has no network timeout to give at all, which is a property |
| | | // of the driver rather than of a connection: asking it again would cost a throw per statement, |
| | | // and the entry a connection's Backstop lives in is gone as soon as nothing runs on it. Held per |
| | | // storage rather than per JVM, like the warnings below: a driver that will not take one of these |
| | | // says so once for every backend running on it, instead of one backend silencing it for all. |
| | | private final AtomicBoolean backstopUnsupported = new AtomicBoolean(); |
| | | private final AtomicBoolean backstopUnsupportedWarned = new AtomicBoolean(); |
| | | private final AtomicBoolean backstopFailedWarned = new AtomicBoolean(); |
| | | private final AtomicBoolean queryTimeoutWarned = new AtomicBoolean(); |
| | | |
| | | /** |
| | | * The socket read timeout of one connection, and the statements running on it. This second |
| | | * layer of the bound is a property of the socket rather than of a statement, so it cannot be |
| | | * armed and put back per statement wherever a connection carries more than one at a time: an |
| | | * {@code ImporterImpl} holds a single connection for the whole of an import and writes to it |
| | | * from every phase-one worker and every phase-two task, and there the first statement to finish |
| | | * would take the backstop away from every statement still in flight - while a statement whose |
| | | * class carries no bound at all would run under whatever value a concurrent one happened to |
| | | * arm, dying at it with nothing to say which property cut it, since such a statement never |
| | | * reaches {@link #timedOut}. |
| | | * <p> |
| | | * So the value armed is the loosest of the bounds of the statements in flight, and a statement |
| | | * with no bound of its own takes it off for as long as it runs: this backstop exists to end a |
| | | * wait nothing else would end, never to cut a statement that was told it may take as long as it |
| | | * needs. What the connection carried before is put back when the last of them is through. |
| | | */ |
| | | private static final class Backstop { |
| | | /** Bounds of the statements in flight, in milliseconds and by count, the loosest last. */ |
| | | final TreeMap<Integer,Integer> bounds=new TreeMap<>(); |
| | | /** Statements in flight with no bound of their own, which no backstop may cut short. */ |
| | | 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. */ |
| | | int previous; |
| | | /** What the backstop has armed, or 0 when the connection carries {@link #previous}. */ |
| | | int armed; |
| | | /** |
| | | * 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 |
| | | * that: the common cause is a connection on its way out, and a driver that has no network |
| | | * timeout at all is remembered for the whole storage instead - see {@link #backstopUnsupported}. |
| | | */ |
| | | boolean failed; |
| | | } |
| | | |
| | | // Keyed by identity on the connection of the driver: CachedConnection.prepareStatement() hands |
| | | // the statement to the connection it wraps, so that is the one a statement reports, while the |
| | | // catalog lookups above hold the wrapper of that same connection - both have to find the same |
| | | // entry, so a wrapper is unwrapped on the way in. Static because the pool these connections |
| | | // come from is static; an entry lives only while statements are running on its connection. |
| | | private static final Map<Connection,Backstop> backstops = new IdentityHashMap<>(); |
| | | |
| | | private static Connection physical(Connection con) { |
| | | return con instanceof CachedConnection ? ((CachedConnection)con).parent : con; |
| | | } |
| | | |
| | | /** |
| | | * Puts the bound of a statement about to run on the connection that will run it, and makes the |
| | | * socket read timeout of that connection fit every statement in flight on it. Reaching this |
| | | * bound, unlike reaching the cancel it backs up, costs the connection: the driver closes it, |
| | | * which is the price of a wait the database was never going to end on its own. |
| | | */ |
| | | private Backstop holdBackstop(Connection con, int seconds) { |
| | | final Connection physical=physical(con); |
| | | if (physical == null) { |
| | | return null; |
| | | } |
| | | final Backstop state; |
| | | synchronized (backstops) { |
| | | state=backstops.computeIfAbsent(physical, c -> new Backstop()); |
| | | state.holders++; // held from here, so that the entry outlives a concurrent release |
| | | } |
| | | synchronized (state) { |
| | | if (seconds > 0) { |
| | | state.bounds.merge(backstopMillis(seconds), 1, Integer::sum); |
| | | }else { |
| | | state.unbounded++; |
| | | } |
| | | applyBackstop(physical, state); |
| | | } |
| | | return state; |
| | | } |
| | | |
| | | private void releaseBackstop(Backstop state, Connection con, int seconds) { |
| | | if (state == null) { |
| | | return; |
| | | } |
| | | final Connection physical=physical(con); |
| | | try { |
| | | synchronized (state) { |
| | | if (seconds > 0) { |
| | | final int millis=backstopMillis(seconds); |
| | | final Integer inFlight=state.bounds.get(millis); |
| | | if (inFlight == null || inFlight <= 1) { |
| | | state.bounds.remove(millis); |
| | | }else { |
| | | state.bounds.put(millis, inFlight-1); |
| | | } |
| | | }else { |
| | | state.unbounded--; |
| | | } |
| | | applyBackstop(physical, state); |
| | | } |
| | | }finally { // the entry is let go whatever the driver did, so that it cannot outlive its connection |
| | | synchronized (backstops) { |
| | | if (--state.holders <= 0) { // nothing is running on it: the connection is on its own again |
| | | backstops.remove(physical); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private static int backstopMillis(int seconds) { |
| | | return (int) Math.min(Integer.MAX_VALUE, (seconds+BACKSTOP_MARGIN_SECONDS)*1000L); |
| | | } |
| | | |
| | | /** |
| | | * 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 |
| | | * with the monitor of {@code state} held, since it both reads those counts and acts on the |
| | | * driver. |
| | | */ |
| | | private void applyBackstop(Connection con, Backstop state) { |
| | | if (state.failed || backstopUnsupported.get()) { |
| | | // but a connection this backstop has already armed does not keep carrying it: the entry |
| | | // remembering what it carried before is dropped when its last statement is through, and the |
| | | // value would go back to the pool as the connection's own read timeout. Reachable through |
| | | // the second guard, which is a latch of the whole storage: a connection armed before it was |
| | | // set would otherwise never be disarmed. Where nothing was armed this costs no call. |
| | | restorePrevious(con, state); |
| | | return; |
| | | } |
| | | 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; |
| | | } |
| | | return; |
| | | } |
| | | if (state.armed == 0) { |
| | | 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. |
| | | if (state.previous > 0 && state.previous <= wanted) { |
| | | if (state.armed != 0) { |
| | | con.setNetworkTimeout(DIRECT_EXECUTOR, state.previous); |
| | | state.armed=0; |
| | | } |
| | | return; |
| | | } |
| | | if (state.armed != wanted) { |
| | | con.setNetworkTimeout(DIRECT_EXECUTOR, wanted); |
| | | state.armed=wanted; |
| | | } |
| | | }catch (SQLException | RuntimeException e) { |
| | | state.failed=true; // whatever the cause, this connection is not asked again while it runs |
| | | // and what it carried before goes back, while there is still an entry saying what that was: |
| | | // this one is dropped as soon as the last statement on the connection is through, and a |
| | | // backstop left armed would go back to the pool as the connection's own read timeout - which |
| | | // is exactly how the next borrower reads it, tightening to it and never replacing it. |
| | | restorePrevious(con, state); |
| | | // The two causes are told apart, because they deserve opposite treatment and one of them |
| | | // would otherwise spend the single warning the other needs: a driver with no network |
| | | // timeout at all says so through SQLFeatureNotSupportedException, and there is nothing to |
| | | // gain by asking it once per statement for the life of the storage, while a connection on |
| | | // its way out - it may be the one that reached this very timeout - says nothing about the |
| | | // driver and must not disable the backstop for the connections that are still healthy. |
| | | if (e instanceof SQLFeatureNotSupportedException) { |
| | | backstopUnsupported.set(true); |
| | | if (backstopUnsupportedWarned.compareAndSet(false, true)) { |
| | | logger.warn(LocalizableMessage.raw("jdbc: the driver takes no socket read timeout (%s): a statement the" |
| | | + " database does not cancel will wait for it indefinitely, unless the connect properties of the URL" |
| | | + " configured for this backend carry one", e.getMessage())); |
| | | } |
| | | }else if (backstopFailedWarned.compareAndSet(false, true)) { |
| | | logger.warn(LocalizableMessage.raw("jdbc: the socket read timeout backing up a cancelled statement could not" |
| | | + " be set on a connection (%s): a statement the database does not cancel will wait for it" |
| | | + " indefinitely there", e.getMessage())); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 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. |
| | | */ |
| | | private static void restorePrevious(Connection con, Backstop state) { |
| | | if (state.armed == 0) { |
| | | return; // the connection carries its own value already |
| | | } |
| | | try { |
| | | con.setNetworkTimeout(DIRECT_EXECUTOR, state.previous); |
| | | }catch (SQLException | RuntimeException ignored) { |
| | | // 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; |
| | | } |
| | | } |
| | | |
| | | // Every driver reports a cancelled statement differently - postgresql as 57014, oracle as |
| | | // ORA-01013, and neither of them as a SQLTimeoutException - so the bound is recognized by the |
| | | // time the statement took rather than by the class or the state of its failure, and that time |
| | | // is taken from the monotonic clock, which a step of the wall clock can neither lengthen nor |
| | | // shorten. What it cannot tell apart is a failure of another kind arriving after the bound, |
| | | // which is why the failure it replaces is chained rather than swallowed. The SQL state and the |
| | | // error number are carried over as well, since a failure that arrives at the bound may still be |
| | | // one a caller classifies: a mysql lock wait, reported in class 40, ends inside a longer bound |
| | | // and stays the replayable conflict it is. The statement itself is left out of the message: a |
| | | // driver renders it with its parameters bound, and those are entry data. |
| | | private SQLException timedOut(SQLException e, String property, int seconds, boolean cancelArmed, |
| | | int backstopArmedMillis, long startedAt) { |
| | | if (seconds <= 0) { |
| | | return e; |
| | | } |
| | | // Which layer was really in force, and until when. Where the cancel is armed, the property |
| | | // ends the wait at its own value. Where it is not - a statement of DatabaseMetaData takes no |
| | | // query timeout, and a driver is free to refuse one - the socket read timeout behind it is the |
| | | // only layer there is, and that one arrives a margin later: measuring such a statement against |
| | | // the property alone reported a connection reset at 121 s as a query timeout of 120 s and sent |
| | | // the operator to a property that bounded nothing. Asking for that layer is not having it, |
| | | // which is why the value armed is passed in rather than derived from the property here: a |
| | | // driver with no network timeout, a connection that failed the call, one already carrying a |
| | | // tighter timeout of its own, and a statement of an unbounded class running beside this one |
| | | // each leave it unarmed. A statement neither layer bounded reached no bound of ours at all, so |
| | | // its failure is the driver's own and is left exactly as it is: naming a property that armed |
| | | // nothing sends an operator to raise a value that changes nothing about the wait they saw. |
| | | final long endsAfterMillis=cancelArmed ? seconds*1000L : backstopArmedMillis; |
| | | if (endsAfterMillis <= 0) { |
| | | return e; |
| | | } |
| | | final long elapsedMillis=(nanoTime()-startedAt)/1000000L; |
| | | // The bound is allowed a little slack under it: a driver keeps its timer in whole seconds and |
| | | // reports the cancel a few milliseconds before the bound is arithmetically due, and measured |
| | | // to the millisecond such a statement would arrive as a bare 57014 or ORA-01013, naming |
| | | // neither the property that cancelled it nor the fact that it was cancelled at all. |
| | | if (elapsedMillis < endsAfterMillis-CLOCK_SLACK_MILLIS) { |
| | | return e; |
| | | } |
| | | // The time is reported as measured rather than as the bound. Where the database does not act |
| | | // on the cancel - a session blocked in a row-lock enqueue on oracle - the wait ends at the |
| | | // socket read timeout, a margin past the property that armed it, and "did not finish within |
| | | // the 120s" of a statement that waited 150 s is a message an operator cannot put next to a |
| | | // clock. The property named still governs both layers, since backstopMillis() derives the |
| | | // second one from it, so raising it stays the remedy either way. |
| | | return new SQLTimeoutException("jdbc: the statement took "+elapsedMillis+" ms, reaching the " |
| | | +(endsAfterMillis/1000L)+"s of " |
| | | +(cancelArmed ? property : BACKSTOP_ALONE+property+" (the only layer bounding a statement that takes no" |
| | | +" query timeout; it is armed at the loosest bound of the statements sharing this connection, this" |
| | | +" one's being "+seconds+"s plus the margin of that layer)") |
| | | +": raise that property, or set it to 0 for no bound", e.getSQLState(), e.getErrorCode(), e); |
| | | } |
| | | |
| | | // Unlike execute(), tolerates a statement that returns a result set - the comment statement of |
| | | // mssql is a batch that ends in an exec - and, unlike it, carries no bound of its own: what is |
| | | // left of this method runs on a stamp connection, which is given a lock timeout of its own |
| | | // (Dialect.lockTimeoutSql) and a socket read timeout in its connect properties. |
| | | void executeAny(PreparedStatement statement) throws SQLException { |
| | | if (logger.isTraceEnabled()) { |
| | | logger.trace(LocalizableMessage.raw("jdbc: %s",statement)); |
| | |
| | | } |
| | | |
| | | Connection getConnection() throws Exception { |
| | | return CachedConnection.getConnection(config.getDBDirectory()); |
| | | return getConnection(true); |
| | | } |
| | | |
| | | /** |
| | |
| | | * per import or per removal buys back exactly what master did on every borrow. |
| | | */ |
| | | Connection getValidatedConnection() throws Exception { |
| | | return CachedConnection.getConnection(config.getDBDirectory(), false); |
| | | return getConnection(false); |
| | | } |
| | | |
| | | // The one borrow of this storage: both methods above go through it, so that whatever stands in |
| | | // for the pool stands in for every path that takes a connection. A stand-in of the trusted |
| | | // borrow alone let the open, the import and the removal - the three that ask for a validated |
| | | // one - reach a real database instead. |
| | | Connection getConnection(boolean trusted) throws Exception { |
| | | return CachedConnection.getConnection(config.getDBDirectory(), trusted); |
| | | } |
| | | |
| | | |
| | |
| | | // Asked of the very session that parses the literal: sql_mode is a session setting, and a |
| | | // session opened at another moment can have been given another value of it. |
| | | boolean isMysqlBackslashEscape(Connection con) throws SQLException { |
| | | try (final PreparedStatement statement=con.prepareStatement("select @@sql_mode"); |
| | | final ResultSet rs=executeResultSet(statement)) { |
| | | final String sqlMode=rs.next() ? rs.getString(1) : null; |
| | | 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"); |
| | | } |
| | | } |
| | |
| | | // A session setting must reach the server as a plain batch: the sql server driver runs a |
| | | // prepared statement through sp_executesql, and a setting made there is reverted when that |
| | | // call returns - before the statement it is meant to protect ever runs. |
| | | // |
| | | // Outside both layers of the bound, like the comment statement executeAny() runs, and for the |
| | | // same reason: this is issued from newStampConnection() on a stamp connection, whose connect |
| | | // properties carry a socket read timeout of their own (Dialect.connectProperties). |
| | | private void executeSessionStatement(Connection con, String sql) throws SQLException { |
| | | try (final Statement statement=con.createStatement()) { |
| | | if (logger.isTraceEnabled()) { |
| | |
| | | } |
| | | try (final PreparedStatement statement=con.prepareStatement(sql)) { |
| | | statement.setString(1,arg); |
| | | try (final ResultSet rs=executeResultSet(statement)) { |
| | | return rs.next() ? rs.getString(1) : null; |
| | | } |
| | | return executeResultSet(statement, rs -> rs.next() ? rs.getString(1) : null); |
| | | } |
| | | } |
| | | |
| | |
| | | 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=Math.max(0,Integer.getInteger(STATISTICS_TIMEOUT_PROPERTY,STATISTICS_TIMEOUT_SECONDS_DEFAULT)); |
| | | final int timeoutSeconds=clampSeconds(Integer.getInteger(STATISTICS_TIMEOUT_PROPERTY,STATISTICS_TIMEOUT_SECONDS_DEFAULT)); |
| | | boolean allRefreshed=true; |
| | | for (final TreeName treeName : trees) { |
| | | final String tableName=getTableName(treeName); |
| | |
| | | throw new IllegalStateException("no statistics refresh for dialect "+dialect); |
| | | } |
| | | try (final PreparedStatement statement=con.prepareStatement(sql)) { |
| | | statement.setQueryTimeout(timeoutSeconds); // 0: wait without limit |
| | | // 0: wait without limit - and false where the driver would not take the cancel, which |
| | | // leaves the socket read timeout behind it as the only layer this refresh runs under |
| | | final boolean cancelArmed=timeoutSeconds>0 && setQueryTimeout(statement, timeoutSeconds); |
| | | for (int i=0;i<args.length;i++) { |
| | | statement.setString(i+1,args[i]); |
| | | } |
| | | if (dialect==Dialect.MYSQL) { // mysql reports analyze problems as a result row, not an SQLException |
| | | try (final ResultSet rs=executeResultSet(statement)) { |
| | | while (rs.next()) { |
| | | if ("error".equalsIgnoreCase(rs.getString("Msg_type"))) { |
| | | throw new SQLException(rs.getString("Msg_text")); |
| | | // Under the bound of the statistics refresh rather than under a class of |
| | | // StatementBound, which would put its own value over one this statement has a |
| | | // property for - but under both layers of it all the same: on oracle this is |
| | | // dbms_stats.gather_table_stats, the engine whose session does not act on the |
| | | // break its driver sends, and it runs at the very end of a successful import, |
| | | // where a cancel that never arrives would park it with the data already |
| | | // committed and nothing left to report. |
| | | bounded(con, STATISTICS_TIMEOUT_PROPERTY, timeoutSeconds, cancelArmed, () -> { |
| | | if (logger.isTraceEnabled()) { |
| | | logger.trace(LocalizableMessage.raw("jdbc: %s",statement)); |
| | | } |
| | | if (dialect==Dialect.MYSQL) { // mysql reports analyze problems as a result row, not an SQLException |
| | | try (final ResultSet rs=statement.executeQuery()) { |
| | | while (rs.next()) { |
| | | if ("error".equalsIgnoreCase(rs.getString("Msg_type"))) { |
| | | throw new SQLException(rs.getString("Msg_text")); |
| | | } |
| | | } |
| | | } |
| | | }else { // tolerates a statement that returns a result set, which execute() does not |
| | | statement.execute(); |
| | | } |
| | | }else { |
| | | executeAny(statement); |
| | | } |
| | | return null; |
| | | }); |
| | | con.commit(); |
| | | } |
| | | }catch (Exception e) { |
| | |
| | | try { |
| | | for (final TreeName treeName : trees) { |
| | | try (final PreparedStatement statement = con.prepareStatement("drop table " + getTableName(treeName))) { |
| | | execute(statement); |
| | | execute(statement, StatementBound.BULK); |
| | | } |
| | | } |
| | | con.commit(); |
| | |
| | | return driverNameOf(con).contains("microsoft") ? "cast(? as char(128))" : "?"; |
| | | } |
| | | |
| | | private class ReadableTransactionImpl implements ReadableTransaction { |
| | | class ReadableTransactionImpl implements ReadableTransaction { |
| | | final Connection con; |
| | | /** |
| | | * The class the statements of this transaction take. It follows who runs them rather than |
| | | * what they look like: an import issues the same select and the same upsert a client |
| | | * operation does, but nobody is waiting on it - and on mssql it works the table unindexed, |
| | | * {@code k} being a {@code varbinary(max)} that cannot be an index key - so bounding an |
| | | * import as an entry read fails an import that ran to the end before this bound existed. |
| | | * The catalog lookups of {@code openTree()} keep the operation class whoever runs them: they |
| | | * read a data dictionary rather than the data, so a wait there is another session's metadata |
| | | * lock, which is one of the waits this bound exists to end. |
| | | */ |
| | | final StatementBound bound; |
| | | boolean isReadOnly=true; |
| | | |
| | | public ReadableTransactionImpl(Connection con) { |
| | | this(con, StatementBound.OPERATION); |
| | | } |
| | | |
| | | ReadableTransactionImpl(Connection con, StatementBound bound) { |
| | | this.con=con; |
| | | this.bound=bound; |
| | | } |
| | | |
| | | @Override |
| | | public ByteString read(TreeName treeName, ByteSequence key) { |
| | | try (final PreparedStatement statement=con.prepareStatement("select v from "+readTableName(treeName)+" where h="+hashParam(con)+" and k=?")){ |
| | | // the non-enrolling name: a read must not put a tree this backend does not own - the |
| | | // shared compressed schema tree of #873 - up for removal |
| | | final String tableName=readTableName(treeName); |
| | | try (final PreparedStatement statement=con.prepareStatement("select v from "+tableName+" where h="+hashParam(con)+" and k=?")){ |
| | | statement.setString(1,key2hash.get(ByteBuffer.wrap(key.toByteArray()))); |
| | | statement.setBytes(2,real2db(key.toByteArray())); |
| | | try(ResultSet rc=executeResultSet(statement)) { |
| | | return rc.next() ? ByteString.wrap(rc.getBytes("v")) : null; |
| | | } |
| | | return executeResultSet(statement, bound, rc -> rc.next() ? valueOfRow(rc, tableName) : null); |
| | | }catch (SQLException e) { |
| | | throw new StorageRuntimeException(e); |
| | | } |
| | |
| | | |
| | | @Override |
| | | public Cursor<ByteString, ByteString> openCursor(TreeName treeName) { |
| | | return new CursorImpl(isReadOnly,con,treeName); |
| | | return new CursorImpl(isReadOnly,con,treeName,bound); |
| | | } |
| | | |
| | | /** |
| | | * {@inheritDoc} |
| | | * <p> |
| | | * The batches of such a cursor are bulk statements however ordinary they look: nobody is |
| | | * waiting on the walk, and on mssql it is not even a walk along an index - {@code k} is a |
| | | * {@code varbinary(max)} there, which cannot be an index key, so every batch is a scan and |
| | | * a sort of the table. Bounding those as entry reads aborted an export or a rebuild that |
| | | * ran to the end before this bound existed. |
| | | */ |
| | | @Override |
| | | public Cursor<ByteString, ByteString> openBulkCursor(TreeName treeName) { |
| | | return new CursorImpl(isReadOnly,con,treeName,StatementBound.BULK); |
| | | } |
| | | |
| | | /** |
| | | * {@inheritDoc} |
| | | * <p> |
| | | * Bulk whoever asks: {@code select count(*)} is a scan of the whole table on every engine |
| | | * here, so what it takes follows the size of the backend rather than the work of the caller |
| | | * that happens to ask. Its callers are administrative either way - {@code dbtest} through |
| | | * {@code BackendStat}, and the counts {@code verify-index} reports - so the override costs a |
| | | * client operation nothing. It is not the count behind {@code NOTE_BACKEND_STARTED}: that one |
| | | * is {@code BackendImpl.getEntryCount()} through {@code RootContainer.getEntryCount()}, which |
| | | * sums {@code id2childrenCount} and never reaches this method. |
| | | * <p> |
| | | * One of the places the class of the transaction is overridden downwards, the others being |
| | | * {@link #openBulkCursor(TreeName)}, {@link CursorImpl#positionToLastKey()} and the DDL a |
| | | * write transaction issues - the {@code create table} and the three {@code create index} of |
| | | * {@code openTree()}, the {@code delete from} of {@code clearTree()} and the {@code drop |
| | | * table} of {@code deleteTree()} - which is where an operation-class transaction, the one |
| | | * {@code write()} runs with, can take the shared backstop of its connection off. The count |
| | | * is deliberately not given here: whoever audits that list has to read it off the class |
| | | * rather than trust a number that a later hard-coded {@code BULK} would leave stale. |
| | | */ |
| | | @Override |
| | | public long getRecordCount(TreeName treeName) { |
| | | try (final PreparedStatement statement=con.prepareStatement("select count(*) from "+readTableName(treeName)); |
| | | final ResultSet rc=executeResultSet(statement)){ |
| | | return rc.next() ? rc.getLong(1) : 0; |
| | | try (final PreparedStatement statement=con.prepareStatement("select count(*) from "+readTableName(treeName))){ |
| | | return executeResultSet(statement, StatementBound.BULK, rc -> rc.next() ? rc.getLong(1) : 0); |
| | | }catch (SQLException e) { |
| | | throw new StorageRuntimeException(e); |
| | | } |
| | |
| | | // the writeable one inherits it. |
| | | 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 { |
| | | 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 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 false; |
| | | } |
| | | } |
| | | /** |
| | |
| | | boolean partlyCommitted; |
| | | |
| | | public WriteableTransactionTransactionImpl(Connection con) { |
| | | super(con); |
| | | this(con, StatementBound.OPERATION); |
| | | } |
| | | |
| | | WriteableTransactionTransactionImpl(Connection con, StatementBound bound) { |
| | | super(con, bound); |
| | | //captured once rather than read per operation: the access mode of the storage is mutable state - |
| | | //ImporterImpl reopens the storage READ_WRITE under its caller - and a transaction has to keep the mode |
| | | //it was created with. It also drives isReadOnly, so that a cursor this transaction opens refuses |
| | |
| | | */ |
| | | private void commitStatement(String sql, boolean ddl) throws SQLException { |
| | | partlyCommitted|=ddl && commitsBeforeDdl(); |
| | | // Bulk, whatever class the transaction itself carries: every statement issued through here is |
| | | // one of the ones #877 names as overridden downwards - the create table and the three create |
| | | // index of openTree(), the delete from of clearTree() and the drop table of deleteTree() - |
| | | // and nobody is waiting on any of them. |
| | | try (final PreparedStatement statement=con.prepareStatement(sql)) { |
| | | execute(statement); |
| | | execute(statement, StatementBound.BULK); |
| | | partlyCommitted=true; // a commit that fails leaves the outcome unknown, which is no more replayable |
| | | con.commit(); |
| | | } |
| | |
| | | } |
| | | |
| | | boolean isExistsIndex(String tableName, String indexName) throws SQLException { |
| | | // approximate=true: with false the oracle driver runs ANALYZE on every call |
| | | try (final ResultSet rs = con.getMetaData().getIndexInfo(null, null, tableName, false, true)) { |
| | | while (rs.next()) { |
| | | if (indexName.equalsIgnoreCase(rs.getString("INDEX_NAME"))) { |
| | | return true; |
| | | 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)) { |
| | | while (rs.next()) { |
| | | if (indexName.equalsIgnoreCase(rs.getString("INDEX_NAME"))) { |
| | | return true; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return false; |
| | | return false; |
| | | }); |
| | | } |
| | | |
| | | public void clearTree(TreeName treeName) { |
| | |
| | | statement.setString(1, key2hash.get(ByteBuffer.wrap(key.toByteArray()))); |
| | | statement.setBytes(2, real2db(key.toByteArray())); |
| | | statement.setBytes(3, value.toByteArray()); |
| | | return (execute(statement) == 1 && statement.getUpdateCount() > 0); |
| | | return (execute(statement, bound) == 1 && statement.getUpdateCount() > 0); |
| | | } |
| | | }else if (driverName.contains("mysql")) { //mysql upsert |
| | | try (final PreparedStatement statement = con.prepareStatement("insert into " + getTableName(treeName) + " (h,k,v) values (?,?,?) as new ON DUPLICATE KEY UPDATE v=new.v")) { |
| | | statement.setString(1, key2hash.get(ByteBuffer.wrap(key.toByteArray()))); |
| | | statement.setBytes(2, real2db(key.toByteArray())); |
| | | statement.setBytes(3, value.toByteArray()); |
| | | return (execute(statement) == 1 && statement.getUpdateCount() > 0); |
| | | return (execute(statement, bound) == 1 && statement.getUpdateCount() > 0); |
| | | } |
| | | }else if (driverName.contains("oracle")) { //ANSI MERGE without ; |
| | | try (final PreparedStatement statement = con.prepareStatement("merge into " + getTableName(treeName) + " old using (select ? h,? k,? v from dual) new on (old.h=new.h and old.k=new.k) WHEN MATCHED THEN UPDATE SET old.v=new.v WHEN NOT MATCHED THEN INSERT (h,k,v) VALUES (new.h,new.k,new.v)")) { |
| | | statement.setString(1, key2hash.get(ByteBuffer.wrap(key.toByteArray()))); |
| | | statement.setBytes(2, real2db(key.toByteArray())); |
| | | statement.setBytes(3, value.toByteArray()); |
| | | return (execute(statement) == 1 && statement.getUpdateCount() > 0); |
| | | return (execute(statement, bound) == 1 && statement.getUpdateCount() > 0); |
| | | } |
| | | }else if (driverName.contains("microsoft")) { //ANSI MERGE with ; WITH (HOLDLOCK) makes the upsert atomic: without it SQL Server MERGE can race two concurrent NOT MATCHED inserts of the same key into a PRIMARY KEY violation. UPDLOCK is required on top of it: with HOLDLOCK alone the search phase takes a shared lock that the WHEN MATCHED update then has to convert to an exclusive one, so two concurrent upserts of the same key deadlock on the conversion; an update lock is taken right away and makes the second transaction wait instead. h is cast back to char so that the join can seek the primary key instead of scanning the whole table under those locks, see hashParam() |
| | | try (final PreparedStatement statement = con.prepareStatement("merge into " + getTableName(treeName) + " WITH (HOLDLOCK, UPDLOCK) old using (select cast(? as char(128)) h,? k,? v) new on (old.h=new.h and old.k=new.k) WHEN MATCHED THEN UPDATE SET old.v=new.v WHEN NOT MATCHED THEN INSERT (h,k,v) VALUES (new.h,new.k,new.v);")) { |
| | | statement.setString(1, key2hash.get(ByteBuffer.wrap(key.toByteArray()))); |
| | | statement.setBytes(2, real2db(key.toByteArray())); |
| | | statement.setBytes(3, value.toByteArray()); |
| | | return (execute(statement) == 1 && statement.getUpdateCount() > 0); |
| | | return (execute(statement, bound) == 1 && statement.getUpdateCount() > 0); |
| | | } |
| | | }else { //ANSI SQL: try update before insert with not exists |
| | | return update(treeName,key,value) || insert(treeName,key,value); |
| | |
| | | statement.setBytes(3, value.toByteArray()); |
| | | statement.setString(4, key2hash.get(ByteBuffer.wrap(key.toByteArray()))); |
| | | statement.setBytes(5, real2db(key.toByteArray())); |
| | | return (execute(statement)==1 && statement.getUpdateCount()>0); |
| | | return (execute(statement, bound)==1 && statement.getUpdateCount()>0); |
| | | } |
| | | } |
| | | |
| | |
| | | statement.setBytes(1,value.toByteArray()); |
| | | statement.setString(2,key2hash.get(ByteBuffer.wrap(key.toByteArray()))); |
| | | statement.setBytes(3,real2db(key.toByteArray())); |
| | | return (execute(statement)==1 && statement.getUpdateCount()>0); |
| | | return (execute(statement, bound)==1 && statement.getUpdateCount()>0); |
| | | } |
| | | } |
| | | |
| | |
| | | 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())); |
| | | return (execute(statement)==1 && statement.getUpdateCount()>0); |
| | | return (execute(statement, bound)==1 && statement.getUpdateCount()>0); |
| | | }catch (SQLException e) { |
| | | throw new StorageRuntimeException(e); |
| | | } |
| | |
| | | ByteString currentValue; |
| | | boolean defined; |
| | | |
| | | public CursorImpl(boolean isReadOnly, Connection con, TreeName treeName) { |
| | | // The class of the statements this cursor issues, from whoever opened it: a search walks its |
| | | // index and has a client waiting, while an import, an export or a rebuild walks a whole tree |
| | | // with nobody waiting - and on mssql it walks it unindexed either way. It is not read off the |
| | | // shape of the statement, because the opening batch of every cursor is the same |
| | | // unconditioned "order by k" that positionToLastKey() issues, search or not. |
| | | final StatementBound batchBound; |
| | | |
| | | public CursorImpl(boolean isReadOnly, Connection con, TreeName treeName, StatementBound batchBound) { |
| | | this.isReadOnly=isReadOnly; |
| | | this.con=con; |
| | | this.treeName=treeName; |
| | | // the read statements below take the non-enrolling name: a cursor is how the migration |
| | | // of #873 reads the shared tree, and reading a tree must not put it up for removal |
| | | this.tableName=readTableName(treeName); |
| | | this.batchBound=batchBound; |
| | | this.limitClause=((CachedConnection)con).parent.getClass().getName().contains("mysql") |
| | | ? " limit ?,?" : " offset ? rows fetch next ? rows only"; |
| | | } |
| | |
| | | return size; |
| | | } |
| | | |
| | | boolean fetchBatch(String condition, byte[] dbKey, long offset, boolean descending, int limit) { |
| | | /** |
| | | * Reads one batch of the cursor. The class of the bound is the caller's: a batch taken |
| | | * along the index of the tree for a client is an operation, while a batch that has to look |
| | | * at the whole table to answer - the one behind {@link #positionToLastKey()} - and every |
| | | * batch of a cursor an import or a rebuild walks ({@link #batchBound}) is bulk work, and |
| | | * the two cannot share a value. |
| | | */ |
| | | boolean fetchBatch(String condition, byte[] dbKey, long offset, boolean descending, int limit, StatementBound bound) { |
| | | fetchCount++; |
| | | buffer.clear(); |
| | | try (final PreparedStatement statement=con.prepareStatement("select k,v from "+tableName |
| | |
| | | } |
| | | statement.setLong(i++,offset); |
| | | statement.setLong(i,limit); |
| | | try(final ResultSet rc=executeResultSet(statement)) { |
| | | return executeResultSet(statement, bound, rc -> { |
| | | while (rc.next()) { |
| | | buffer.add(new byte[][]{rc.getBytes(1),rc.getBytes(2)}); |
| | | buffer.add(new byte[][]{rc.getBytes(1),valueOfRow(rc.getBytes(2),tableName)}); |
| | | } |
| | | } |
| | | return !buffer.isEmpty(); |
| | | }); |
| | | }catch (SQLException e) { |
| | | throw new StorageRuntimeException(e); |
| | | } |
| | | return !buffer.isEmpty(); |
| | | } |
| | | |
| | | void advanceFromBuffer() { |
| | |
| | | |
| | | @Override |
| | | public boolean next() { |
| | | if (buffer.isEmpty() && !fetchBatch(currentKeyDb==null?null:">",currentKeyDb,0,false,adaptiveBatchSize())) { |
| | | if (buffer.isEmpty() && !fetchBatch(currentKeyDb==null?null:">",currentKeyDb,0,false,adaptiveBatchSize(),batchBound)) { |
| | | defined=false; |
| | | return false; |
| | | } |
| | |
| | | try (final PreparedStatement statement=con.prepareStatement("delete from "+writeTableName+" where h="+hashParam(con)+" and k=?")){ |
| | | statement.setString(1,key2hash.get(ByteBuffer.wrap(db2real(currentKeyDb)))); |
| | | statement.setBytes(2,currentKeyDb); |
| | | execute(statement); |
| | | execute(statement, batchBound); |
| | | }catch (SQLException e) { |
| | | throw new StorageRuntimeException(e); |
| | | } |
| | |
| | | if (!buffer.isEmpty()) { // jumped outside the buffered range: random access, back to small batches |
| | | nextBatchSize=initialBatchSize; |
| | | } |
| | | if (fetchBatch(">=",target,0,false,adaptiveBatchSize())) { |
| | | if (fetchBatch(">=",target,0,false,adaptiveBatchSize(),batchBound)) { |
| | | advanceFromBuffer(); |
| | | return true; |
| | | } |
| | |
| | | @Override |
| | | public boolean positionToKey(ByteSequence key) { |
| | | final byte[] real=key.toByteArray(); |
| | | // The row is wrapped inside the handler rather than after it, so that null keeps meaning |
| | | // "no such key" and only that: a row whose v is null - which the schema allows, however |
| | | // this backend writes it - has to fail here as it fails in read(), rather than report a |
| | | // key that exists as absent. Both go through valueOfRow(), which is where that failure |
| | | // is named. |
| | | final ByteString value; |
| | | try (final PreparedStatement statement=con.prepareStatement("select v from "+tableName+" where h="+hashParam(con)+" and k=?")){ |
| | | statement.setString(1,key2hash.get(ByteBuffer.wrap(real))); |
| | | statement.setBytes(2,real2db(real)); |
| | | try(final ResultSet rc=executeResultSet(statement)) { |
| | | if (rc.next()) { |
| | | buffer.clear(); |
| | | nextBatchSize=initialBatchSize; |
| | | currentKeyDb=real2db(real); |
| | | currentKey=ByteString.wrap(real); |
| | | currentValue=ByteString.wrap(rc.getBytes("v")); |
| | | defined=true; |
| | | return true; |
| | | } |
| | | } |
| | | value=executeResultSet(statement, batchBound, rc -> rc.next() ? valueOfRow(rc, tableName) : null); |
| | | }catch (SQLException e) { |
| | | throw new StorageRuntimeException(e); |
| | | } |
| | | if (value!=null) { |
| | | buffer.clear(); |
| | | nextBatchSize=initialBatchSize; |
| | | currentKeyDb=real2db(real); |
| | | currentKey=ByteString.wrap(real); |
| | | currentValue=value; |
| | | defined=true; |
| | | return true; |
| | | } |
| | | defined=false; |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Bulk, not operation: with no condition to seek on, this is {@code order by k desc} over |
| | | * the whole table - and on mssql, where {@code k} is a {@code varbinary(max)} that cannot |
| | | * be an index key, a scan and a sort of it. It is also not on a search path: every open of |
| | | * a backend runs it once per base DN, through {@code EntryContainer.getHighestEntryID()}, |
| | | * outside the try/catch of {@code BackendImpl.openBackend()} - a bound of two minutes here |
| | | * would turn a large backend that opens slowly into one that does not open at all. |
| | | */ |
| | | @Override |
| | | public boolean positionToLastKey() { |
| | | if (fetchBatch(null,null,0,true,1)) { |
| | | if (fetchBatch(null,null,0,true,1,StatementBound.BULK)) { |
| | | advanceFromBuffer(); |
| | | return true; |
| | | } |
| | |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * The class of the cursor, unlike {@link #positionToLastKey()}, which is bulk however it |
| | | * was opened: an offset comes from the VLV request of a client, so this runs on a search |
| | | * path and has to give the worker thread back - an import has no VLV position to seek to. |
| | | * That a deep offset is served by walking to it - the engines have no other way to answer |
| | | * an {@code offset ?} - is what makes the bound reachable here, and reaching it answers the |
| | | * request with an error rather than parking a thread of the server on it. |
| | | */ |
| | | @Override |
| | | public boolean positionToIndex(int index) { |
| | | if (!buffer.isEmpty()) { // absolute jump: random access, back to small batches |
| | | nextBatchSize=initialBatchSize; |
| | | } |
| | | if (index>=0 && fetchBatch(null,null,index,false,adaptiveBatchSize())) { |
| | | if (index>=0 && fetchBatch(null,null,index,false,adaptiveBatchSize(),batchBound)) { |
| | | advanceFromBuffer(); |
| | | return true; |
| | | } |
| | |
| | | return tree2table.asMap().keySet(); |
| | | } |
| | | |
| | | private final class ImporterImpl implements Importer { |
| | | final class ImporterImpl implements Importer { |
| | | final Connection con; |
| | | final ReadableTransactionImpl txr; |
| | | final WriteableTransactionTransactionImpl txw; |
| | |
| | | |
| | | final Boolean isOpen; |
| | | |
| | | public ImporterImpl() { |
| | | isOpen=getStorageStatus().isWorking(); |
| | | if (!isOpen) { |
| | | try { |
| | | open(AccessMode.READ_WRITE); |
| | | }catch (Exception e) { |
| | | throw new StorageRuntimeException(e); |
| | | } |
| | | /** |
| | | * Both transactions of an import take the bulk class, and with them every statement it |
| | | * issues: phase one writes the trees through {@code put()}, phase two reads them back |
| | | * through {@code read()} and walks them through {@code openCursor()}, and none of that has |
| | | * a client waiting on it. Bounding those as entry reads is not merely strict, it fails work |
| | | * that ran to the end before this bound existed: {@code h} is the primary key on every |
| | | * dialect and the default lock wait is forever on mssql, postgres and oracle, so an upsert |
| | | * of an online import blocked by an LDAP write on the same table sat until the bound of an |
| | | * entry read and then failed the import. |
| | | */ |
| | | ImporterImpl(Connection con, boolean isOpen) { |
| | | // An import writes by definition, so a storage that is not writeable refuses one where the |
| | | // importer is built - which is where it was refused until the write transaction of a read-only |
| | | // storage became one that is granted and checks per operation (#874). Left to that check, an |
| | | // import of such a storage would take a connection out of the pool, begin its transaction and |
| | | // fail at the first tree it clears rather than at its start. |
| | | // What arrives here read-only is a storage that was already open: import-ldif and |
| | | // rebuild-index both close it first, and startImport() opens a closed one READ_WRITE - an |
| | | // import of any storage of this server reopens it that way - so those two arrive writeable. |
| | | if (!accessMode.isWriteable()) { |
| | | throw new ReadOnlyStorageException(); |
| | | } |
| | | try { |
| | | con = getValidatedConnection(); |
| | | }catch (Exception e){ |
| | | throw new StorageRuntimeException(e); |
| | | } |
| | | txr =new ReadableTransactionImpl(con); |
| | | txw =new WriteableTransactionTransactionImpl(con); |
| | | this.con=con; |
| | | this.isOpen=isOpen; |
| | | txr=new ReadableTransactionImpl(con, StatementBound.BULK); |
| | | txw=new WriteableTransactionTransactionImpl(con, StatementBound.BULK); |
| | | } |
| | | |
| | | @Override |
| | |
| | | aborted = true; |
| | | } |
| | | |
| | | // The connection goes back whatever the commit does, and the storage this importer opened |
| | | // is closed whatever the connection does: an importer is closed on the way out of a failed |
| | | // import as readily as a finished one - a clearTree() that reaches the bulk bound is one |
| | | // way there - and a commit that throws on the way would otherwise leave the connection |
| | | // out of the pool for good, holding the transaction and the locks of that import. |
| | | @Override |
| | | public void close() { |
| | | try { |
| | |
| | | return txr.read(treeName, key); |
| | | } |
| | | |
| | | // Bulk like every other statement of an import, by the class of the transaction it comes |
| | | // from: this walks a whole tree with no client waiting on it - phase one of a rebuild-index |
| | | // reads every record of id2entry through this cursor (OnDiskMergeImporter.ID2EntrySource) - |
| | | // and on mssql it walks it unindexed, so a batch of it is a scan and a sort of the table |
| | | // rather than a step along an index. |
| | | @Override |
| | | public SequentialCursor<ByteString, ByteString> openCursor(TreeName treeName) { |
| | | return txr.openCursor(treeName); |
| | |
| | | //import |
| | | @Override |
| | | public Importer startImport() throws ConfigException, StorageRuntimeException { |
| | | return new ImporterImpl(); |
| | | final boolean wasOpen=getStorageStatus().isWorking(); |
| | | if (!wasOpen) { |
| | | try { |
| | | open(AccessMode.READ_WRITE); |
| | | }catch (Exception e) { |
| | | throw new StorageRuntimeException(e); |
| | | } |
| | | } |
| | | final Connection con; |
| | | try { |
| | | con=getValidatedConnection(); |
| | | }catch (Exception e){ |
| | | // and the storage this method opened goes back with it: ImporterImpl.close() is what closes |
| | | // it again when an import opened it, and no importer is going to be built to reach that |
| | | if (!wasOpen) { |
| | | close(); |
| | | } |
| | | throw new StorageRuntimeException(e); |
| | | } |
| | | // outside the catch: the importer of a read-only storage throws ReadOnlyStorageException, |
| | | // which a caller tells apart from any other failure of an import |
| | | boolean built=false; |
| | | try { |
| | | final Importer importer=new ImporterImpl(con, wasOpen); |
| | | built=true; |
| | | return importer; |
| | | }finally { |
| | | // and the connection borrowed above goes back on every path that does not build an |
| | | // importer to hold it: it is the one an import keeps for its whole duration, so leaving it |
| | | // here takes it out of the pool for good, with the transaction it had already begun. A |
| | | // finally rather than a catch, so that it covers what a catch has to name - an Error |
| | | // leaves the pool one connection short exactly as ReadOnlyStorageException did. |
| | | if (!built) { |
| | | try { |
| | | con.close(); |
| | | }catch (SQLException ignored) { |
| | | // the importer was never built; the failure to report is the one on its way out |
| | | } |
| | | // and the storage this method opened goes back with the connection, for the reason the |
| | | // borrow above gives: ImporterImpl.close() is what closes it again when an import |
| | | // opened it, and there is no importer here to reach that |
| | | if (!wasOpen) { |
| | | close(); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | //backup |
| | |
| | | long undefined = 0; |
| | | long count = 0; |
| | | BackendTreeKeyValue keyDecoder = new BackendTreeKeyValue(index); |
| | | try (Cursor<ByteString, EntryIDSet> cursor = index.openCursor(txn)) |
| | | // dbtest walks the index whole, on the command line of an operator: bulk work either way |
| | | try (Cursor<ByteString, EntryIDSet> cursor = index.openBulkCursor(txn)) |
| | | { |
| | | while (cursor.next()) |
| | | { |
| | |
| | | long count = 0; |
| | | long totalKeySize = 0; |
| | | long totalDataSize = 0; |
| | | try (final Cursor<ByteString, ByteString> cursor = txn.openCursor(target.getTreeName())) |
| | | // dbtest walks the tree whole, on the command line of an operator: bulk work either way |
| | | try (final Cursor<ByteString, ByteString> cursor = txn.openBulkCursor(target.getTreeName())) |
| | | { |
| | | ByteString key; |
| | | ByteString maxKey = null; |
| | |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2012-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | |
| | | public final Cursor<ByteString, EntryIDSet> openCursor(ReadableTransaction txn) |
| | | { |
| | | checkNotNull(txn, "txn must not be null"); |
| | | return CursorTransformer.transformValues(txn.openCursor(getName()), |
| | | return decoding(txn.openCursor(getName())); |
| | | } |
| | | |
| | | @Override |
| | | public final Cursor<ByteString, EntryIDSet> openBulkCursor(ReadableTransaction txn) |
| | | { |
| | | checkNotNull(txn, "txn must not be null"); |
| | | return decoding(txn.openBulkCursor(getName())); |
| | | } |
| | | |
| | | private Cursor<ByteString, EntryIDSet> decoding(Cursor<ByteString, ByteString> cursor) |
| | | { |
| | | return CursorTransformer.transformValues(cursor, |
| | | new ValueTransformer<ByteString, ByteString, EntryIDSet, NeverThrowsException>() |
| | | { |
| | | @Override |
| | |
| | | |
| | | long getNumberOfEntriesInBaseDN0(ReadableTransaction txn) |
| | | { |
| | | return id2childrenCount.getTotalCount(txn); |
| | | return getNumberOfEntriesInBaseDN0(txn, false); |
| | | } |
| | | |
| | | /** |
| | | * The same count, told which kind of work it is part of: {@code verify-index} reads it before |
| | | * walking the whole backend, with nobody waiting on the walk or on the count that sizes it, while |
| | | * {@code cn=monitor} and the searches of {@code GroupManager} and {@code SubentryManager} read it |
| | | * for a client. |
| | | * <p> |
| | | * The read behind {@code NOTE_BACKEND_STARTED} is deliberately left with the client callers, |
| | | * though nobody waits on it either: it goes through {@code BackendImpl.getEntryCount()}, which is |
| | | * the same method those three call, and what a bound costs it there is a log line reporting -1 |
| | | * entries - that method answers -1 for any failure rather than failing the open. |
| | | * |
| | | * @param txn storage transaction |
| | | * @param partOfAWholeTreeWalk whether this read belongs to a walk of a whole tree rather than to |
| | | * a client operation |
| | | * @return The number of entries stored in this entry container including the baseDN. |
| | | * @see ReadableTransaction#openBulkCursor(TreeName) |
| | | */ |
| | | long getNumberOfEntriesInBaseDN0(ReadableTransaction txn, boolean partOfAWholeTreeWalk) |
| | | { |
| | | return id2childrenCount.getTotalCount(txn, partOfAWholeTreeWalk); |
| | | } |
| | | |
| | | /** |
| | |
| | | * |
| | | * Copyright 2006-2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2012-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | |
| | | * @throws LDIFException If an error occurs while trying to determine |
| | | * whether to write an entry. |
| | | */ |
| | | private void exportContainer(ReadableTransaction txn, EntryContainer entryContainer) |
| | | // Visible to the test that pins the class of the cursor opened here: a walk of the whole of |
| | | // id2entry with nobody waiting on it, which a storage engine that bounds a statement must not |
| | | // bound as it bounds an operation (#877). |
| | | void exportContainer(ReadableTransaction txn, EntryContainer entryContainer) |
| | | throws StorageRuntimeException, IOException, LDIFException |
| | | { |
| | | ID2Entry id2entry = entryContainer.getID2Entry(); |
| | | try (final Cursor<ByteString, ByteString> cursor = txn.openCursor(id2entry.getName())) |
| | | // The whole of id2entry with nobody waiting on the walk: an export-ldif, or the generation ID |
| | | // a replicated domain computes for itself the first time it starts (LDAPReplicationDomain |
| | | // .computeGenerationId), which is why this must not be bounded as the work of an operation. |
| | | try (final Cursor<ByteString, ByteString> cursor = txn.openBulkCursor(id2entry.getName())) |
| | | { |
| | | while (cursor.next()) |
| | | { |
| | |
| | | this.counter = new ShardedCounter(name); |
| | | } |
| | | |
| | | SequentialCursor<EntryID, Void> openCursor(ReadableTransaction txn) |
| | | /** |
| | | * Walks the children counts whole, which {@code verify-index} does and no client operation does: |
| | | * there is no overload of this method that would take the bound of an operation by accident. |
| | | * Reading the count of a single entry is another matter - see {@link #getCount}. |
| | | * |
| | | * @see ReadableTransaction#openBulkCursor(TreeName) |
| | | */ |
| | | SequentialCursor<EntryID, Void> openBulkCursor(ReadableTransaction txn) |
| | | { |
| | | return transformKeysAndValues(counter.openCursor(txn), |
| | | return transformKeysAndValues(counter.openBulkCursor(txn), |
| | | TO_ENTRY_ID, CursorTransformer.<ByteString, Void> keepValuesUnchanged()); |
| | | } |
| | | |
| | |
| | | */ |
| | | long getCount(ReadableTransaction txn, EntryID entryID) |
| | | { |
| | | return counter.getCount(txn, toKey(entryID)); |
| | | return getCount(txn, entryID, false); |
| | | } |
| | | |
| | | /** |
| | | * Get the number of children for the given entry, as part of a walk of a whole tree rather than |
| | | * of a client operation. {@code verify-index} reads one of these per DN while it walks dn2id |
| | | * whole, and no client is waiting on any of them. |
| | | * |
| | | * @param txn storage transaction |
| | | * @param entryID The entryID identifying to the counter |
| | | * @param partOfAWholeTreeWalk whether this read belongs to a walk of a whole tree |
| | | * @return Value of the counter. 0 if no counter is associated yet. |
| | | * @see ReadableTransaction#openBulkCursor(TreeName) |
| | | */ |
| | | long getCount(ReadableTransaction txn, EntryID entryID, boolean partOfAWholeTreeWalk) |
| | | { |
| | | return counter.getCount(txn, toKey(entryID), partOfAWholeTreeWalk); |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | long getTotalCount(ReadableTransaction txn) |
| | | { |
| | | return getCount(txn, TOTAL_COUNT_ENTRY_ID); |
| | | return getTotalCount(txn, false); |
| | | } |
| | | |
| | | /** |
| | | * The same total, told which kind of work it is part of. It is a read of this tree like any |
| | | * other - a cursor positioned on one key, which on a storage engine that walks a table rather |
| | | * than an index is a scan of it - so what it may take follows who is waiting on it: |
| | | * {@code verify-index} reads it once to size the progress report of a walk of the whole backend, |
| | | * with nobody waiting, while {@code cn=monitor} and the searches of {@code GroupManager} and |
| | | * {@code SubentryManager} read the same total for a client. |
| | | * |
| | | * @param txn storage transaction |
| | | * @param partOfAWholeTreeWalk whether this read belongs to a walk of a whole tree rather than to |
| | | * a client operation |
| | | * @return Sum of all the counter contained in this tree |
| | | * @see ReadableTransaction#openBulkCursor(TreeName) |
| | | */ |
| | | long getTotalCount(ReadableTransaction txn, boolean partOfAWholeTreeWalk) |
| | | { |
| | | return getCount(txn, TOTAL_COUNT_ENTRY_ID, partOfAWholeTreeWalk); |
| | | } |
| | | |
| | | /** |
| | |
| | | { |
| | | // Make sure the tree is there and readable, even if the storage is READ_ONLY. |
| | | // Would be nice if there were a better way... |
| | | try (final Cursor<ByteString, ByteString> cursor = txn.openCursor(getName())) |
| | | // Bulk: the first batch of a cursor carries no seek predicate, so this is a walk of the whole |
| | | // tree as far as the storage is concerned, and it runs on every open of the backend. A bound |
| | | // meant for an entry read would keep a large backend from opening at all on an engine where |
| | | // such a batch is not a step along an index. |
| | | try (final Cursor<ByteString, ByteString> cursor = txn.openBulkCursor(getName())) |
| | | { |
| | | cursor.next(); |
| | | } |
| | |
| | | |
| | | /** |
| | | * Check that a record entry exists in the entry tree. |
| | | * <p> |
| | | * Bulk, like the walk it belongs to: {@code VerifyJob.iterateID2ChildrenCount()} is its one |
| | | * caller and asks this once per record of the children count tree, inside a cursor over the |
| | | * whole of it. Read as a client operation those would put the bound of an entry read over a |
| | | * job nobody is waiting on, once per record - the same hazard the walk around them was given |
| | | * {@link ReadableTransaction#openBulkCursor(TreeName)} for (#877). |
| | | * |
| | | * @param txn a non null transaction |
| | | * @param entryID The entry ID which forms the key. |
| | |
| | | { |
| | | checkNotNull(txn, "txn must not be null"); |
| | | checkNotNull(entryID, "entryID must not be null"); |
| | | try(final Cursor<ByteString, ByteString> cursor = txn.openCursor(getName())) { |
| | | try(final Cursor<ByteString, ByteString> cursor = txn.openBulkCursor(getName())) { |
| | | return cursor.positionToKey(entryID.toByteString()); |
| | | } |
| | | } |
| | |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2012-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | |
| | | |
| | | Cursor<ByteString, EntryIDSet> openCursor(ReadableTransaction txn); |
| | | |
| | | /** |
| | | * Opens a cursor over the whole index for a task no client operation is waiting on, such as |
| | | * {@code verify-index} or {@code dbtest}. |
| | | * <p> |
| | | * Abstract rather than a {@code default} answering as {@link #openCursor(ReadableTransaction)} |
| | | * does, which is the compatibility the SPI needs for engines outside this repository: this |
| | | * interface is package-private with one implementor, and a second one inheriting that default |
| | | * would silently walk a whole index under the bound of a client operation. A compile error is |
| | | * the better answer here. |
| | | * |
| | | * @param txn |
| | | * the transaction to read the index with |
| | | * @return a cursor over every key of this index |
| | | * @see ReadableTransaction#openBulkCursor(org.opends.server.backends.pluggable.spi.TreeName) |
| | | */ |
| | | Cursor<ByteString, EntryIDSet> openBulkCursor(ReadableTransaction txn); |
| | | |
| | | boolean setIndexEntryLimit(int indexEntryLimit); |
| | | |
| | | boolean setConfidential(boolean indexConfidential); |
| | |
| | | return 0; |
| | | } |
| | | long copied = 0; |
| | | try (Cursor<ByteString, ByteString> cursor = txn.openCursor(from)) |
| | | try (Cursor<ByteString, ByteString> cursor = txn.openBulkCursor(from)) |
| | | { |
| | | while (cursor.next()) |
| | | { |
| | |
| | | // Cursor through the object class database and load the object class set |
| | | // definitions. At the same time, figure out the highest token value and |
| | | // initialize the object class counter to one greater than that. |
| | | // Both trees are read whole while the backend opens, with no client operation waiting on it. |
| | | if (txn.treeExists(ocTree)) |
| | | { |
| | | try (Cursor<ByteString, ByteString> ocCursor = txn.openCursor(ocTree)) |
| | | try (Cursor<ByteString, ByteString> ocCursor = txn.openBulkCursor(ocTree)) |
| | | { |
| | | while (ocCursor.next()) |
| | | { |
| | |
| | | // Cursor through the attribute description database and load the attribute set definitions. |
| | | if (txn.treeExists(adTree)) |
| | | { |
| | | try (Cursor<ByteString, ByteString> adCursor = txn.openCursor(adTree)) |
| | | try (Cursor<ByteString, ByteString> adCursor = txn.openBulkCursor(adTree)) |
| | | { |
| | | while (adCursor.next()) |
| | | { |
| | |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2015 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | |
| | | super(name); |
| | | } |
| | | |
| | | SequentialCursor<ByteString, Void> openCursor(ReadableTransaction txn) |
| | | /** |
| | | * Walks this counter whole, which {@code verify-index} does and no client operation does: there |
| | | * is no overload of this method that would take the bound of an operation by accident. Reading a |
| | | * single counter is another matter - {@link #getCount} opens a cursor of its own, and one of a |
| | | * client operation unless the caller says otherwise. |
| | | * |
| | | * @see ReadableTransaction#openBulkCursor(TreeName) |
| | | */ |
| | | SequentialCursor<ByteString, Void> openBulkCursor(ReadableTransaction txn) |
| | | { |
| | | return uniqueKeys(txn.openBulkCursor(getName())); |
| | | } |
| | | |
| | | private SequentialCursor<ByteString, Void> uniqueKeys(Cursor<ByteString, ByteString> cursor) |
| | | { |
| | | return new UniqueKeysCursor<>(transformKeysAndValues( |
| | | txn.openCursor(getName()), TO_KEY, |
| | | cursor, TO_KEY, |
| | | CursorTransformer.<ByteString, ByteString, Void> constant(null))); |
| | | } |
| | | |
| | | private Cursor<ByteString, Long> openCursor0(ReadableTransaction txn) |
| | | private Cursor<ByteString, Long> openCursor0(ReadableTransaction txn, boolean partOfAWholeTreeWalk) |
| | | { |
| | | return transformKeysAndValues(txn.openCursor(getName()), TO_KEY, TO_LONG); |
| | | return transformKeysAndValues( |
| | | partOfAWholeTreeWalk ? txn.openBulkCursor(getName()) : txn.openCursor(getName()), TO_KEY, TO_LONG); |
| | | } |
| | | |
| | | void addCount(final WriteableTransaction txn, ByteSequence key, final long delta) |
| | |
| | | |
| | | long getCount(final ReadableTransaction txn, ByteSequence key) |
| | | { |
| | | return getCount(txn, key, false); |
| | | } |
| | | |
| | | /** |
| | | * The same read, told which kind of work it is part of. A client operation reads a counter of its |
| | | * own and takes the bound of one - {@code numSubordinates} of a search |
| | | * ({@code EntryContainer.getNumberOfChildren}), the entry count of a VLV index a search is paging |
| | | * through ({@code VLVIndex.getEntryCount}) - while {@code verify-index} reads one per DN of the |
| | | * tree it is walking, with nobody waiting on it: bounding those as client operations is what #877 |
| | | * exists to stop, and on the JDBC backend it aborted a verify of a backend large enough. |
| | | * <p> |
| | | * The third caller is {@code ID2ChildrenCount.getTotalCount}, which is read both ways and is told |
| | | * which it is by its own caller: a verify sizes its progress report with it, {@code cn=monitor} |
| | | * and the searches of {@code GroupManager} and {@code SubentryManager} read it for a client. A |
| | | * delete and a modify DN reach neither form - they go through {@link #removeCount}, which is a |
| | | * client operation by construction. |
| | | * |
| | | * @param txn storage transaction |
| | | * @param key the counter to read |
| | | * @param partOfAWholeTreeWalk whether this read belongs to a walk of a whole tree rather than to |
| | | * a client operation |
| | | * @return Value of the counter. 0 if no counter is associated yet. |
| | | * @see ReadableTransaction#openBulkCursor(TreeName) |
| | | */ |
| | | long getCount(final ReadableTransaction txn, ByteSequence key, boolean partOfAWholeTreeWalk) |
| | | { |
| | | long counterValue = 0; |
| | | try (final SequentialCursor<ByteString, Long> cursor = new ShardCursor(openCursor0(txn), key)) |
| | | try (final SequentialCursor<ByteString, Long> cursor = |
| | | new ShardCursor(openCursor0(txn, partOfAWholeTreeWalk), key)) |
| | | { |
| | | while (cursor.next()) |
| | | { |
| | |
| | | long removeCount(final WriteableTransaction txn, ByteSequence key) |
| | | { |
| | | long counterValue = 0; |
| | | try (final SequentialCursor<ByteString, Long> cursor = new ShardCursor(openCursor0(txn), key)) |
| | | // a removal is always a client operation: an entry is being deleted or moved |
| | | try (final SequentialCursor<ByteString, Long> cursor = new ShardCursor(openCursor0(txn, false), key)) |
| | | { |
| | | // Iterate over and remove all the thread local shards |
| | | while (cursor.next()) |
| | |
| | | } |
| | | |
| | | @Override |
| | | public Cursor<ByteString, ByteString> openBulkCursor(final TreeName name) |
| | | { |
| | | traceEnter("openBulkCursor", "name", name); |
| | | final Cursor<ByteString, ByteString> cursor = txn.openBulkCursor(name); |
| | | traceLeave("openBulkCursor", "name", name); |
| | | return new TracedCursor(cursor); |
| | | } |
| | | |
| | | @Override |
| | | public ByteString read(final TreeName name, final ByteSequence key) |
| | | { |
| | | traceEnter("read", "name", name, "key", hex(key)); |
| | |
| | | } |
| | | |
| | | @Override |
| | | public Cursor<ByteString, ByteString> openBulkCursor(final TreeName name) |
| | | { |
| | | traceEnter("openBulkCursor", "name", name); |
| | | final Cursor<ByteString, ByteString> cursor = txn.openBulkCursor(name); |
| | | traceLeave("openBulkCursor", "name", name); |
| | | return new TracedCursor(cursor); |
| | | } |
| | | |
| | | @Override |
| | | public void openTree(final TreeName name, boolean createOnDemand) |
| | | { |
| | | traceEnter("openTree", "name", name, "createOnDemand", createOnDemand); |
| | |
| | | /** Indicates whether the children count tree is to be verified. */ |
| | | private boolean verifyID2ChildrenCount; |
| | | |
| | | // The trees below, and the iterate* methods that walk them, are visible to the test pinning the |
| | | // class of the cursors they open: every one of these walks a tree whole with nobody waiting on |
| | | // it, which a storage engine that bounds a statement must not bound as an operation (#877). |
| | | /** The entry tree. */ |
| | | private ID2Entry id2entry; |
| | | ID2Entry id2entry; |
| | | /** The DN tree. */ |
| | | private DN2ID dn2id; |
| | | DN2ID dn2id; |
| | | /** The children tree. */ |
| | | private ID2ChildrenCount id2childrenCount; |
| | | ID2ChildrenCount id2childrenCount; |
| | | |
| | | /** A list of the attribute indexes to be verified. */ |
| | | private final ArrayList<AttributeIndex> attrIndexList = new ArrayList<>(); |
| | |
| | | * |
| | | * @throws StorageRuntimeException If an error occurs in the storage. |
| | | */ |
| | | private void iterateID2Entry(ReadableTransaction txn) throws StorageRuntimeException |
| | | void iterateID2Entry(ReadableTransaction txn) throws StorageRuntimeException |
| | | { |
| | | try(final Cursor<ByteString, ByteString> cursor = txn.openCursor(id2entry.getName())) |
| | | // Every tree this job walks, it walks whole, and no client operation is waiting on it. |
| | | try(final Cursor<ByteString, ByteString> cursor = txn.openBulkCursor(id2entry.getName())) |
| | | { |
| | | long storedEntryCount = id2entry.getRecordCount(txn); |
| | | while (cursor.next()) |
| | |
| | | * |
| | | * @throws StorageRuntimeException If an error occurs in the storage. |
| | | */ |
| | | private void iterateDN2ID(ReadableTransaction txn) throws StorageRuntimeException |
| | | void iterateDN2ID(ReadableTransaction txn) throws StorageRuntimeException |
| | | { |
| | | final Deque<ChildrenCount> childrenCounters = new LinkedList<>(); |
| | | ChildrenCount currentNode = null; |
| | | |
| | | try(final Cursor<ByteString, ByteString> cursor = txn.openCursor(dn2id.getName())) |
| | | try(final Cursor<ByteString, ByteString> cursor = txn.openBulkCursor(dn2id.getName())) |
| | | { |
| | | while (cursor.next()) |
| | | { |
| | |
| | | |
| | | private void verifyID2ChildrenCount(ReadableTransaction txn, ChildrenCount parent) { |
| | | final long expected = parent.numberOfChildren; |
| | | final long currentValue = id2childrenCount.getCount(txn, parent.entryID); |
| | | // Part of the walk of dn2id above, and bounded as one: this runs once per DN of the tree, so |
| | | // reading it as a client operation would put the bound of an entry read over a read of a |
| | | // backend nobody is waiting on - which on the JDBC backend aborts a verify of a large one. |
| | | final long currentValue = id2childrenCount.getCount(txn, parent.entryID, true); |
| | | if (expected != currentValue) |
| | | { |
| | | errorCount++; |
| | |
| | | |
| | | private void iterateID2ChildrenCount(ReadableTransaction txn) throws StorageRuntimeException |
| | | { |
| | | try (final SequentialCursor<EntryID, Void> cursor = id2childrenCount.openCursor(txn)) |
| | | try (final SequentialCursor<EntryID, Void> cursor = id2childrenCount.openBulkCursor(txn)) |
| | | { |
| | | while (cursor.next()) |
| | | { |
| | |
| | | * @throws StorageRuntimeException If an error occurs in the storage. |
| | | * @throws DirectoryException If an error occurs reading values in the index. |
| | | */ |
| | | private void iterateVLVIndex(ReadableTransaction txn, VLVIndex vlvIndex, boolean verifyID) |
| | | void iterateVLVIndex(ReadableTransaction txn, VLVIndex vlvIndex, boolean verifyID) |
| | | throws StorageRuntimeException, DirectoryException |
| | | { |
| | | if(vlvIndex == null || !verifyID) |
| | |
| | | return; |
| | | } |
| | | |
| | | try(final Cursor<ByteString, ByteString> cursor = txn.openCursor(vlvIndex.getName())) |
| | | try(final Cursor<ByteString, ByteString> cursor = txn.openBulkCursor(vlvIndex.getName())) |
| | | { |
| | | while (cursor.next()) |
| | | { |
| | |
| | | return; |
| | | } |
| | | |
| | | try(final Cursor<ByteString,EntryIDSet> cursor = index.openCursor(txn)) |
| | | try(final Cursor<ByteString,EntryIDSet> cursor = index.openBulkCursor(txn)) |
| | | { |
| | | while (cursor.next()) |
| | | { |
| | |
| | | } |
| | | } |
| | | |
| | | /** This class reports progress of the verify job at fixed intervals. */ |
| | | private final class ProgressTask extends TimerTask |
| | | /** |
| | | * This class reports progress of the verify job at fixed intervals. |
| | | * <p> |
| | | * Visible, with its constructor, to the test pinning the class of the reads it makes to size |
| | | * that report: they belong to the walk they measure rather than to a client operation (#877). |
| | | */ |
| | | final class ProgressTask extends TimerTask |
| | | { |
| | | /** The total number of records to process. */ |
| | | private long totalCount; |
| | |
| | | * through indexes or the entries. |
| | | * @throws StorageRuntimeException An error occurred while accessing the storage. |
| | | */ |
| | | private ProgressTask(boolean indexIterator, ReadableTransaction txn) throws StorageRuntimeException |
| | | ProgressTask(boolean indexIterator, ReadableTransaction txn) throws StorageRuntimeException |
| | | { |
| | | previousTime = System.currentTimeMillis(); |
| | | |
| | |
| | | } |
| | | else |
| | | { |
| | | totalCount = rootContainer.getEntryContainer(verifyConfig.getBaseDN()).getNumberOfEntriesInBaseDN0(txn); |
| | | // Part of this walk, like the counts of the branch above: it sizes a verify of the whole |
| | | // backend, and the branch above is only reached by "verify-index --clean" - a plain |
| | | // verify-index, with or without an index named, comes here. Read as a client operation it |
| | | // would take the bound of one on a storage engine that bounds a statement, which on a |
| | | // backend large enough aborts the verify before its first record (#877). |
| | | totalCount = rootContainer.getEntryContainer(verifyConfig.getBaseDN()).getNumberOfEntriesInBaseDN0(txn, true); |
| | | } |
| | | } |
| | | |
| | |
| | | Cursor<ByteString, ByteString> openCursor(TreeName treeName); |
| | | |
| | | /** |
| | | * Opens a cursor on the tree whose name is provided, for a walk of that whole tree with no |
| | | * client operation waiting on it: an export, a verify, a rebuild, or the load of a tree while |
| | | * the backend opens. |
| | | * <p> |
| | | * A storage engine that bounds how long a statement may take must not bound such a walk as it |
| | | * bounds the work of a client operation: what this legitimately takes follows the size of the |
| | | * tree, and cutting it short fails an administrative task that would otherwise have run to the |
| | | * end. An engine with no such bound - every one but the JDBC backend - answers this exactly as |
| | | * {@link #openCursor(TreeName)} does. |
| | | * |
| | | * @param treeName |
| | | * the tree name |
| | | * @return a new cursor |
| | | */ |
| | | default Cursor<ByteString, ByteString> openBulkCursor(TreeName treeName) |
| | | { |
| | | return openCursor(treeName); |
| | | } |
| | | |
| | | /** |
| | | * Returns the number of key/value pairs in the provided tree. |
| | | * |
| | | * @param treeName |
| New file |
| | |
| | | /* |
| | | * 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.i18n.LocalizableMessage; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.server.config.server.JDBCBackendCfg; |
| | | import org.mockito.InOrder; |
| | | import org.mockito.invocation.InvocationOnMock; |
| | | import org.mockito.stubbing.Answer; |
| | | import org.opends.server.DirectoryServerTestCase; |
| | | import org.opends.server.backends.jdbc.JDBCStorage.StatementBound; |
| | | import org.opends.server.backends.pluggable.spi.AccessMode; |
| | | import org.opends.server.backends.pluggable.spi.ReadOnlyStorageException; |
| | | 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.testng.annotations.AfterMethod; |
| | | import org.testng.annotations.BeforeMethod; |
| | | import org.testng.annotations.Test; |
| | | |
| | | import java.sql.Connection; |
| | | import java.sql.PreparedStatement; |
| | | import java.sql.ResultSet; |
| | | import java.sql.SQLException; |
| | | import java.sql.SQLFeatureNotSupportedException; |
| | | import java.sql.SQLTimeoutException; |
| | | import java.util.concurrent.CountDownLatch; |
| | | import java.util.concurrent.Executor; |
| | | import java.util.concurrent.TimeUnit; |
| | | import java.util.concurrent.atomic.AtomicInteger; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | import java.util.regex.Matcher; |
| | | import java.util.regex.Pattern; |
| | | |
| | | import static java.util.Collections.singletonList; |
| | | 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.anyString; |
| | | import static org.mockito.Mockito.atLeastOnce; |
| | | import static org.mockito.Mockito.doThrow; |
| | | import static org.mockito.Mockito.eq; |
| | | import static org.mockito.Mockito.inOrder; |
| | | 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.mockito.Mockito.times; |
| | | import static org.testng.Assert.assertEquals; |
| | | import static org.testng.Assert.assertFalse; |
| | | import static org.testng.Assert.assertSame; |
| | | import static org.testng.Assert.assertTrue; |
| | | import static org.testng.Assert.fail; |
| | | |
| | | /** |
| | | * Which bound a statement of the JDBC backend is given, and what reaching it looks like to the |
| | | * caller (#877). Needs no database: the statement is a mock, so the policy is pinned wherever the |
| | | * build runs, while the container suites cover a statement really blocked on a lock. |
| | | */ |
| | | @SuppressWarnings("javadoc") |
| | | // Every wait of this suite is bounded where it is taken - awaitOrFail() and Background.joinOrFail() |
| | | // - rather than by a timeOut on this annotation: a method carrying a @Test of its own replaces the |
| | | // one of the class outright, only the groups of the two being merged, so a timeOut declared here |
| | | // would bound nothing. A statement of another thread that never arrives fails this suite there. |
| | | @Test(groups = { "precommit", "jdbc" }, sequential = true) |
| | | public class JDBCStatementBoundTestCase extends DirectoryServerTestCase { |
| | | |
| | | private JDBCStorage storage; |
| | | |
| | | /** |
| | | * A storage of its own for every test. Not merely tidy: a storage carries latches that are |
| | | * meant to be one-shot for its whole life - the driver having no network timeout, and the |
| | | * warnings for that and for a refused query timeout - so a test that trips one on a shared |
| | | * instance would leave {@code applyBackstop()} returning at its first guard for every test |
| | | * after it, turning their {@code verify(con, never())} assertions green on a run that reached |
| | | * nothing. Constructing one costs a mock configuration and no connection at all. |
| | | */ |
| | | @BeforeMethod |
| | | public void createStorage() { |
| | | storage = new JDBCStorage(mockCfg(JDBCBackendCfg.class), null); |
| | | } |
| | | |
| | | @AfterMethod |
| | | public void clearProperties() { |
| | | for (final StatementBound bound : StatementBound.values()) { |
| | | System.clearProperty(bound.property); |
| | | } |
| | | System.clearProperty(JDBCStorage.STATISTICS_TIMEOUT_PROPERTY); |
| | | storage.accessMode = AccessMode.READ_ONLY; // an import test opens it for writing |
| | | } |
| | | |
| | | /** How long a test waits for a statement running on another thread before it fails. */ |
| | | private static final long WAIT_MILLIS = 30000; |
| | | |
| | | /** |
| | | * A storage whose clock the test moves by hand. Whether a failure is the bound arriving turns |
| | | * on a few milliseconds either side of it, and a sleep cannot pin that: a loaded box lengthens |
| | | * one, so a test that oversleeps passes whether the slack under the bound exists or not - and |
| | | * a bound of a second costs the suite a second of waiting to say so. |
| | | */ |
| | | private static final class SteppedClockStorage extends JDBCStorage { |
| | | /** Milliseconds since the statement under test started, as the classification will see it. */ |
| | | volatile long millis; |
| | | |
| | | SteppedClockStorage() { |
| | | super(mockCfg(JDBCBackendCfg.class), null); |
| | | } |
| | | |
| | | @Override |
| | | long nanoTime() { |
| | | return millis * 1000000L; |
| | | } |
| | | } |
| | | |
| | | private static void awaitOrFail(CountDownLatch latch, String what) throws InterruptedException { |
| | | assertTrue(latch.await(WAIT_MILLIS, TimeUnit.MILLISECONDS), what + " within " + WAIT_MILLIS + " ms"); |
| | | } |
| | | |
| | | private static void sleep(long millis) { |
| | | try { |
| | | Thread.sleep(millis); |
| | | } catch (InterruptedException e) { |
| | | Thread.currentThread().interrupt(); |
| | | } |
| | | } |
| | | |
| | | /** A statement that reports it is running and then waits for the test to let it finish. */ |
| | | private PreparedStatement lingering(Connection con, CountDownLatch running, CountDownLatch mayFinish) |
| | | throws SQLException { |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | when(statement.getConnection()).thenReturn(con); |
| | | when(statement.executeUpdate()).thenAnswer(new Answer<Integer>() { |
| | | @Override |
| | | public Integer answer(InvocationOnMock invocation) throws Throwable { |
| | | running.countDown(); |
| | | awaitOrFail(mayFinish, "the statement was never let go"); |
| | | return 1; |
| | | } |
| | | }); |
| | | return statement; |
| | | } |
| | | |
| | | private interface Execution { |
| | | void run() throws Exception; |
| | | } |
| | | |
| | | /** |
| | | * A statement running on a thread of its own, with whatever it threw kept for the assertion: |
| | | * {@code Thread.join()} does not rethrow, so a failure in the background would otherwise leave |
| | | * the verifications of a test passing on a run that never reached the state they check. |
| | | */ |
| | | private static final class Background { |
| | | final Thread thread; |
| | | final AtomicReference<Throwable> failure = new AtomicReference<>(); |
| | | |
| | | Background(String name, final Execution execution) { |
| | | thread = new Thread(new Runnable() { |
| | | @Override |
| | | public void run() { |
| | | try { |
| | | execution.run(); |
| | | } catch (Throwable t) { |
| | | failure.set(t); |
| | | } |
| | | } |
| | | }, name); |
| | | } |
| | | |
| | | void joinOrFail() throws Exception { |
| | | thread.join(WAIT_MILLIS); |
| | | assertFalse(thread.isAlive(), thread.getName() + " did not finish within " + WAIT_MILLIS + " ms"); |
| | | final Throwable thrown = failure.get(); |
| | | if (thrown instanceof Exception) { |
| | | throw (Exception) thrown; |
| | | } |
| | | if (thrown != null) { |
| | | throw new AssertionError(thrown); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private static Background start(String name, Execution execution) { |
| | | final Background background = new Background(name, execution); |
| | | background.thread.start(); |
| | | return background; |
| | | } |
| | | |
| | | /** |
| | | * An entry read that has not come back in two minutes is stuck, while a count or the delete |
| | | * that empties a tree before an import legitimately takes longer than anything can guess - so |
| | | * the bulk class stays unbounded until a deployment says otherwise. |
| | | */ |
| | | @Test |
| | | public void testDefaultsBoundAnOperationAndLeaveBulkAlone() throws Exception { |
| | | assertEquals(StatementBound.OPERATION.seconds(), 120); |
| | | assertEquals(StatementBound.BULK.seconds(), 0); |
| | | } |
| | | |
| | | @Test |
| | | public void testEachClassIsConfiguredByItsOwnProperty() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "7"); |
| | | assertEquals(StatementBound.OPERATION.seconds(), 7); |
| | | assertEquals(StatementBound.BULK.seconds(), 0, "the bulk class followed the operation one"); |
| | | |
| | | System.setProperty(StatementBound.BULK.property, "900"); |
| | | assertEquals(StatementBound.BULK.seconds(), 900); |
| | | assertEquals(StatementBound.OPERATION.seconds(), 7); |
| | | } |
| | | |
| | | @Test |
| | | public void testAValueThatIsNoBoundLeavesTheStatementUnbounded() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "0"); |
| | | assertEquals(StatementBound.OPERATION.seconds(), 0); |
| | | |
| | | System.setProperty(StatementBound.OPERATION.property, "-1"); |
| | | assertEquals(StatementBound.OPERATION.seconds(), 0); |
| | | } |
| | | |
| | | /** |
| | | * A value that is not a number is not a way to switch the bound off: it is ignored in favour |
| | | * of the default, as {@code Integer.getInteger()} has it, so a typo leaves the class bounded |
| | | * rather than silently unbounding it. |
| | | */ |
| | | @Test |
| | | public void testAValueThatIsNotANumberFallsBackToTheDefault() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "two minutes"); |
| | | assertEquals(StatementBound.OPERATION.seconds(), 120); |
| | | |
| | | // from a bound that took, so that the fallback is what the assertion below can be seeing: |
| | | // the default of this class is 0, which is also what a value read as a number would give |
| | | System.setProperty(StatementBound.BULK.property, "900"); |
| | | assertEquals(StatementBound.BULK.seconds(), 900); |
| | | System.setProperty(StatementBound.BULK.property, "as long as it takes"); |
| | | assertEquals(StatementBound.BULK.seconds(), 0); |
| | | } |
| | | |
| | | /** |
| | | * A bound larger than the second layer can hold is taken down to what it can hold, and stays a |
| | | * bound: that layer is a socket read timeout, which is milliseconds of an {@code int}, so |
| | | * {@code Integer.MAX_VALUE} - the usual "no bound" idiom - has no value of it to be given, and |
| | | * a negative one is a call every driver refuses, leaving the connection carrying whatever the |
| | | * statement before it armed. {@code 0} is what says "no bound" here, and the ceiling is not it. |
| | | */ |
| | | @Test |
| | | public void testABoundLargerThanTheBackstopCanHoldIsTakenDownToIt() throws Exception { |
| | | assertEquals(JDBCStorage.clampSeconds(Integer.MAX_VALUE), JDBCStorage.MAX_BOUND_SECONDS); |
| | | System.setProperty(StatementBound.OPERATION.property, String.valueOf(Integer.MAX_VALUE)); |
| | | assertEquals(StatementBound.OPERATION.seconds(), JDBCStorage.MAX_BOUND_SECONDS); |
| | | |
| | | final Connection con = mock(Connection.class); |
| | | when(con.getNetworkTimeout()).thenReturn(0); |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | when(statement.getConnection()).thenReturn(con); |
| | | |
| | | storage.execute(statement); |
| | | |
| | | verify(statement).setQueryTimeout(JDBCStorage.MAX_BOUND_SECONDS); |
| | | // and what the second layer is armed with is still a positive int of milliseconds, which is |
| | | // the whole of what the ceiling is for: a negative one is the call a driver refuses |
| | | verify(con).setNetworkTimeout(any(Executor.class), |
| | | eq((JDBCStorage.MAX_BOUND_SECONDS + JDBCStorage.BACKSTOP_MARGIN_SECONDS) * 1000)); |
| | | } |
| | | |
| | | @Test |
| | | public void testTheBoundReachesTheStatement() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "7"); |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | |
| | | storage.execute(statement); |
| | | |
| | | verify(statement).setQueryTimeout(7); |
| | | } |
| | | |
| | | /** An unbounded class costs no call of its own: a fresh statement is unbounded already. */ |
| | | @Test |
| | | public void testAnUnboundedClassSetsNothing() throws Exception { |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | |
| | | storage.execute(statement, StatementBound.BULK); |
| | | |
| | | verify(statement, never()).setQueryTimeout(anyInt()); |
| | | } |
| | | |
| | | /** |
| | | * Behind the cancel is a socket read timeout, for the databases that do not act on a cancel: |
| | | * it is armed for the statement and put back once nothing is running on the connection any |
| | | * more. What a connection carrying several statements at once does with it is pinned by the |
| | | * three tests below. |
| | | */ |
| | | @Test |
| | | public void testTheBackstopIsArmedAndPutBack() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "7"); |
| | | final Connection con = mock(Connection.class); |
| | | when(con.getNetworkTimeout()).thenReturn(0); // no bound of its own |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | when(statement.getConnection()).thenReturn(con); |
| | | |
| | | storage.execute(statement); |
| | | |
| | | final InOrder inOrder = inOrder(con); |
| | | inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq((7 + JDBCStorage.BACKSTOP_MARGIN_SECONDS) * 1000)); |
| | | inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq(0)); |
| | | } |
| | | |
| | | /** |
| | | * The backstop only ever tightens. A read timeout a deployment gave its connections is the |
| | | * bound it asked for, and this one - deliberately the looser of the two, so that the cancel |
| | | * has room to arrive first - must not stand in for it while a statement runs. |
| | | */ |
| | | @Test |
| | | public void testTheBackstopDoesNotLoosenATighterBound() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "7"); |
| | | final Connection con = mock(Connection.class); |
| | | when(con.getNetworkTimeout()).thenReturn(5000); // tighter than 7s plus the margin |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | when(statement.getConnection()).thenReturn(con); |
| | | |
| | | storage.execute(statement); |
| | | |
| | | verify(con, never()).setNetworkTimeout(any(Executor.class), anyInt()); |
| | | } |
| | | |
| | | /** |
| | | * A statement of a class that carries no bound takes the backstop off the connection for as |
| | | * long as it runs. The socket read timeout is a property of the connection, and an importer |
| | | * writes to a single one from every phase-one worker and every phase-two task, so the bulk |
| | | * {@code delete from} that empties a tree would otherwise be cut at the bound of an entry read |
| | | * happening to run beside it - and cut without ever naming a property, since a statement of an |
| | | * unbounded class has none to name. Two threads, because that is how the two meet. |
| | | */ |
| | | @Test |
| | | public void testAnUnboundedStatementTakesTheBackstopOffWhileItRuns() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "7"); |
| | | System.setProperty(StatementBound.BULK.property, "0"); |
| | | final Connection con = mock(Connection.class); |
| | | when(con.getNetworkTimeout()).thenReturn(0); |
| | | final CountDownLatch operationRunning = new CountDownLatch(1); |
| | | final CountDownLatch operationMayFinish = new CountDownLatch(1); |
| | | final CountDownLatch bulkRunning = new CountDownLatch(1); |
| | | final CountDownLatch bulkMayFinish = new CountDownLatch(1); |
| | | final PreparedStatement operation = lingering(con, operationRunning, operationMayFinish); |
| | | final PreparedStatement bulk = lingering(con, bulkRunning, bulkMayFinish); |
| | | |
| | | final Background entryRead = start("entry-read", () -> storage.execute(operation)); |
| | | awaitOrFail(operationRunning, "the entry read never started"); |
| | | final Background clearTree = start("clear-tree", () -> storage.execute(bulk, StatementBound.BULK)); |
| | | awaitOrFail(bulkRunning, "the bulk statement never started"); |
| | | bulkMayFinish.countDown(); |
| | | clearTree.joinOrFail(); |
| | | operationMayFinish.countDown(); |
| | | entryRead.joinOrFail(); |
| | | |
| | | final InOrder inOrder = inOrder(con); |
| | | inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq((7 + JDBCStorage.BACKSTOP_MARGIN_SECONDS) * 1000)); |
| | | 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(0)); // and the entry read is through |
| | | } |
| | | |
| | | /** |
| | | * 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. |
| | | */ |
| | | @Test |
| | | public void testTheBackstopOutlastsTheStatementThatArmedIt() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "7"); |
| | | final Connection con = mock(Connection.class); |
| | | when(con.getNetworkTimeout()).thenReturn(0); |
| | | final CountDownLatch running = new CountDownLatch(1); |
| | | final CountDownLatch mayFinish = new CountDownLatch(1); |
| | | final PreparedStatement lingering = lingering(con, running, mayFinish); |
| | | final PreparedStatement passing = mock(PreparedStatement.class); |
| | | when(passing.getConnection()).thenReturn(con); |
| | | when(passing.executeUpdate()).thenReturn(1); |
| | | |
| | | final Background outliving = start("outliving", () -> storage.execute(lingering)); |
| | | awaitOrFail(running, "the statement that arms the backstop never started"); |
| | | storage.execute(passing); // joins that connection and is through while the other one runs |
| | | |
| | | verify(con, never()).setNetworkTimeout(any(Executor.class), eq(0)); |
| | | mayFinish.countDown(); |
| | | outliving.joinOrFail(); |
| | | final InOrder inOrder = inOrder(con); |
| | | inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq((7 + JDBCStorage.BACKSTOP_MARGIN_SECONDS) * 1000)); |
| | | inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq(0)); |
| | | } |
| | | |
| | | /** |
| | | * With bounds of two classes in flight on one connection, the value armed is the loosest of |
| | | * them: a socket read timeout is shared by everything running on the connection, so tightening |
| | | * it to the bound of an entry read would cut the bulk statement beside it long before the bound |
| | | * that statement was actually given. |
| | | */ |
| | | @Test |
| | | public void testTheBackstopFollowsTheLoosestBoundInFlight() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "7"); |
| | | System.setProperty(StatementBound.BULK.property, "100"); |
| | | final Connection con = mock(Connection.class); |
| | | when(con.getNetworkTimeout()).thenReturn(0); |
| | | final CountDownLatch bulkRunning = new CountDownLatch(1); |
| | | final CountDownLatch bulkMayFinish = new CountDownLatch(1); |
| | | final PreparedStatement bulk = lingering(con, bulkRunning, bulkMayFinish); |
| | | final PreparedStatement operation = mock(PreparedStatement.class); |
| | | when(operation.getConnection()).thenReturn(con); |
| | | when(operation.executeUpdate()).thenReturn(1); |
| | | |
| | | final Background count = start("count", () -> storage.execute(bulk, StatementBound.BULK)); |
| | | awaitOrFail(bulkRunning, "the bulk statement never started"); |
| | | storage.execute(operation); // an entry read of another thread, with a tighter bound |
| | | bulkMayFinish.countDown(); |
| | | count.joinOrFail(); |
| | | |
| | | final InOrder inOrder = inOrder(con); |
| | | inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq((100 + JDBCStorage.BACKSTOP_MARGIN_SECONDS) * 1000)); |
| | | inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq(0)); |
| | | verify(con, never()).setNetworkTimeout(any(Executor.class), eq((7 + JDBCStorage.BACKSTOP_MARGIN_SECONDS) * 1000)); |
| | | |
| | | } |
| | | |
| | | /** |
| | | * The other order, which is the one that moves the backstop while a statement is running: a |
| | | * bound looser than what is armed re-arms the connection to its own value, and the tighter |
| | | * statement left behind gets its bound back the moment the looser one is through. |
| | | */ |
| | | @Test |
| | | public void testALooserBoundRearmsTheBackstopAndTheTighterOneGetsItBack() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "7"); |
| | | System.setProperty(StatementBound.BULK.property, "100"); |
| | | final Connection con = mock(Connection.class); |
| | | when(con.getNetworkTimeout()).thenReturn(0); |
| | | final CountDownLatch operationRunning = new CountDownLatch(1); |
| | | final CountDownLatch operationMayFinish = new CountDownLatch(1); |
| | | final CountDownLatch bulkRunning = new CountDownLatch(1); |
| | | final CountDownLatch bulkMayFinish = new CountDownLatch(1); |
| | | final PreparedStatement operation = lingering(con, operationRunning, operationMayFinish); |
| | | final PreparedStatement bulk = lingering(con, bulkRunning, bulkMayFinish); |
| | | |
| | | final Background entryRead = start("entry-read", () -> storage.execute(operation)); |
| | | awaitOrFail(operationRunning, "the entry read never started"); |
| | | final Background count = start("count", () -> storage.execute(bulk, StatementBound.BULK)); |
| | | awaitOrFail(bulkRunning, "the bulk statement never started"); |
| | | bulkMayFinish.countDown(); |
| | | count.joinOrFail(); |
| | | operationMayFinish.countDown(); |
| | | entryRead.joinOrFail(); |
| | | |
| | | final InOrder inOrder = inOrder(con); |
| | | inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq((7 + JDBCStorage.BACKSTOP_MARGIN_SECONDS) * 1000)); |
| | | inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq((100 + JDBCStorage.BACKSTOP_MARGIN_SECONDS) * 1000)); |
| | | inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq((7 + JDBCStorage.BACKSTOP_MARGIN_SECONDS) * 1000)); |
| | | inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq(0)); |
| | | } |
| | | |
| | | /** |
| | | * A failure that arrives before the bound is the caller's to classify and must reach it as it |
| | | * stands: a lock wait reported in class 40 is the conflict {@code JDBCStorage.write()} replays, |
| | | * and wrapping it would take it out of that class. |
| | | */ |
| | | @Test |
| | | public void testAFailureInsideTheBoundIsPassedThrough() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "60"); |
| | | final SQLException conflict = new SQLException("lock wait timeout exceeded", "40001", 1205); |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | when(statement.executeUpdate()).thenThrow(conflict); |
| | | |
| | | try { |
| | | storage.execute(statement); |
| | | fail("the failure of the statement must reach the caller"); |
| | | } catch (SQLException e) { |
| | | assertSame(e, conflict); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * A failure that arrives at the bound names the property that produced it - every driver |
| | | * reports a cancelled statement differently, and none of them knows why it was cancelled - and |
| | | * still carries the SQL state and the error number of the failure it replaces. |
| | | */ |
| | | @Test |
| | | public void testAFailureAtTheBoundNamesTheProperty() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "1"); |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | when(statement.executeUpdate()).thenAnswer(new Answer<Integer>() { |
| | | @Override |
| | | public Integer answer(InvocationOnMock invocation) throws Throwable { |
| | | Thread.sleep(1100); // the driver cancelled it at the bound this test set |
| | | throw new SQLException("canceling statement due to user request", "57014", 0); |
| | | } |
| | | }); |
| | | |
| | | try { |
| | | storage.execute(statement); |
| | | fail("the failure of the statement must reach the caller"); |
| | | } catch (SQLTimeoutException e) { |
| | | assertEquals(e.getSQLState(), "57014"); |
| | | assertTrue(e.getMessage().contains(StatementBound.OPERATION.property), e.getMessage()); |
| | | assertEquals(((SQLException) e.getCause()).getSQLState(), "57014"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * A driver reporting the cancel a few milliseconds before the bound is arithmetically due - |
| | | * its timer is kept in whole seconds, while this classification is measured to the millisecond |
| | | * - is still the bound arriving, and the failure has to name the property all the same. |
| | | * Without the slack under it, the one failure this classification exists to name reached the |
| | | * caller as a bare 57014 or ORA-01013, which no caller can tell from a cancel of an operator. |
| | | */ |
| | | @Test |
| | | public void testAFailureJustUnderTheBoundStillNamesTheProperty() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "1"); |
| | | final SteppedClockStorage clocked = new SteppedClockStorage(); |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | when(statement.executeUpdate()).thenAnswer(new Answer<Integer>() { |
| | | @Override |
| | | public Integer answer(InvocationOnMock invocation) throws Throwable { |
| | | // a literal, deliberately not derived from CLOCK_SLACK_MILLIS: a test computing its |
| | | // own input from the constant it pins follows that constant to zero and pins nothing |
| | | clocked.millis = 875; // 125 ms under the bound, inside a slack of 250 |
| | | throw new SQLException("canceling statement due to user request", "57014", 0); |
| | | } |
| | | }); |
| | | |
| | | try { |
| | | clocked.execute(statement); |
| | | fail("the failure of the statement must reach the caller"); |
| | | } catch (SQLTimeoutException e) { |
| | | assertTrue(e.getMessage().contains(StatementBound.OPERATION.property), e.getMessage()); |
| | | // and the time it names is the one measured, so that this cannot pass on a clock that |
| | | // simply ran past the bound while the assertion above looked only at the message |
| | | assertTrue(e.getMessage().contains("875 ms"), e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * The other side of that slack, which is what keeps it a slack rather than a second bound: a |
| | | * failure further under the bound than a driver's whole-second timer could account for is a |
| | | * failure of its own, and the caller has to see it as one. |
| | | */ |
| | | @Test |
| | | public void testAFailureFurtherUnderTheBoundIsStillPassedThrough() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "1"); |
| | | final SteppedClockStorage clocked = new SteppedClockStorage(); |
| | | final SQLException conflict = new SQLException("lock wait timeout exceeded", "40001", 1205); |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | when(statement.executeUpdate()).thenAnswer(new Answer<Integer>() { |
| | | @Override |
| | | public Integer answer(InvocationOnMock invocation) throws Throwable { |
| | | clocked.millis = 749; // a millisecond further out than the slack of 250 reaches |
| | | throw conflict; |
| | | } |
| | | }); |
| | | |
| | | try { |
| | | clocked.execute(statement); |
| | | fail("the failure of the statement must reach the caller"); |
| | | } catch (SQLException e) { |
| | | assertSame(e, conflict); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * And the edge of that slack belongs to the bound. The two cases above sit either side of it, |
| | | * so both of them pass whether the comparison is {@code <} or {@code <=} - the point where the |
| | | * two differ is exactly a slack under the bound, and that is what this pins. |
| | | */ |
| | | @Test |
| | | public void testAFailureExactlyASlackUnderTheBoundIsStillTheBound() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "1"); |
| | | final SteppedClockStorage clocked = new SteppedClockStorage(); |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | when(statement.executeUpdate()).thenAnswer(new Answer<Integer>() { |
| | | @Override |
| | | public Integer answer(InvocationOnMock invocation) throws Throwable { |
| | | // a literal for the reason the case above gives: derived from CLOCK_SLACK_MILLIS it |
| | | // would follow that constant wherever it went and pin nothing |
| | | clocked.millis = 750; // the bound of a second, less a slack of 250 |
| | | throw new SQLException("canceling statement due to user request", "57014", 0); |
| | | } |
| | | }); |
| | | |
| | | try { |
| | | clocked.execute(statement); |
| | | fail("the failure of the statement must reach the caller"); |
| | | } catch (SQLTimeoutException e) { |
| | | assertTrue(e.getMessage().contains(StatementBound.OPERATION.property), e.getMessage()); |
| | | assertTrue(e.getMessage().contains("750 ms"), e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * A statement that neither layer bounded reaches its caller exactly as it is. Asking for the |
| | | * second layer is not having it: a driver may have no network timeout at all, a connection may |
| | | * refuse the call, one may already carry a timeout of a deployment's own, and a statement of an |
| | | * unbounded class running beside this one takes the backstop off outright. A catalog lookup |
| | | * takes no query timeout by construction, so with the backstop unarmed nothing ends its wait - |
| | | * and the failure that finally does is the driver's own. Reported as the bound, it sent an |
| | | * operator to raise a property that had bounded nothing about the wait they had just watched. |
| | | */ |
| | | @Test |
| | | public void testAFailureOfAStatementNeitherLayerBoundedIsPassedThrough() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "1"); |
| | | final SteppedClockStorage clocked = new SteppedClockStorage(); // its own, for its own latch |
| | | final Connection con = mock(Connection.class); |
| | | when(con.getNetworkTimeout()).thenReturn(0); |
| | | doThrow(new SQLFeatureNotSupportedException("no network timeout")) |
| | | .when(con).setNetworkTimeout(any(Executor.class), anyInt()); |
| | | final SQLException reset = new SQLException("connection reset by peer", "08006", 0); |
| | | |
| | | try { |
| | | clocked.bounded(con, StatementBound.OPERATION, () -> { |
| | | clocked.millis = 600000; // ten minutes on a metadata lock, with nothing to end it |
| | | throw reset; |
| | | }); |
| | | fail("the failure of the lookup must reach the caller"); |
| | | } catch (SQLException e) { |
| | | assertSame(e, reset, "a statement neither layer bounded was reported as having reached a bound"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * The failure reports the time the statement really took rather than the bound it reached. |
| | | * A cancel that is armed is not a cancel that is acted upon - a session blocked in a row-lock |
| | | * enqueue on oracle does not process the break its driver sends - and the wait then ends at |
| | | * the socket read timeout behind it, a margin later than the property that armed it. Reported |
| | | * as the property alone, the message put a time next to a clock that disagreed with it. |
| | | */ |
| | | @Test |
| | | public void testAFailureAtTheBoundReportsTheTimeItReallyTook() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "1"); |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | when(statement.executeUpdate()).thenAnswer(new Answer<Integer>() { |
| | | @Override |
| | | public Integer answer(InvocationOnMock invocation) throws Throwable { |
| | | Thread.sleep(1500); // the cancel was armed at 1 s and the database did not act on it |
| | | throw new SQLException("connection reset by peer", "08006", 0); |
| | | } |
| | | }); |
| | | |
| | | try { |
| | | storage.execute(statement); |
| | | fail("the failure of the statement must reach the caller"); |
| | | } catch (SQLTimeoutException e) { |
| | | final Matcher took = Pattern.compile("took (\\d+) ms").matcher(e.getMessage()); |
| | | assertTrue(took.find(), e.getMessage()); |
| | | assertTrue(Long.parseLong(took.group(1)) >= 1500, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * The rows are read while the bound is still armed. A driver hands them over as they are asked |
| | | * for - oracle prefetches ten at a time, mssql buffers adaptively - so a drain that happened |
| | | * after the bound was released would be a wait with nothing bounding it, which is the hang |
| | | * #877 is about rather than a detail of where the call sits. |
| | | */ |
| | | @Test |
| | | public void testTheRowsAreReadWhileTheBoundIsStillArmed() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "7"); |
| | | final Connection con = mock(Connection.class); |
| | | when(con.getNetworkTimeout()).thenReturn(0); |
| | | final ResultSet rows = mock(ResultSet.class); |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | when(statement.getConnection()).thenReturn(con); |
| | | when(statement.executeQuery()).thenReturn(rows); |
| | | |
| | | assertEquals(storage.executeResultSet(statement, ResultSet::next), Boolean.FALSE); |
| | | |
| | | final InOrder inOrder = inOrder(con, rows); |
| | | inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq((7 + JDBCStorage.BACKSTOP_MARGIN_SECONDS) * 1000)); |
| | | inOrder.verify(rows).next(); |
| | | inOrder.verify(rows).close(); // the rows are done with before the backstop goes back |
| | | inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq(0)); |
| | | } |
| | | |
| | | /** A transfer of rows cut at the bound names the property that cut it, as an execution does. */ |
| | | @Test |
| | | public void testAFailureWhileTheRowsAreReadIsMeasuredAgainstTheBound() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "1"); |
| | | final ResultSet rows = mock(ResultSet.class); |
| | | when(rows.next()).thenAnswer(new Answer<Boolean>() { |
| | | @Override |
| | | public Boolean answer(InvocationOnMock invocation) throws Throwable { |
| | | Thread.sleep(1100); // the driver cancelled the transfer at the bound this test set |
| | | throw new SQLException("canceling statement due to user request", "57014", 0); |
| | | } |
| | | }); |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | when(statement.executeQuery()).thenReturn(rows); |
| | | |
| | | try { |
| | | storage.executeResultSet(statement, ResultSet::next); |
| | | fail("the failure of the transfer must reach the caller"); |
| | | } catch (SQLTimeoutException e) { |
| | | assertTrue(e.getMessage().contains(StatementBound.OPERATION.property), e.getMessage()); |
| | | assertEquals(e.getSQLState(), "57014"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * A driver is allowed to have no query timeout at all, and this backend takes whatever URL a |
| | | * deployment configures. Such a driver has to keep working, with the socket read timeout as |
| | | * its whole bound, rather than fail every statement it is given. |
| | | */ |
| | | @Test |
| | | public void testADriverWithoutAQueryTimeoutKeepsWorkingUnderTheBackstop() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "7"); |
| | | final Connection con = mock(Connection.class); |
| | | when(con.getNetworkTimeout()).thenReturn(0); |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | when(statement.getConnection()).thenReturn(con); |
| | | doThrow(new SQLFeatureNotSupportedException("no query timeout")).when(statement).setQueryTimeout(anyInt()); |
| | | when(statement.executeUpdate()).thenReturn(1); |
| | | |
| | | assertEquals(storage.execute(statement), 1); |
| | | |
| | | verify(con).setNetworkTimeout(any(Executor.class), eq((7 + JDBCStorage.BACKSTOP_MARGIN_SECONDS) * 1000)); |
| | | } |
| | | |
| | | /** |
| | | * The catalog lookups of {@code openTree()} are bounded too. {@code DatabaseMetaData} takes no |
| | | * query timeout, so the socket read timeout behind the cancel is the only layer they can be |
| | | * given - and they run once per tree on every open of a backend, behind the same locks as the |
| | | * {@code create table} they guard. |
| | | */ |
| | | @Test |
| | | public void testACatalogLookupIsBoundedByTheBackstopAlone() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "7"); |
| | | final Connection con = mock(Connection.class); |
| | | when(con.getNetworkTimeout()).thenReturn(0); |
| | | |
| | | assertEquals(storage.bounded(con, StatementBound.OPERATION, () -> "asked the catalog"), "asked the catalog"); |
| | | |
| | | final InOrder inOrder = inOrder(con); |
| | | inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq((7 + JDBCStorage.BACKSTOP_MARGIN_SECONDS) * 1000)); |
| | | inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq(0)); |
| | | } |
| | | |
| | | /** |
| | | * Which class a batch of a cursor belongs to follows what the database has to do to answer it. |
| | | * {@code positionToLastKey()} has no key to seek on, so it is an {@code order by k desc} over |
| | | * the whole table - a scan and a sort of it on mssql, where {@code k} cannot be an index key - |
| | | * and every open of a backend runs it once per base DN through |
| | | * {@code EntryContainer.getHighestEntryID()}, outside the try/catch of |
| | | * {@code BackendImpl.openBackend()}. Bounding that as an entry read would turn a large backend |
| | | * that opens slowly into one that does not open at all, while the batches the cursor walks |
| | | * along its index stay operations and keep the bound of one. |
| | | */ |
| | | @Test |
| | | public void testTheScanBehindTheHighestEntryIdIsBulkAndTheBatchesOfACursorAreNot() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "7"); |
| | | System.setProperty(StatementBound.BULK.property, "0"); |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | when(statement.executeQuery()).thenReturn(mock(ResultSet.class)); |
| | | final Connection parent = mock(Connection.class); |
| | | when(parent.prepareStatement(anyString())).thenReturn(statement); |
| | | final JDBCStorage.CursorImpl cursor = storage.new CursorImpl(true, new CachedConnection("jdbc:mock", parent), |
| | | new TreeName("dc=example,dc=com", "id2entry"), StatementBound.OPERATION); |
| | | |
| | | cursor.positionToLastKey(); |
| | | verify(statement, never()).setQueryTimeout(anyInt()); |
| | | |
| | | cursor.next(); |
| | | verify(statement).setQueryTimeout(7); |
| | | } |
| | | |
| | | /** |
| | | * A cursor opened for a walk of a whole tree takes bulk batches, however ordinary the statement |
| | | * looks: nobody is waiting on that walk, and on mssql it is not even a walk along an index - |
| | | * {@code k} is a {@code varbinary(max)} there, which cannot be an index key, so every batch is |
| | | * a scan and a sort of the whole table. This is the class an export, a verify, a rebuild and |
| | | * the load of a tree at open ask for through {@code ReadableTransaction.openBulkCursor()}, |
| | | * while the cursor of a search keeps the bound of an operation. |
| | | */ |
| | | @Test |
| | | public void testTheBatchesOfABulkCursorAreBulkAndThoseOfASearchAreNot() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "7"); |
| | | System.setProperty(StatementBound.BULK.property, "0"); |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | when(statement.executeQuery()).thenReturn(mock(ResultSet.class)); |
| | | final Connection parent = mock(Connection.class); |
| | | when(parent.prepareStatement(anyString())).thenReturn(statement); |
| | | final JDBCStorage.ReadableTransactionImpl txn = |
| | | storage.new ReadableTransactionImpl(new CachedConnection("jdbc:mock", parent)); |
| | | final TreeName tree = new TreeName("dc=example,dc=com", "id2entry"); |
| | | |
| | | txn.openBulkCursor(tree).next(); |
| | | verify(statement, never()).setQueryTimeout(anyInt()); |
| | | |
| | | txn.openCursor(tree).next(); |
| | | verify(statement).setQueryTimeout(7); |
| | | } |
| | | |
| | | /** |
| | | * Every statement an import issues is bulk, by the class of the transactions it works through: |
| | | * phase one writes the trees through {@code put()}, phase two reads them back through |
| | | * {@code read()} and walks them through {@code openCursor()}, and no client is waiting on any |
| | | * of it. An upsert of an online import blocked by an LDAP write on the same table would |
| | | * otherwise sit until the bound of an entry read and then fail the whole import - {@code h} is |
| | | * the primary key on every dialect, and the default lock wait is forever on three of the four. |
| | | */ |
| | | @Test |
| | | public void testEveryStatementOfAnImportIsBulk() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "7"); |
| | | System.setProperty(StatementBound.BULK.property, "0"); |
| | | final Connection parent = mock(Connection.class); |
| | | // a read timeout of a deployment's own, and deliberately not zero: restoring it and taking |
| | | // the backstop off are one and the same call when what came before was zero, and the |
| | | // assertions below would then hold whichever of the two the code did |
| | | when(parent.getNetworkTimeout()).thenReturn(90000); |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | // the statement reports the connection it runs on, as CachedConnection.prepareStatement() |
| | | // has it: without this the import would be measured against the first layer alone, and the |
| | | // second one - the layer that decides whether an import can hang on a peer that stopped |
| | | // answering - would never be entered at all |
| | | when(statement.getConnection()).thenReturn(parent); |
| | | when(statement.executeQuery()).thenReturn(mock(ResultSet.class)); |
| | | when(parent.prepareStatement(anyString())).thenReturn(statement); |
| | | storage.accessMode = AccessMode.READ_WRITE; // an import has the storage open for writing |
| | | final JDBCStorage.ImporterImpl importer = |
| | | storage.new ImporterImpl(new CachedConnection("jdbc:mock", parent), true); |
| | | final TreeName tree = new TreeName("dc=example,dc=com", "id2entry"); |
| | | |
| | | // an entry read of a client arms the backstop on the very connection the import writes to, |
| | | // which is the shape of an online import: one connection, statements of both classes on it |
| | | final CountDownLatch running = new CountDownLatch(1); |
| | | final CountDownLatch mayFinish = new CountDownLatch(1); |
| | | final PreparedStatement operation = lingering(parent, running, mayFinish); |
| | | final Background entryRead = start("entry-read", () -> storage.execute(operation)); |
| | | awaitOrFail(running, "the entry read never started"); |
| | | // atLeastOnce, not the implicit times(1) of a bare verify: every statement of the import |
| | | // below takes the backstop off and its release arms it again while the entry read is still |
| | | // in flight, so this holds by where it stands in the method rather than by what it pins |
| | | verify(parent, atLeastOnce()) |
| | | .setNetworkTimeout(any(Executor.class), eq((7 + JDBCStorage.BACKSTOP_MARGIN_SECONDS) * 1000)); |
| | | |
| | | importer.openCursor(tree).next(); |
| | | importer.read(tree, ByteString.valueOfUtf8("key")); |
| | | importer.put(tree, ByteString.valueOfUtf8("key"), ByteString.valueOfUtf8("value")); |
| | | |
| | | verify(statement, never()).setQueryTimeout(anyInt()); |
| | | // and none of them runs under the socket read timeout of the entry read beside it either: |
| | | // while that read is still in flight, the import takes the backstop off the connection, |
| | | // which is what putting the connection's own value back looks like |
| | | verify(parent, atLeastOnce()).setNetworkTimeout(any(Executor.class), eq(90000)); |
| | | // and never a zero: nothing here has a zero to put back, so a run that reached this state |
| | | // by restoring one would be a run that read the previous value of another connection |
| | | verify(parent, never()).setNetworkTimeout(any(Executor.class), eq(0)); |
| | | |
| | | mayFinish.countDown(); |
| | | entryRead.joinOrFail(); |
| | | } |
| | | |
| | | /** |
| | | * A statement bounded by the socket read timeout alone is measured against what that layer |
| | | * really allows it - its bound plus the margin the layer carries - rather than against the |
| | | * property: nothing cuts a catalog lookup at the bound itself, so a connection reset arriving |
| | | * just after it is the caller's failure to see, not a query timeout that never happened. |
| | | */ |
| | | @Test |
| | | public void testAFailureBeforeTheBackstopOfACatalogLookupIsPassedThrough() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "1"); |
| | | final Connection con = mock(Connection.class); |
| | | when(con.getNetworkTimeout()).thenReturn(0); |
| | | final SQLException reset = new SQLException("connection reset by peer", "08006", 0); |
| | | |
| | | try { |
| | | storage.bounded(con, StatementBound.OPERATION, () -> { |
| | | sleep(1100); // past the property, well inside the margin of the layer behind it |
| | | throw reset; |
| | | }); |
| | | fail("the failure of the lookup must reach the caller"); |
| | | } catch (SQLException e) { |
| | | assertSame(e, reset); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * A driver with no network timeout at all is asked once and then left alone: it says so with |
| | | * {@code SQLFeatureNotSupportedException}, and asking it again costs a throw on every statement |
| | | * for the life of the storage. Its own storage here, since that is the scope of the latch. |
| | | */ |
| | | @Test |
| | | public void testADriverWithoutANetworkTimeoutIsNotAskedAgain() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "7"); |
| | | final JDBCStorage isolated = new JDBCStorage(mockCfg(JDBCBackendCfg.class), null); |
| | | final Connection con = mock(Connection.class); |
| | | when(con.getNetworkTimeout()).thenReturn(0); |
| | | doThrow(new SQLFeatureNotSupportedException("no network timeout")) |
| | | .when(con).setNetworkTimeout(any(Executor.class), anyInt()); |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | when(statement.getConnection()).thenReturn(con); |
| | | when(statement.executeUpdate()).thenReturn(1); |
| | | |
| | | assertEquals(isolated.execute(statement), 1); |
| | | assertEquals(isolated.execute(statement), 1); |
| | | |
| | | verify(con, times(1)).setNetworkTimeout(any(Executor.class), anyInt()); |
| | | } |
| | | |
| | | /** |
| | | * A connection that failed the call says nothing about the driver - it may be the very one |
| | | * that reached this timeout - so the next statement is armed as usual. The two causes share a |
| | | * catch and must not share a verdict: taking one for the other silences the backstop of a |
| | | * whole storage on a single dying connection. |
| | | */ |
| | | @Test |
| | | public void testAConnectionThatFailedTheBackstopDoesNotSpeakForTheDriver() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "7"); |
| | | final Connection con = mock(Connection.class); |
| | | when(con.getNetworkTimeout()).thenReturn(0); |
| | | doThrow(new SQLException("the connection is closed", "08003", 0)) |
| | | .when(con).setNetworkTimeout(any(Executor.class), anyInt()); |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | when(statement.getConnection()).thenReturn(con); |
| | | when(statement.executeUpdate()).thenReturn(1); |
| | | |
| | | assertEquals(storage.execute(statement), 1); |
| | | assertEquals(storage.execute(statement), 1); |
| | | |
| | | verify(con, times(2)).setNetworkTimeout(any(Executor.class), |
| | | eq((7 + JDBCStorage.BACKSTOP_MARGIN_SECONDS) * 1000)); |
| | | } |
| | | |
| | | /** |
| | | * The statistics refresh after an import runs under a bound of its own - it takes as long as a |
| | | * scan of the table it describes, which no class of {@link StatementBound} can be asked to |
| | | * allow - and under both layers of it. The second one is the reason: on oracle this statement |
| | | * is {@code dbms_stats.gather_table_stats}, the engine whose session does not act on the break |
| | | * its driver sends, and it runs at the very end of a successful import, where a cancel that |
| | | * never arrives would park the import with its data already committed. |
| | | */ |
| | | @Test |
| | | public void testTheStatisticsRefreshRunsUnderItsOwnBoundAndTheBackstop() throws Exception { |
| | | System.setProperty(JDBCStorage.STATISTICS_TIMEOUT_PROPERTY, "60"); |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | final Connection con = mock(oracleConnection.class); // the dialect is read off the connection |
| | | when(con.getNetworkTimeout()).thenReturn(0); |
| | | when(con.prepareStatement(anyString())).thenReturn(statement); |
| | | |
| | | assertTrue(storage.updateTableStatistics(con, singletonList(new TreeName("dc=example,dc=com", "id2entry")))); |
| | | |
| | | verify(statement).setQueryTimeout(60); |
| | | final InOrder inOrder = inOrder(con, statement); |
| | | inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq((60 + JDBCStorage.BACKSTOP_MARGIN_SECONDS) * 1000)); |
| | | inOrder.verify(statement).execute(); |
| | | inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq(0)); |
| | | } |
| | | |
| | | /** |
| | | * A connection whose driver refuses the call mid-flight is given back what it carried before. |
| | | * The entry holding that value is dropped as soon as the last statement on the connection is |
| | | * through, so a backstop left armed goes back to the pool as the connection's own read timeout |
| | | * - and the next borrower, which only ever tightens, reads it as the value of a deployment and |
| | | * keeps it from then on, cutting a statement of an unbounded class at a bound it never had. |
| | | */ |
| | | @Test |
| | | public void testAConnectionThatFailedTheBackstopIsGivenBackWhatItCarried() throws Exception { |
| | | System.setProperty(StatementBound.OPERATION.property, "7"); |
| | | System.setProperty(StatementBound.BULK.property, "100"); |
| | | final Connection con = mock(Connection.class); |
| | | // a read timeout of a deployment's own, and looser than either bound in flight below: a |
| | | // tighter one is what the backstop declines to loosen, and it would put that value back |
| | | // instead of ever reaching the call that fails here |
| | | when(con.getNetworkTimeout()).thenReturn(200000); |
| | | // the arming of the tighter bound goes through, and the re-arm of the looser one does not |
| | | doThrow(new SQLException("the connection is closed", "08003", 0)).when(con) |
| | | .setNetworkTimeout(any(Executor.class), eq((100 + JDBCStorage.BACKSTOP_MARGIN_SECONDS) * 1000)); |
| | | final CountDownLatch running = new CountDownLatch(1); |
| | | final CountDownLatch mayFinish = new CountDownLatch(1); |
| | | final PreparedStatement operation = lingering(con, running, mayFinish); |
| | | final PreparedStatement bulk = mock(PreparedStatement.class); |
| | | when(bulk.getConnection()).thenReturn(con); |
| | | when(bulk.executeUpdate()).thenReturn(1); |
| | | |
| | | final Background entryRead = start("entry-read", () -> storage.execute(operation)); |
| | | awaitOrFail(running, "the entry read never started"); |
| | | assertEquals(storage.execute(bulk, StatementBound.BULK), 1); // its re-arm is what fails |
| | | mayFinish.countDown(); |
| | | entryRead.joinOrFail(); |
| | | |
| | | final InOrder inOrder = inOrder(con); |
| | | inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq((7 + JDBCStorage.BACKSTOP_MARGIN_SECONDS) * 1000)); |
| | | inOrder.verify(con).setNetworkTimeout(any(Executor.class), eq(200000)); |
| | | } |
| | | |
| | | /** |
| | | * The connection an import would have held goes back to the pool when the importer cannot be |
| | | * built on it. That is a designed path rather than an accident: an import of a read-only storage |
| | | * throws {@code ReadOnlyStorageException} where the importer is built, and the connection |
| | | * borrowed for the import - the one it keeps for its whole duration - was leaving the pool for |
| | | * good there, with the transaction it had already begun. |
| | | */ |
| | | @Test |
| | | public void testStartImportGivesTheConnectionBackWhenTheImporterCannotBeBuilt() throws Exception { |
| | | final Connection con = mock(Connection.class); |
| | | final JDBCStorage readOnly = new JDBCStorage(mockCfg(JDBCBackendCfg.class), null) { |
| | | @Override |
| | | Connection getConnection(boolean trusted) { |
| | | return con; |
| | | } |
| | | |
| | | @Override |
| | | public StorageStatus getStorageStatus() { |
| | | return StorageStatus.working(); // open already, so that startImport() borrows and no more |
| | | } |
| | | }; |
| | | readOnly.accessMode = AccessMode.READ_ONLY; |
| | | |
| | | try { |
| | | readOnly.startImport(); |
| | | fail("an import of a read-only storage must not be handed an importer"); |
| | | } catch (ReadOnlyStorageException expected) { |
| | | // the designed path this test is about |
| | | } |
| | | |
| | | verify(con).close(); |
| | | } |
| | | |
| | | /** |
| | | * And the storage goes back with the connection where this method is what opened it: |
| | | * {@code ImporterImpl.close()} is the only thing that closes a storage an import opened, so a |
| | | * failure between the open and the importer that would have held it leaves it open for good. |
| | | * The borrow of the connection was already covered that way; the build of the importer was not. |
| | | * <p> |
| | | * What fails the build here is a storage that is not writeable - the one failure of the |
| | | * importer's constructor a test can produce from outside it, and it takes an open that leaves |
| | | * the storage read-only to get there. What it stands for is any {@code Error} out of that |
| | | * constructor, which is what the {@code finally} is for. |
| | | */ |
| | | @Test |
| | | public void testStartImportClosesTheStorageItOpenedWhenTheImporterCannotBeBuilt() throws Exception { |
| | | final Connection con = mock(Connection.class); |
| | | final AtomicInteger opens = new AtomicInteger(); |
| | | final AtomicInteger closes = new AtomicInteger(); |
| | | final JDBCStorage notOpen = new JDBCStorage(mockCfg(JDBCBackendCfg.class), null) { |
| | | @Override |
| | | Connection getConnection(boolean trusted) { |
| | | return con; |
| | | } |
| | | |
| | | @Override |
| | | public StorageStatus getStorageStatus() { |
| | | return StorageStatus.lockedDown(LocalizableMessage.raw("closed")); // so startImport() opens it |
| | | } |
| | | |
| | | @Override |
| | | public void open(AccessMode accessMode) { |
| | | opens.incrementAndGet(); // and leaves this storage read-only, so that the build below fails |
| | | } |
| | | |
| | | @Override |
| | | public void close() { |
| | | closes.incrementAndGet(); |
| | | } |
| | | }; |
| | | |
| | | try { |
| | | notOpen.startImport(); |
| | | fail("an import that cannot be given an importer must not report one"); |
| | | } catch (ReadOnlyStorageException expected) { |
| | | // the build failing after this method opened the storage, which is the path under test |
| | | } |
| | | |
| | | assertEquals(opens.get(), 1, "the storage was not opened by startImport(), so nothing was owed back"); |
| | | verify(con).close(); |
| | | assertEquals(closes.get(), 1, "the storage this method opened was left open"); |
| | | } |
| | | |
| | | /** |
| | | * A row whose {@code v} is null is a row that exists, and reading it has to fail rather than |
| | | * report the key as absent - which is what {@code read()} of the same row does. Reading the |
| | | * rows inside the bound had turned the value into a raw {@code byte[]} on the way out of the |
| | | * handler, and null then stood in for both. |
| | | */ |
| | | @Test |
| | | public void testARowWithoutAValueFailsRatherThanReportingTheKeyAsAbsent() throws Exception { |
| | | final TreeName treeName = new TreeName("dc=example,dc=com", "id2entry"); |
| | | final ResultSet rows = mock(ResultSet.class); |
| | | when(rows.next()).thenReturn(true); |
| | | when(rows.getBytes("v")).thenReturn(null); // the schema allows it, however this backend writes |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | when(statement.executeQuery()).thenReturn(rows); |
| | | final Connection parent = mock(Connection.class); |
| | | when(parent.prepareStatement(anyString())).thenReturn(statement); |
| | | final JDBCStorage.CursorImpl cursor = storage.new CursorImpl(true, new CachedConnection("jdbc:mock", parent), |
| | | treeName, StatementBound.OPERATION); |
| | | |
| | | try { |
| | | cursor.positionToKey(ByteString.valueOfUtf8("key")); |
| | | fail("a row whose value is null must not be read as a key that is not there"); |
| | | } catch (StorageRuntimeException expected) { |
| | | // the failure the production path names, not whatever null happens to reach first: a bare |
| | | // NullPointerException out of ByteString.wrap is satisfied by any unrelated one later |
| | | // introduced into positionToKey, and it says nothing about which table holds the row |
| | | assertTrue(expected.getMessage().contains("no value"), expected.getMessage()); |
| | | assertTrue(expected.getMessage().contains(storage.getTableName(treeName)), expected.getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * And so does a batch of a cursor, which is the third reader of a value and the one that would |
| | | * fail furthest from the row: a batch is buffered whole and unwrapped a row at a time |
| | | * afterwards, so left unchecked it fails from {@code advanceFromBuffer()} - outside the bound |
| | | * and outside the {@code catch} of the batch that read it. |
| | | */ |
| | | @Test |
| | | public void testABatchWithARowWithoutAValueFailsTheSameWay() throws Exception { |
| | | final TreeName treeName = new TreeName("dc=example,dc=com", "id2entry"); |
| | | final ResultSet rows = mock(ResultSet.class); |
| | | when(rows.next()).thenReturn(true, false); |
| | | when(rows.getBytes(1)).thenReturn(ByteString.valueOfUtf8("key").toByteArray()); |
| | | when(rows.getBytes(2)).thenReturn(null); |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | when(statement.executeQuery()).thenReturn(rows); |
| | | final Connection parent = mock(Connection.class); |
| | | when(parent.prepareStatement(anyString())).thenReturn(statement); |
| | | final JDBCStorage.CursorImpl cursor = storage.new CursorImpl(true, new CachedConnection("jdbc:mock", parent), |
| | | treeName, StatementBound.OPERATION); |
| | | |
| | | try { |
| | | cursor.next(); |
| | | fail("a batch holding a row whose value is null must not hand that row out"); |
| | | } catch (StorageRuntimeException expected) { |
| | | assertTrue(expected.getMessage().contains("no value"), expected.getMessage()); |
| | | assertTrue(expected.getMessage().contains(storage.getTableName(treeName)), expected.getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * And a read of the same row fails the same way, which is the whole of what {@code null} means |
| | | * here: the two answer with it for one reason only, and a row that exists is not that reason. |
| | | */ |
| | | @Test |
| | | public void testAReadOfARowWithoutAValueFailsTheSameWay() throws Exception { |
| | | final TreeName treeName = new TreeName("dc=example,dc=com", "id2entry"); |
| | | final ResultSet rows = mock(ResultSet.class); |
| | | when(rows.next()).thenReturn(true); |
| | | when(rows.getBytes("v")).thenReturn(null); |
| | | final PreparedStatement statement = mock(PreparedStatement.class); |
| | | when(statement.executeQuery()).thenReturn(rows); |
| | | final Connection parent = mock(Connection.class); |
| | | when(parent.prepareStatement(anyString())).thenReturn(statement); |
| | | |
| | | try { |
| | | storage.new ReadableTransactionImpl(new CachedConnection("jdbc:mock", parent)) |
| | | .read(treeName, ByteString.valueOfUtf8("key")); |
| | | fail("a row whose value is null must not be read as a key that is not there"); |
| | | } catch (StorageRuntimeException expected) { |
| | | assertTrue(expected.getMessage().contains("no value"), expected.getMessage()); |
| | | } |
| | | } |
| | | |
| | | // JDBCStorage.dialectOf() reads the engine off the class name of the connection, so a mock of |
| | | // this interface is an oracle connection as far as the storage is concerned - which is the |
| | | // whole reason for the lower case name here. |
| | | private interface oracleConnection extends Connection {} |
| | | } |
| | |
| | | |
| | | import static org.forgerock.opendj.config.ConfigurationMock.mockCfg; |
| | | import static org.mockito.Mockito.when; |
| | | import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString; |
| | | import static org.testng.Assert.assertEquals; |
| | | import static org.testng.Assert.assertFalse; |
| | | import static org.testng.Assert.assertNotEquals; |
| | |
| | | } |
| | | |
| | | /** |
| | | * A statement of this backend has to end even when another session holds what it needs: a row |
| | | * locked by a transaction that never commits used to park the worker thread that issued the |
| | | * write for good, with nothing in the log to say so (#877). |
| | | */ |
| | | @Test(timeOut = 600000) |
| | | public void testWriteBlockedByAnotherSessionGivesUpAtItsBound() throws Exception { |
| | | assertBoundedWhileRowsAreLocked("testStatementBound", JDBCStorage.StatementBound.OPERATION, |
| | | new BlockedOperation() { |
| | | @Override |
| | | public void run(JDBCStorage storage, TreeName tree) throws Exception { |
| | | storage.write(new WriteOperation() { |
| | | @Override |
| | | public void run(WriteableTransaction txn) throws Exception { |
| | | txn.put(tree, key(1), value(2)); |
| | | } |
| | | }); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * The bulk class keeps a bound of its own: a count or the delete that empties a tree before an |
| | | * import legitimately takes minutes, so it must not be cut at the bound of an entry read - and |
| | | * must still be able to give up (#877). |
| | | */ |
| | | @Test(timeOut = 600000) |
| | | public void testBulkStatementGivesUpAtItsOwnBound() throws Exception { |
| | | assertBoundedWhileRowsAreLocked("testBulkBound", JDBCStorage.StatementBound.BULK, |
| | | new BlockedOperation() { |
| | | @Override |
| | | public void run(JDBCStorage storage, TreeName tree) throws Exception { |
| | | // the importer is where "delete from <table>" - the bulk class - is reachable: |
| | | // AbstractTwoPhaseImportStrategy clears every tree before an import writes to it |
| | | try (final Importer importer = storage.startImport()) { |
| | | importer.clearTree(tree); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private interface BlockedOperation { |
| | | void run(JDBCStorage storage, TreeName tree) throws Exception; |
| | | } |
| | | |
| | | /** |
| | | * Whether the failure the operation gave up with is the one its bound produced: the message of |
| | | * a statement classified as having reached its bound names the property that bounded it, and it |
| | | * arrives wrapped in whatever the storage throws to its caller. |
| | | */ |
| | | private static boolean namesTheBound(Throwable failure, JDBCStorage.StatementBound bound) { |
| | | return namedInTheChain(failure, bound.property); |
| | | } |
| | | |
| | | /** |
| | | * Whether the statement ran under the socket read timeout alone, which is what |
| | | * {@code timedOut()} says of one whose driver would not take the cancel. That degradation is by |
| | | * design - {@code JDBCStorage.setQueryTimeout()} warns once and carries on - and it is |
| | | * therefore silent: with a ceiling wide enough for the second layer, a run with the first one |
| | | * gone entirely ends at the backstop and passes as the bound doing its work. |
| | | */ |
| | | private static boolean ranUnderTheBackstopAlone(Throwable failure) { |
| | | return namedInTheChain(failure, JDBCStorage.BACKSTOP_ALONE); |
| | | } |
| | | |
| | | /** Cause hops walked below, as {@code JDBCStorage} bounds its own classifier: a guard against a cycle. */ |
| | | private static final int MAX_CAUSE_HOPS = 16; |
| | | |
| | | private static boolean namedInTheChain(Throwable failure, String text) { |
| | | // bounded by hops rather than by t != t.getCause(), which only catches a cause that is its |
| | | // own: a wrapper re-attaching an exception it has already wrapped makes a cycle of two, and |
| | | // walking that one spins until the harness times the whole suite out |
| | | Throwable t = failure; |
| | | for (int hops = 0; t != null && hops < MAX_CAUSE_HOPS; t = t.getCause(), hops++) { |
| | | if (t.getMessage() != null && t.getMessage().contains(text)) { |
| | | return true; |
| | | } |
| | | if (t == t.getCause()) { |
| | | break; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Runs the given operation while another session holds every row of the tree in an uncommitted |
| | | * transaction, with only the property of the given class bounding it: the operation must give |
| | | * up inside that bound instead of waiting for a lock that is never released. |
| | | */ |
| | | private void assertBoundedWhileRowsAreLocked(String treeId, JDBCStorage.StatementBound bound, BlockedOperation blocked) |
| | | throws Exception { |
| | | final int boundSeconds = 5; |
| | | final JDBCStorage storage = new JDBCStorage(createBackendCfg(), null); |
| | | final TreeName tree = new TreeName(treeId, "tree"); |
| | | 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)); |
| | | } |
| | | }); |
| | | // another session takes an exclusive lock on every row of the table and keeps it: the |
| | | // same statement clearTree() issues, so it is known to parse on all four dialects |
| | | try (final Connection blocker = DriverManager.getConnection(getJdbcUrl())) { |
| | | blocker.setAutoCommit(false); |
| | | try (final Statement lock = blocker.createStatement()) { |
| | | lock.executeUpdate("delete from " + storage.getTableName(tree)); |
| | | } |
| | | // the rows go back whatever the assertions below do with the run: the cleanup of |
| | | // this method drops the table, which is a bulk statement and unbounded here, so a |
| | | // lock still held would park it until the timeout of the harness and turn one |
| | | // failed assertion into a stalled build |
| | | try { |
| | | // only the class under test is bounded, so a pass through the other one cannot |
| | | // be mistaken for the bound working |
| | | for (final JDBCStorage.StatementBound each : JDBCStorage.StatementBound.values()) { |
| | | System.setProperty(each.property, each == bound ? Integer.toString(boundSeconds) : "0"); |
| | | } |
| | | // the monotonic clock, which is what timedOut() measures the bound with: a step of |
| | | // the wall clock can neither lengthen nor shorten what the assertions below allow |
| | | final long startedAt = System.nanoTime(); |
| | | Exception failure = null; |
| | | try { |
| | | blocked.run(storage, tree); |
| | | fail("the operation must give up while the rows it needs are locked"); |
| | | } catch (Exception expected) { |
| | | failure = expected; // the bound was reached and the transaction rolled back |
| | | } |
| | | final long elapsed = (System.nanoTime() - startedAt) / 1000000L; |
| | | // The failure has to be the one the bound produces, not any failure at all: an |
| | | // operation that fell over at once for an unrelated reason would otherwise pass |
| | | // this test at t=0. timedOut() names the property in the message of everything it |
| | | // classifies as reaching the bound. |
| | | assertTrue(namesTheBound(failure, bound), "gave up with " + stackTraceToSingleLineString(failure) |
| | | + ", which does not name " + bound.property); |
| | | // And under the layer it is supposed to be under. The ceiling below has to be |
| | | // wide enough for the second one, since that is what ends the wait on oracle, |
| | | // and a ceiling that wide cannot tell a working first layer from a missing one: |
| | | // a driver that stops taking setQueryTimeout degrades to the backstop silently |
| | | // by design, ends there, and would be scored as the bound doing its work. The |
| | | // message says which layer it was, so this assertion can too. |
| | | assertFalse(ranUnderTheBackstopAlone(failure), "the driver would not take a query timeout, so " |
| | | + "the statement ran under the socket read timeout alone: " |
| | | + stackTraceToSingleLineString(failure)); |
| | | // And it has to arrive at the bound rather than at something else that happens to |
| | | // end the wait inside a generous ceiling: with the bound deleted, mysql would still |
| | | // come back after its own innodb_lock_wait_timeout of 50 s, and the assertion has |
| | | // to fail then. The ceiling is what the bound really allows a statement, which is |
| | | // the second layer rather than the property: holdBackstop() arms the socket read |
| | | // timeout at the bound plus its margin on every engine, not only on oracle, and a |
| | | // run where the cancel of the driver does not land ends there. Scoring that as a |
| | | // failure would fail this suite for the second layer doing exactly what it exists |
| | | // to do - and on oracle, where a session in a row-lock enqueue never acts on the |
| | | // break its driver sends, that is not an edge case but the normal path. |
| | | final long ceilingSeconds = boundSeconds + JDBCStorage.BACKSTOP_MARGIN_SECONDS + 10; |
| | | // with a little slack under the bound: a driver keeps its timer in whole seconds and |
| | | // may report the cancel a few milliseconds before the bound is arithmetically due, |
| | | // which is the slack timedOut() classifies such a statement with |
| | | assertTrue(elapsed >= boundSeconds * 1000L - JDBCStorage.CLOCK_SLACK_MILLIS, |
| | | "gave up after " + elapsed + " ms, before its bound of " |
| | | + boundSeconds + " s: something other than the bound ended the wait"); |
| | | assertTrue(elapsed < ceilingSeconds * 1000L, "gave up only after " + elapsed + " ms, past the " |
| | | + ceilingSeconds + " s this bound of " + boundSeconds + " s allows"); |
| | | }finally { |
| | | // in a catch of its own: a rollback that throws would otherwise replace the |
| | | // assertion above, and the run would report an unrelated connection problem |
| | | // instead of the bound that was missed. Nothing is lost by swallowing it - a |
| | | // session that cannot roll back has no rows left locked either. |
| | | try { |
| | | blocker.rollback(); |
| | | } catch (SQLException releasingTheRows) { |
| | | // the assertions above are the outcome of this test, not this |
| | | } |
| | | } |
| | | } |
| | | } finally { |
| | | for (final JDBCStorage.StatementBound each : JDBCStorage.StatementBound.values()) { |
| | | System.clearProperty(each.property); |
| | | } |
| | | try { |
| | | storage.write(new WriteOperation() { |
| | | @Override |
| | | public void run(WriteableTransaction txn) throws Exception { |
| | | txn.deleteTree(tree); |
| | | } |
| | | }); |
| | | } catch (Exception ignored) {} |
| | | storage.close(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Forward repositioning inside the already-fetched batch must be served from the buffer without SQL, |
| | | * and batch sizes must grow from "fetchsize.initial" to "fetchsize" on sequential reads (#860). |
| | | */ |
| New file |
| | |
| | | /* |
| | | * 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.mockito.Mockito.any; |
| | | import static org.mockito.Mockito.mock; |
| | | import static org.mockito.Mockito.never; |
| | | import static org.mockito.Mockito.times; |
| | | import static org.mockito.Mockito.verify; |
| | | import static org.mockito.Mockito.when; |
| | | |
| | | import java.io.ByteArrayOutputStream; |
| | | import java.lang.reflect.Field; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.opends.server.DirectoryServerTestCase; |
| | | import org.opends.server.backends.VerifyConfig; |
| | | import org.opends.server.backends.pluggable.spi.AccessMode; |
| | | import org.opends.server.backends.pluggable.spi.Cursor; |
| | | import org.opends.server.backends.pluggable.spi.ReadableTransaction; |
| | | import org.opends.server.backends.pluggable.spi.Storage; |
| | | import org.opends.server.backends.pluggable.spi.TreeName; |
| | | import org.opends.server.backends.pluggable.spi.WriteableTransaction; |
| | | import org.opends.server.core.ServerContext; |
| | | import org.opends.server.crypto.CryptoSuite; |
| | | import org.opends.server.types.LDIFExportConfig; |
| | | import org.testng.annotations.Test; |
| | | |
| | | /** |
| | | * Which cursor the walks of a whole tree ask for (#877). |
| | | * <p> |
| | | * {@code ReadableTransaction.openBulkCursor()} is a {@code default} answering exactly as |
| | | * {@code openCursor()} does, so every storage engine but the JDBC backend behaves the same either |
| | | * way and a call site turned back into {@code openCursor()} would compile, run, and stay invisible |
| | | * everywhere - while on the JDBC backend it would take the bound of a client operation, two |
| | | * minutes by default, and abort the walk of a tree larger than that. Some of these walks have |
| | | * nobody at a command line to see it happen: the load of the compressed schema and the read that |
| | | * checks id2entry is there, both on the path of {@code start-ds}, and the generation ID a |
| | | * replicated domain computes for itself the first time it starts, which is an export of the whole |
| | | * of id2entry. |
| | | * <p> |
| | | * Each test below pins one such call site twice: that the bulk cursor is what it asks for, and |
| | | * that it asks for no cursor of an operation at all. The second half only bites where the walk |
| | | * really runs its body, so a walk whose fixture holds a record - {@code iterateDN2ID} - is given |
| | | * one: with an empty tree the loop stops on its first step and every {@code never()} below it |
| | | * passes on a run that reached nothing. |
| | | * <p> |
| | | * Six call sites are pinned that way - {@code ExportJob}, the id2entry, dn2id and VLV walks of |
| | | * {@code VerifyJob}, and both trees of {@code PersistentCompressedSchema} - together with the |
| | | * children count each row of the dn2id walk reads and the total the progress report of a verify |
| | | * is sized with, the latter pinned one hop above its cursor. Three more are held by other means: |
| | | * {@code ID2Entry.afterOpen()} has {@code ID2EntryTest}, the override that gives the class its |
| | | * meaning has {@code JDBCStatementBoundTestCase}, and {@code VerifyJob.iterateID2ChildrenCount()} |
| | | * cannot be reverted at all, since {@code ID2ChildrenCount} exposes no cursor but the bulk one and |
| | | * the revert would not compile. The last three tests pin a delegation rather than its caller, which |
| | | * is all that is available for them. |
| | | * <p> |
| | | * What none of this covers is the single-row {@code ReadableTransaction.read()} these same walks |
| | | * make - {@code id2entry.get()} once per row of dn2id and of a VLV index - which has no bulk form |
| | | * in the SPI at all and takes the class of the transaction it is made through. That is a gap of |
| | | * the SPI rather than of a call site: a read by primary key is not the scan a cursor batch is, so |
| | | * what it risks is a lock wait rather than a walk cut short. |
| | | * <p> |
| | | * Three call sites are left uncovered and are named here rather than passed over: the attribute |
| | | * index of {@code verify-index} ({@code VerifyJob.iterateAttrIndex}), whose {@code MatchingRuleIndex} |
| | | * is {@code final} and so cannot be handed to this suite, and the two of {@code BackendStat}. All |
| | | * three walk a tree only on the command line of an operator. |
| | | */ |
| | | @SuppressWarnings("javadoc") |
| | | @Test(groups = { "precommit", "pluggablebackend" }, sequential = true) |
| | | public class BulkCursorTest extends DirectoryServerTestCase |
| | | { |
| | | private final TreeName id2entryName = new TreeName("dc=example,dc=com", "id2entry"); |
| | | |
| | | @SuppressWarnings("unchecked") |
| | | private static Cursor<ByteString, ByteString> emptyCursor() |
| | | { |
| | | return mock(Cursor.class); // next() answers false, so the walk stops on its first step |
| | | } |
| | | |
| | | /** A cursor over a single record, so that the body of a walk really runs once. */ |
| | | @SuppressWarnings("unchecked") |
| | | private static Cursor<ByteString, ByteString> cursorOver(ByteString key, ByteString value) |
| | | { |
| | | final Cursor<ByteString, ByteString> cursor = mock(Cursor.class); |
| | | when(cursor.next()).thenReturn(true, false); |
| | | when(cursor.getKey()).thenReturn(key); |
| | | when(cursor.getValue()).thenReturn(value); |
| | | return cursor; |
| | | } |
| | | |
| | | /** A transaction whose every cursor is the empty one above, whichever kind is asked for. */ |
| | | private static ReadableTransaction transactionWithEmptyCursors() |
| | | { |
| | | final ReadableTransaction txn = mock(ReadableTransaction.class); |
| | | when(txn.openBulkCursor(any(TreeName.class))).thenReturn(emptyCursor()); |
| | | when(txn.openCursor(any(TreeName.class))).thenReturn(emptyCursor()); |
| | | return txn; |
| | | } |
| | | |
| | | /** |
| | | * Gives a mock of a tree the name it would have been constructed with. |
| | | * {@code AbstractTree.getName()} is {@code public final} over a private field, so mockito cannot |
| | | * stub it and the instance it builds - whose constructor never runs - answers {@code null}. A |
| | | * production call of {@code openBulkCursor(index.getName())} then passes {@code null}, which |
| | | * {@code any(TreeName.class)} happily matches under the mockito pinned here: the assertion would |
| | | * accept a walk of any tree at all, the wrong one included. |
| | | */ |
| | | private static <T extends AbstractTree> T named(T tree, TreeName name) throws Exception |
| | | { |
| | | final Field field = AbstractTree.class.getDeclaredField("name"); |
| | | field.setAccessible(true); |
| | | field.set(tree, name); |
| | | return tree; |
| | | } |
| | | |
| | | /** |
| | | * An {@code export-ldif} walks the whole of id2entry, and so does the generation ID a replicated |
| | | * domain computes for itself the first time it starts - {@code LDAPReplicationDomain |
| | | * .computeGenerationId()} exports the backend to compute it, with no client operation waiting on |
| | | * it and no operator watching it fail. |
| | | */ |
| | | @Test |
| | | public void testAnExportWalksId2entryWithABulkCursor() throws Exception |
| | | { |
| | | final ReadableTransaction txn = transactionWithEmptyCursors(); |
| | | final EntryContainer entryContainer = mock(EntryContainer.class); |
| | | when(entryContainer.getID2Entry()).thenReturn(new ID2Entry(id2entryName, new DataConfig.Builder().build())); |
| | | |
| | | // LDIFExportConfig is final and nothing is written here, the walk stopping on its first step |
| | | new ExportJob(new LDIFExportConfig(new ByteArrayOutputStream())).exportContainer(txn, entryContainer); |
| | | |
| | | verify(txn).openBulkCursor(id2entryName); |
| | | verify(txn, never()).openCursor(any(TreeName.class)); |
| | | } |
| | | |
| | | /** {@code verify-index} walks id2entry whole, checking every entry against the indexes. */ |
| | | @Test |
| | | public void testAVerifyWalksId2entryWithABulkCursor() throws Exception |
| | | { |
| | | final ReadableTransaction txn = transactionWithEmptyCursors(); |
| | | final VerifyJob job = new VerifyJob(mock(RootContainer.class), mock(VerifyConfig.class)); |
| | | job.id2entry = new ID2Entry(id2entryName, new DataConfig.Builder().build()); |
| | | |
| | | job.iterateID2Entry(txn); |
| | | |
| | | verify(txn).openBulkCursor(id2entryName); |
| | | verify(txn, never()).openCursor(any(TreeName.class)); |
| | | } |
| | | |
| | | /** |
| | | * And it walks dn2id whole as well, rebuilding the children counts as it goes - which is a read |
| | | * of the children count tree per DN, inside that walk and belonging to it. Read as a client |
| | | * operation those would put the bound of an entry read over a verify nobody is waiting on, once |
| | | * for every DN of the backend, so the tree here holds a record: with an empty one the walk stops |
| | | * before its first row and the assertions below hold whatever the counters do. |
| | | */ |
| | | @Test |
| | | public void testAVerifyWalksDn2idAndItsChildrenCountsWithBulkCursors() throws Exception |
| | | { |
| | | final TreeName dn2idName = new TreeName("dc=example,dc=com", "dn2id"); |
| | | final TreeName id2childrenCountName = new TreeName("dc=example,dc=com", "id2childrencount"); |
| | | final ReadableTransaction txn = transactionWithEmptyCursors(); |
| | | // built before it is handed over: stubbing it inside the argument of thenReturn() would open a |
| | | // stubbing while the one it belongs to is still unfinished, which mockito refuses |
| | | final Cursor<ByteString, ByteString> dn2idRows = |
| | | cursorOver(ByteString.valueOfUtf8("dc=example,dc=com"), ByteString.valueOfLong(1)); |
| | | when(txn.openBulkCursor(dn2idName)).thenReturn(dn2idRows); |
| | | final VerifyJob job = new VerifyJob(mock(RootContainer.class), mock(VerifyConfig.class)); |
| | | job.dn2id = new DN2ID(dn2idName, DN.valueOf("dc=example,dc=com")); |
| | | // read for the one row above, and answered with nothing: the entry it points at is not what |
| | | // this suite is about, and a missing one is counted as an error rather than thrown |
| | | job.id2entry = new ID2Entry(id2entryName, new DataConfig.Builder().build()); |
| | | job.id2childrenCount = new ID2ChildrenCount(id2childrenCountName); |
| | | |
| | | job.iterateDN2ID(txn); |
| | | |
| | | verify(txn).openBulkCursor(dn2idName); |
| | | verify(txn).openBulkCursor(id2childrenCountName); // the count of the DN the walk just passed |
| | | verify(txn, never()).openCursor(any(TreeName.class)); |
| | | } |
| | | |
| | | /** A VLV index of a verify is walked whole too, key by key. */ |
| | | @Test |
| | | public void testAVerifyWalksAVlvIndexWithABulkCursor() throws Exception |
| | | { |
| | | final TreeName vlvIndexName = new TreeName("dc=example,dc=com", "vlv.people"); |
| | | final ReadableTransaction txn = transactionWithEmptyCursors(); |
| | | final VerifyJob job = new VerifyJob(mock(RootContainer.class), mock(VerifyConfig.class)); |
| | | |
| | | job.iterateVLVIndex(txn, named(mock(VLVIndex.class), vlvIndexName), true); |
| | | |
| | | verify(txn).openBulkCursor(vlvIndexName); |
| | | verify(txn, never()).openCursor(any(TreeName.class)); |
| | | } |
| | | |
| | | /** |
| | | * The compressed schema is loaded by walking both of its trees whole, while the backend opens: |
| | | * {@code RootContainer.open()} constructs it before a single client operation can run, and a |
| | | * walk cut short there is a backend that does not open at all. |
| | | */ |
| | | @Test |
| | | public void testTheCompressedSchemaIsLoadedWithBulkCursors() throws Exception |
| | | { |
| | | final WriteableTransaction txn = mock(WriteableTransaction.class); |
| | | // both trees are there and empty: loadTrees() walks only a tree that exists (#873), and the |
| | | // record counts a mock answers with leave nothing to migrate from the legacy pair |
| | | when(txn.treeExists(any(TreeName.class))).thenReturn(true); |
| | | when(txn.openBulkCursor(any(TreeName.class))).thenReturn(emptyCursor()); |
| | | when(txn.openCursor(any(TreeName.class))).thenReturn(emptyCursor()); |
| | | |
| | | new PersistentCompressedSchema(mock(ServerContext.class), "bulkCursorTest", mock(Storage.class), txn, |
| | | AccessMode.READ_ONLY); |
| | | |
| | | verify(txn, times(2)).openBulkCursor(any(TreeName.class)); // the object classes and the attributes |
| | | verify(txn, never()).openCursor(any(TreeName.class)); |
| | | } |
| | | |
| | | /** |
| | | * An index walked whole - by {@code verify-index} or by {@code dbtest} - asks the transaction |
| | | * for a bulk cursor, while {@code Index.openCursor()} stays what an operation evaluating a |
| | | * filter takes. This pins the delegation rather than either of its call sites: the index a |
| | | * verify walks is a {@code MatchingRuleIndex}, which is {@code final} and cannot be handed to a |
| | | * mock, and the one {@code dbtest} walks is chosen inside {@code BackendStat}. |
| | | */ |
| | | @Test |
| | | public void testAnIndexWalkedWholeAsksForABulkCursor() throws Exception |
| | | { |
| | | final TreeName indexName = new TreeName("dc=example,dc=com", "cn.caseIgnoreMatch"); |
| | | final ReadableTransaction txn = transactionWithEmptyCursors(); |
| | | final CryptoSuite cryptoSuite = mock(CryptoSuite.class); |
| | | when(cryptoSuite.isEncrypted()).thenReturn(false); |
| | | final DefaultIndex index = |
| | | new DefaultIndex(indexName, mock(State.class), 5, mock(EntryContainer.class), cryptoSuite); |
| | | |
| | | index.openBulkCursor(txn); |
| | | |
| | | verify(txn).openBulkCursor(indexName); |
| | | verify(txn, never()).openCursor(any(TreeName.class)); |
| | | } |
| | | |
| | | /** |
| | | * The children counts are walked whole by {@code verify-index} through {@code ShardedCounter}. |
| | | * The delegation again, its one call site - {@code VerifyJob.iterateID2ChildrenCount()} - being |
| | | * held by the compiler instead: {@code ID2ChildrenCount} exposes no cursor but this one, so a |
| | | * revert to {@code openCursor} does not compile. |
| | | */ |
| | | @Test |
| | | public void testTheChildrenCountsAreWalkedWithABulkCursor() throws Exception |
| | | { |
| | | final TreeName id2childrenCountName = new TreeName("dc=example,dc=com", "id2childrencount"); |
| | | final ReadableTransaction txn = transactionWithEmptyCursors(); |
| | | |
| | | new ID2ChildrenCount(id2childrenCountName).openBulkCursor(txn); |
| | | |
| | | verify(txn).openBulkCursor(id2childrenCountName); |
| | | verify(txn, never()).openCursor(any(TreeName.class)); |
| | | } |
| | | |
| | | /** |
| | | * The record check inside that walk is bulk as well: {@code VerifyJob.iterateID2ChildrenCount()} |
| | | * asks it once per record of the children count tree, so a cursor of a client operation there is |
| | | * the same hazard as one over the tree itself. The delegation again - that walk is private, and |
| | | * {@code containsEntryID} has no other caller to keep an operation-class form for. |
| | | */ |
| | | @Test |
| | | public void testTheRecordCheckOfAWholeTreeWalkAsksForABulkCursor() throws Exception |
| | | { |
| | | final ReadableTransaction txn = transactionWithEmptyCursors(); |
| | | |
| | | new ID2Entry(id2entryName, new DataConfig.Builder().build()).containsEntryID(txn, new EntryID(1)); |
| | | |
| | | verify(txn).openBulkCursor(id2entryName); |
| | | verify(txn, never()).openCursor(any(TreeName.class)); |
| | | } |
| | | |
| | | /** |
| | | * And a count read outside such a walk is a client operation, which the bulk read above must not |
| | | * quietly turn into: an LDAP search asking for {@code numSubordinates} reads one, and there a |
| | | * bound of a client operation is exactly what it should take. |
| | | */ |
| | | @Test |
| | | public void testAChildrenCountOfAClientOperationStaysAnOperation() throws Exception |
| | | { |
| | | final TreeName id2childrenCountName = new TreeName("dc=example,dc=com", "id2childrencount"); |
| | | final ReadableTransaction txn = transactionWithEmptyCursors(); |
| | | |
| | | new ID2ChildrenCount(id2childrenCountName).getCount(txn, new EntryID(1)); |
| | | |
| | | verify(txn).openCursor(id2childrenCountName); |
| | | verify(txn, never()).openBulkCursor(any(TreeName.class)); |
| | | } |
| | | |
| | | /** |
| | | * The total is one such count read on one key, so it takes the class of whoever asks: the walk |
| | | * that reads it to size its progress report, or the client operation that reads the same total. |
| | | */ |
| | | @Test |
| | | public void testTheTotalCountOfAWholeTreeWalkAsksForABulkCursor() throws Exception |
| | | { |
| | | final TreeName id2childrenCountName = new TreeName("dc=example,dc=com", "id2childrencount"); |
| | | final ReadableTransaction txn = transactionWithEmptyCursors(); |
| | | |
| | | new ID2ChildrenCount(id2childrenCountName).getTotalCount(txn, true); |
| | | |
| | | verify(txn).openBulkCursor(id2childrenCountName); |
| | | verify(txn, never()).openCursor(any(TreeName.class)); |
| | | } |
| | | |
| | | /** And {@code cn=monitor} reading the same total is a client operation. */ |
| | | @Test |
| | | public void testTheTotalCountOfAClientOperationStaysAnOperation() throws Exception |
| | | { |
| | | final TreeName id2childrenCountName = new TreeName("dc=example,dc=com", "id2childrencount"); |
| | | final ReadableTransaction txn = transactionWithEmptyCursors(); |
| | | |
| | | new ID2ChildrenCount(id2childrenCountName).getTotalCount(txn); |
| | | |
| | | verify(txn).openCursor(id2childrenCountName); |
| | | verify(txn, never()).openBulkCursor(any(TreeName.class)); |
| | | } |
| | | |
| | | /** |
| | | * The count a verify reads to size its progress report belongs to the walk it measures. Its |
| | | * three siblings - the record counts of dn2id, of the children count tree and of a VLV index - |
| | | * are bulk by the tree they count, and this one was the branch left reading as a client |
| | | * operation: it is also the only one a plain {@code verify-index} reaches, the other three |
| | | * being the {@code --clean} path. |
| | | * <p> |
| | | * Pinned on the container rather than on a cursor, that read being one hop further down: |
| | | * {@code getNumberOfEntriesInBaseDN0} to {@code ID2ChildrenCount.getTotalCount} to the cursor |
| | | * the two tests above pin. |
| | | */ |
| | | @Test |
| | | public void testTheProgressCountOfAVerifyIsReadAsPartOfItsWalk() throws Exception |
| | | { |
| | | final DN baseDN = DN.valueOf("dc=example,dc=com"); |
| | | final VerifyConfig verifyConfig = mock(VerifyConfig.class); |
| | | when(verifyConfig.getBaseDN()).thenReturn(baseDN); |
| | | final EntryContainer entryContainer = mock(EntryContainer.class); |
| | | final RootContainer rootContainer = mock(RootContainer.class); |
| | | when(rootContainer.getEntryContainer(baseDN)).thenReturn(entryContainer); |
| | | final ReadableTransaction txn = transactionWithEmptyCursors(); |
| | | |
| | | // false: the entry iterator, which is what a verify-index runs unless it was given --clean |
| | | new VerifyJob(rootContainer, verifyConfig).new ProgressTask(false, txn); |
| | | |
| | | verify(entryContainer).getNumberOfEntriesInBaseDN0(txn, true); |
| | | verify(entryContainer, never()).getNumberOfEntriesInBaseDN0(txn); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 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.mockito.Mockito.mock; |
| | | import static org.mockito.Mockito.verify; |
| | | import static org.mockito.Mockito.when; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.opends.server.DirectoryServerTestCase; |
| | | import org.opends.server.backends.pluggable.spi.Cursor; |
| | | import org.opends.server.backends.pluggable.spi.TreeName; |
| | | import org.opends.server.backends.pluggable.spi.WriteableTransaction; |
| | | import org.testng.annotations.Test; |
| | | |
| | | @SuppressWarnings("javadoc") |
| | | @Test(groups = { "precommit", "pluggablebackend" }, sequential = true) |
| | | public class ID2EntryTest extends DirectoryServerTestCase |
| | | { |
| | | /** |
| | | * The read that checks the tree is there when a backend opens asks for a bulk cursor. Its first |
| | | * batch carries no key to seek on, so a storage engine sees a walk of the whole tree - on the |
| | | * JDBC backend against SQL Server, a scan and a sort of it, {@code k} being a |
| | | * {@code varbinary(max)} that cannot be an index key - and this runs once per base DN on every |
| | | * open, outside the try/catch of {@code BackendImpl.openBackend()}. Bounded as the work of a |
| | | * client operation, a large backend would stop opening at all (#877). |
| | | */ |
| | | @Test |
| | | public void testTheReadThatOpensTheTreeAsksForABulkCursor() throws Exception |
| | | { |
| | | final TreeName name = new TreeName("dc=example,dc=com", "id2entry"); |
| | | final WriteableTransaction txn = mock(WriteableTransaction.class); |
| | | @SuppressWarnings("unchecked") |
| | | final Cursor<ByteString, ByteString> cursor = mock(Cursor.class); |
| | | when(txn.openBulkCursor(name)).thenReturn(cursor); |
| | | |
| | | new ID2Entry(name, new DataConfig.Builder().build()).open(txn, false); |
| | | |
| | | verify(txn).openBulkCursor(name); |
| | | verify(cursor).next(); |
| | | } |
| | | } |