| | |
| | | * Oracle asserts the other half of the same contract: nothing of ours is set there, because its |
| | | * own {@code ddl_lock_timeout} gives up at once - so the drop still fails rather than hanging, and |
| | | * the failure names the engine's own doing rather than a property of ours that armed nothing. |
| | | * <p> |
| | | * The bound is set looser than the row lock bound of the write this drop runs inside, deliberately: |
| | | * sql server has one {@code LOCK_TIMEOUT} for both waits, so a DDL bound tighter than the row one |
| | | * passes this case whether this backend armed it or merely inherited the row bound of the write |
| | | * around it - and inheriting it is exactly what would leave this property governing nothing and the |
| | | * failure naming neither property (#915). |
| | | */ |
| | | @Test(timeOut = 120000) |
| | | public void testTheDdlGivesUpOnALockAnotherSessionHolds() throws Exception { |
| | | final TreeName tree = new TreeName("testDdlLockBound", "tree"); |
| | | final JDBCStorage storage = new JDBCStorage(createBackendCfg(), null); |
| | | System.setProperty(JDBCStorage.DDL_LOCK_TIMEOUT_PROPERTY, "2"); |
| | | System.setProperty(JDBCStorage.DDL_LOCK_TIMEOUT_PROPERTY, "7"); |
| | | try { |
| | | storage.open(AccessMode.READ_WRITE); |
| | | storage.write(new WriteOperation() { |
| | |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * A write queued behind a row lock another session holds gives up at |
| | | * {@code ROW_LOCK_TIMEOUT_PROPERTY} and is replayed, and the replay goes through once that session |
| | | * lets the rows go: the wait ends inside the replay window instead of consuming it, which is what |
| | | * left the operation with no replay at all before (#903, #915). |
| | | * <p> |
| | | * On an engine with no session setting for that wait - oracle - the same write waits the blocker |
| | | * out on its first attempt and needs no replay, which is what "left alone" means there. That is |
| | | * asserted rather than skipped: it is the difference this bound makes, read from the one place it |
| | | * shows. |
| | | */ |
| | | @Test(timeOut = 300000) |
| | | public void testAWriteBlockedByARowLockIsReplayedInsideTheWindow() throws Exception { |
| | | final int boundSeconds = 2; |
| | | final TreeName tree = new TreeName("testRowLockBound", "tree"); |
| | | final JDBCStorage storage = new JDBCStorage(createBackendCfg(), null); |
| | | System.setProperty(JDBCStorage.ROW_LOCK_TIMEOUT_PROPERTY, Integer.toString(boundSeconds)); |
| | | final ExecutorService releasing = Executors.newSingleThreadExecutor(); |
| | | 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)); |
| | | } |
| | | }); |
| | | |
| | | // a session of its own, taking an exclusive lock on the row the write below wants: an update |
| | | // that changes nothing takes the same lock as one that does, and it parses on all four engines |
| | | try (final Connection holder = DriverManager.getConnection(getJdbcUrl())) { |
| | | holder.setAutoCommit(false); |
| | | try (final PreparedStatement lock = holder.prepareStatement( |
| | | "update " + storage.getTableName(tree) + " set v=v")) { |
| | | lock.executeUpdate(); |
| | | } |
| | | // held for twice the bound, so that the first attempt of the write gives up on it and the |
| | | // replay finds the row free - both inside the replay window, which is what this is about |
| | | final Future<?> released = releasing.submit(new Callable<Void>() { |
| | | @Override |
| | | public Void call() throws Exception { |
| | | Thread.sleep(boundSeconds * 2 * 1000L); |
| | | holder.rollback(); // the rows go back exactly as they were |
| | | return null; |
| | | } |
| | | }); |
| | | |
| | | final AtomicInteger attempts = new AtomicInteger(); |
| | | final long startedAt = System.nanoTime(); |
| | | storage.write(new WriteOperation() { |
| | | @Override |
| | | public void run(WriteableTransaction txn) throws Exception { |
| | | attempts.incrementAndGet(); |
| | | txn.put(tree, key(1), value(2)); |
| | | } |
| | | }); |
| | | final long elapsedMillis = (System.nanoTime() - startedAt) / 1000000L; |
| | | released.get(); // the failure of the holder, if it had one, rather than a stuck rollback |
| | | |
| | | if (dialect() == JDBCStorage.Dialect.ORACLE) { |
| | | assertEquals(attempts.get(), 1, "oracle has no session setting for a row lock enqueue, so the" |
| | | + " write should have waited the holder out on its first attempt (" + elapsedMillis + " ms)"); |
| | | } else { |
| | | assertTrue(attempts.get() >= 2, "the write was not replayed: it waited " + elapsedMillis |
| | | + " ms for a row lock that this engine was told to give up on after " + boundSeconds + " s"); |
| | | assertTrue(elapsedMillis >= boundSeconds * 1000L - JDBCStorage.CLOCK_SLACK_MILLIS, |
| | | "the write came back after " + elapsedMillis + " ms, before the bound it was given:" |
| | | + " something other than the row lock ended its first attempt"); |
| | | } |
| | | } |
| | | } finally { |
| | | System.clearProperty(JDBCStorage.ROW_LOCK_TIMEOUT_PROPERTY); |
| | | releasing.shutdownNow(); |
| | | try { |
| | | storage.write(new WriteOperation() { |
| | | @Override |
| | | public void run(WriteableTransaction txn) throws Exception { |
| | | txn.deleteTree(tree); |
| | | } |
| | | }); |
| | | } catch (Exception ignored) { |
| | | } finally { |
| | | storage.close(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private interface BlockedOperation { |
| | | void run(JDBCStorage storage, TreeName tree) throws Exception; |
| | | } |
| | |
| | | for (final JDBCStorage.StatementBound each : JDBCStorage.StatementBound.values()) { |
| | | System.setProperty(each.property, each == bound ? Integer.toString(boundSeconds) : "0"); |
| | | } |
| | | // and the row lock bound of a write is off here for the same reason: it is tighter |
| | | // than the bound under test, so it - and not the statement bound this case is about - |
| | | // would be what ended the wait, and the failure would name no property of this class |
| | | // (#915). testAWriteBlockedByARowLockIsReplayedInsideTheWindow covers that layer |
| | | System.setProperty(JDBCStorage.ROW_LOCK_TIMEOUT_PROPERTY, "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(); |
| | |
| | | for (final JDBCStorage.StatementBound each : JDBCStorage.StatementBound.values()) { |
| | | System.clearProperty(each.property); |
| | | } |
| | | System.clearProperty(JDBCStorage.ROW_LOCK_TIMEOUT_PROPERTY); |
| | | try { |
| | | storage.write(new WriteOperation() { |
| | | @Override |