@Test public void testCheckpointRollback() throws Exception { // start a transaction, using checkpoints between writes transactionContext.start(); transactionAwareHTable.put( new Put(TestBytes.row).add(TestBytes.family, TestBytes.qualifier, TestBytes.value)); transactionContext.checkpoint(); transactionAwareHTable.put( new Put(TestBytes.row2).add(TestBytes.family, TestBytes.qualifier, TestBytes.value2)); transactionContext.checkpoint(); transactionAwareHTable.put( new Put(TestBytes.row3).add(TestBytes.family, TestBytes.qualifier, TestBytes.value)); transactionContext.abort(); transactionContext.start(); verifyRow(transactionAwareHTable, TestBytes.row, null); verifyRow(transactionAwareHTable, TestBytes.row2, null); verifyRow(transactionAwareHTable, TestBytes.row3, null); Scan scan = new Scan(); ResultScanner scanner = transactionAwareHTable.getScanner(scan); assertNull(scanner.next()); scanner.close(); transactionContext.finish(); }
/** * Test aborted transactional delete requests, that must be rolled back. * * @throws Exception */ @Test public void testAbortedTransactionalDelete() throws Exception { transactionContext.start(); Put put = new Put(TestBytes.row); put.add(TestBytes.family, TestBytes.qualifier, TestBytes.value); transactionAwareHTable.put(put); transactionContext.finish(); transactionContext.start(); Result result = transactionAwareHTable.get(new Get(TestBytes.row)); transactionContext.finish(); byte[] value = result.getValue(TestBytes.family, TestBytes.qualifier); assertArrayEquals(TestBytes.value, value); transactionContext.start(); Delete delete = new Delete(TestBytes.row); transactionAwareHTable.delete(delete); transactionContext.abort(); transactionContext.start(); result = transactionAwareHTable.get(new Get(TestBytes.row)); transactionContext.finish(); value = result.getValue(TestBytes.family, TestBytes.qualifier); assertArrayEquals(TestBytes.value, value); }
/** * Tests that each transaction can see its own persisted writes, while not seeing writes from * other in-progress transactions. */ @Test public void testReadYourWrites() throws Exception { // In-progress tx1: started before our main transaction HTable hTable1 = new HTable(testUtil.getConfiguration(), TestBytes.table); TransactionAwareHTable txHTable1 = new TransactionAwareHTable(hTable1); TransactionContext inprogressTxContext1 = new TransactionContext(new InMemoryTxSystemClient(txManager), txHTable1); // In-progress tx2: started while our main transaction is running HTable hTable2 = new HTable(testUtil.getConfiguration(), TestBytes.table); TransactionAwareHTable txHTable2 = new TransactionAwareHTable(hTable2); TransactionContext inprogressTxContext2 = new TransactionContext(new InMemoryTxSystemClient(txManager), txHTable2); // create an in-progress write that should be ignored byte[] col2 = Bytes.toBytes("col2"); inprogressTxContext1.start(); Put putCol2 = new Put(TestBytes.row); byte[] valueCol2 = Bytes.toBytes("writing in progress"); putCol2.add(TestBytes.family, col2, valueCol2); txHTable1.put(putCol2); // start a tx and write a value to test reading in same tx transactionContext.start(); Put put = new Put(TestBytes.row); byte[] value = Bytes.toBytes("writing"); put.add(TestBytes.family, TestBytes.qualifier, value); transactionAwareHTable.put(put); // test that a write from a tx started after the first is not visible inprogressTxContext2.start(); Put put2 = new Put(TestBytes.row); byte[] value2 = Bytes.toBytes("writing2"); put2.add(TestBytes.family, TestBytes.qualifier, value2); txHTable2.put(put2); Get get = new Get(TestBytes.row); Result row = transactionAwareHTable.get(get); assertFalse(row.isEmpty()); byte[] col1Value = row.getValue(TestBytes.family, TestBytes.qualifier); Assert.assertNotNull(col1Value); Assert.assertArrayEquals(value, col1Value); // write from in-progress transaction should not be visible byte[] col2Value = row.getValue(TestBytes.family, col2); assertNull(col2Value); // commit in-progress transaction, should still not be visible inprogressTxContext1.finish(); get = new Get(TestBytes.row); row = transactionAwareHTable.get(get); assertFalse(row.isEmpty()); col2Value = row.getValue(TestBytes.family, col2); assertNull(col2Value); transactionContext.finish(); inprogressTxContext2.abort(); }
@Test public void testExternalTxContext() throws Exception { ResultSet rs; Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES); Connection conn = DriverManager.getConnection(getUrl(), props); conn.setAutoCommit(false); PhoenixConnection pconn = conn.unwrap(PhoenixConnection.class); TransactionSystemClient txServiceClient = pconn.getQueryServices().getTransactionSystemClient(); String fullTableName = "T"; Statement stmt = conn.createStatement(); stmt.execute( "CREATE TABLE " + fullTableName + "(K VARCHAR PRIMARY KEY, V1 VARCHAR, V2 VARCHAR) TRANSACTIONAL=true"); HTableInterface htable = pconn.getQueryServices().getTable(Bytes.toBytes(fullTableName)); stmt.executeUpdate("upsert into " + fullTableName + " values('x', 'a', 'a')"); conn.commit(); try (Connection newConn = DriverManager.getConnection(getUrl(), props)) { rs = newConn.createStatement().executeQuery("select count(*) from " + fullTableName); assertTrue(rs.next()); assertEquals(1, rs.getInt(1)); } // Use HBase level Tephra APIs to start a new transaction TransactionAwareHTable txAware = new TransactionAwareHTable(htable, TxConstants.ConflictDetection.ROW); TransactionContext txContext = new TransactionContext(txServiceClient, txAware); txContext.start(); // Use HBase APIs to add a new row Put put = new Put(Bytes.toBytes("z")); put.addColumn( QueryConstants.DEFAULT_COLUMN_FAMILY_BYTES, QueryConstants.EMPTY_COLUMN_BYTES, QueryConstants.EMPTY_COLUMN_VALUE_BYTES); put.addColumn( QueryConstants.DEFAULT_COLUMN_FAMILY_BYTES, Bytes.toBytes("V1"), Bytes.toBytes("b")); txAware.put(put); // Use Phoenix APIs to add new row (sharing the transaction context) pconn.setTransactionContext(txContext); conn.createStatement().executeUpdate("upsert into " + fullTableName + " values('y', 'c', 'c')"); // New connection should not see data as it hasn't been committed yet try (Connection newConn = DriverManager.getConnection(getUrl(), props)) { rs = newConn.createStatement().executeQuery("select count(*) from " + fullTableName); assertTrue(rs.next()); assertEquals(1, rs.getInt(1)); } // Use new connection to create a row with a conflict Connection connWithConflict = DriverManager.getConnection(getUrl(), props); connWithConflict .createStatement() .execute("upsert into " + fullTableName + " values('z', 'd', 'd')"); // Existing connection should see data even though it hasn't been committed yet rs = conn.createStatement().executeQuery("select count(*) from " + fullTableName); assertTrue(rs.next()); assertEquals(3, rs.getInt(1)); // Use Tephra APIs directly to finish (i.e. commit) the transaction txContext.finish(); // Confirm that attempt to commit row with conflict fails try { connWithConflict.commit(); fail(); } catch (SQLException e) { assertEquals( SQLExceptionCode.TRANSACTION_CONFLICT_EXCEPTION.getErrorCode(), e.getErrorCode()); } finally { connWithConflict.close(); } // New connection should now see data as it has been committed try (Connection newConn = DriverManager.getConnection(getUrl(), props)) { rs = newConn.createStatement().executeQuery("select count(*) from " + fullTableName); assertTrue(rs.next()); assertEquals(3, rs.getInt(1)); } // Repeat the same as above, but this time abort the transaction txContext = new TransactionContext(txServiceClient, txAware); txContext.start(); // Use HBase APIs to add a new row put = new Put(Bytes.toBytes("j")); put.addColumn( QueryConstants.DEFAULT_COLUMN_FAMILY_BYTES, QueryConstants.EMPTY_COLUMN_BYTES, QueryConstants.EMPTY_COLUMN_VALUE_BYTES); put.addColumn( QueryConstants.DEFAULT_COLUMN_FAMILY_BYTES, Bytes.toBytes("V1"), Bytes.toBytes("e")); txAware.put(put); // Use Phoenix APIs to add new row (sharing the transaction context) pconn.setTransactionContext(txContext); conn.createStatement().executeUpdate("upsert into " + fullTableName + " values('k', 'f', 'f')"); // Existing connection should see data even though it hasn't been committed yet rs = conn.createStatement().executeQuery("select count(*) from " + fullTableName); assertTrue(rs.next()); assertEquals(5, rs.getInt(1)); connWithConflict .createStatement() .execute("upsert into " + fullTableName + " values('k', 'g', 'g')"); rs = connWithConflict.createStatement().executeQuery("select count(*) from " + fullTableName); assertTrue(rs.next()); assertEquals(4, rs.getInt(1)); // Use Tephra APIs directly to abort (i.e. rollback) the transaction txContext.abort(); rs = conn.createStatement().executeQuery("select count(*) from " + fullTableName); assertTrue(rs.next()); assertEquals(3, rs.getInt(1)); // Should succeed since conflicting row was aborted connWithConflict.commit(); // New connection should now see data as it has been committed try (Connection newConn = DriverManager.getConnection(getUrl(), props)) { rs = newConn.createStatement().executeQuery("select count(*) from " + fullTableName); assertTrue(rs.next()); assertEquals(4, rs.getInt(1)); } // Even using HBase APIs directly, we shouldn't find 'j' since a delete marker would have been // written to hide it. Result result = htable.get(new Get(Bytes.toBytes("j"))); assertTrue(result.isEmpty()); }
@Test public void testNoneLevelConflictDetection() throws Exception { InMemoryTxSystemClient txClient = new InMemoryTxSystemClient(txManager); TransactionAwareHTable txTable1 = new TransactionAwareHTable( new HTable(conf, TestBytes.table), TxConstants.ConflictDetection.NONE); TransactionContext txContext1 = new TransactionContext(txClient, txTable1); TransactionAwareHTable txTable2 = new TransactionAwareHTable( new HTable(conf, TestBytes.table), TxConstants.ConflictDetection.NONE); TransactionContext txContext2 = new TransactionContext(txClient, txTable2); // overlapping writes to the same row + column should not conflict txContext1.start(); txTable1.put( new Put(TestBytes.row).add(TestBytes.family, TestBytes.qualifier, TestBytes.value)); // changes should not be visible yet txContext2.start(); Result row = txTable2.get(new Get(TestBytes.row)); assertTrue(row.isEmpty()); txTable2.put( new Put(TestBytes.row).add(TestBytes.family, TestBytes.qualifier, TestBytes.value2)); // both commits should succeed txContext1.finish(); txContext2.finish(); txContext1.start(); row = txTable1.get(new Get(TestBytes.row)); assertFalse(row.isEmpty()); assertArrayEquals(TestBytes.value2, row.getValue(TestBytes.family, TestBytes.qualifier)); txContext1.finish(); // transaction abort should still rollback changes txContext1.start(); txTable1.put( new Put(TestBytes.row2).add(TestBytes.family, TestBytes.qualifier, TestBytes.value)); txContext1.abort(); // changes to row2 should be rolled back txContext2.start(); Result row2 = txTable2.get(new Get(TestBytes.row2)); assertTrue(row2.isEmpty()); txContext2.finish(); // transaction invalidate should still make changes invisible txContext1.start(); Transaction tx1 = txContext1.getCurrentTransaction(); txTable1.put( new Put(TestBytes.row3).add(TestBytes.family, TestBytes.qualifier, TestBytes.value)); assertNotNull(tx1); txClient.invalidate(tx1.getWritePointer()); // changes to row2 should be rolled back txContext2.start(); Result row3 = txTable2.get(new Get(TestBytes.row3)); assertTrue(row3.isEmpty()); txContext2.finish(); }
@Test public void testRowLevelConflictDetection() throws Exception { TransactionAwareHTable txTable1 = new TransactionAwareHTable( new HTable(conf, TestBytes.table), TxConstants.ConflictDetection.ROW); TransactionContext txContext1 = new TransactionContext(new InMemoryTxSystemClient(txManager), txTable1); TransactionAwareHTable txTable2 = new TransactionAwareHTable( new HTable(conf, TestBytes.table), TxConstants.ConflictDetection.ROW); TransactionContext txContext2 = new TransactionContext(new InMemoryTxSystemClient(txManager), txTable2); byte[] row1 = Bytes.toBytes("row1"); byte[] row2 = Bytes.toBytes("row2"); byte[] col1 = Bytes.toBytes("c1"); byte[] col2 = Bytes.toBytes("c2"); byte[] val1 = Bytes.toBytes("val1"); byte[] val2 = Bytes.toBytes("val2"); // test that concurrent writing to different rows succeeds txContext1.start(); txTable1.put(new Put(row1).add(TestBytes.family, col1, val1)); txContext2.start(); txTable2.put(new Put(row2).add(TestBytes.family, col1, val2)); // should be no conflicts txContext1.finish(); txContext2.finish(); transactionContext.start(); Result res = transactionAwareHTable.get(new Get(row1)); assertFalse(res.isEmpty()); Cell cell = res.getColumnLatestCell(TestBytes.family, col1); assertNotNull(cell); assertArrayEquals(val1, CellUtil.cloneValue(cell)); res = transactionAwareHTable.get(new Get(row2)); assertFalse(res.isEmpty()); cell = res.getColumnLatestCell(TestBytes.family, col1); assertNotNull(cell); assertArrayEquals(val2, CellUtil.cloneValue(cell)); transactionContext.finish(); // test that writing to different columns in the same row fails txContext1.start(); txTable1.put(new Put(row1).add(TestBytes.family, col1, val2)); txContext2.start(); txTable2.put(new Put(row1).add(TestBytes.family, col2, val2)); txContext1.finish(); try { txContext2.finish(); fail("txContext2 should have encountered a row-level conflict during commit"); } catch (TransactionConflictException tce) { txContext2.abort(); } transactionContext.start(); res = transactionAwareHTable.get(new Get(row1)); assertFalse(res.isEmpty()); cell = res.getColumnLatestCell(TestBytes.family, col1); assertNotNull(cell); // should now be val2 assertArrayEquals(val2, CellUtil.cloneValue(cell)); cell = res.getColumnLatestCell(TestBytes.family, col2); // col2 should not be visible due to conflict assertNull(cell); transactionContext.finish(); // test that writing to the same column in the same row fails txContext1.start(); txTable1.put(new Put(row2).add(TestBytes.family, col2, val1)); txContext2.start(); txTable2.put(new Put(row2).add(TestBytes.family, col2, val2)); txContext1.finish(); try { txContext2.finish(); fail("txContext2 should have encountered a row and column level conflict during commit"); } catch (TransactionConflictException tce) { txContext2.abort(); } transactionContext.start(); res = transactionAwareHTable.get(new Get(row2)); assertFalse(res.isEmpty()); cell = res.getColumnLatestCell(TestBytes.family, col2); assertNotNull(cell); // should now be val1 assertArrayEquals(val1, CellUtil.cloneValue(cell)); transactionContext.finish(); // verify change set that is being reported only on rows txContext1.start(); txTable1.put(new Put(row1).add(TestBytes.family, col1, val1)); txTable1.put(new Put(row2).add(TestBytes.family, col2, val2)); Collection<byte[]> changeSet = txTable1.getTxChanges(); assertNotNull(changeSet); assertEquals(2, changeSet.size()); assertTrue(changeSet.contains(txTable1.getChangeKey(row1, null, null))); assertTrue(changeSet.contains(txTable1.getChangeKey(row2, null, null))); txContext1.finish(); }