diff --git a/phoenix-core/src/it/java/org/apache/phoenix/replication/SystemCatalogWALEntryFilterIT.java b/phoenix-core/src/it/java/org/apache/phoenix/replication/SystemCatalogWALEntryFilterIT.java index 1b5b34783e1..ab6af25aa0a 100644 --- a/phoenix-core/src/it/java/org/apache/phoenix/replication/SystemCatalogWALEntryFilterIT.java +++ b/phoenix-core/src/it/java/org/apache/phoenix/replication/SystemCatalogWALEntryFilterIT.java @@ -31,6 +31,7 @@ import org.apache.phoenix.jdbc.PhoenixDatabaseMetaData; import org.apache.phoenix.mapreduce.util.ConnectionUtil; import org.apache.phoenix.schema.PTable; +import org.apache.phoenix.util.ByteUtil; import org.apache.phoenix.util.PhoenixRuntime; import org.apache.phoenix.util.ReadOnlyProps; import org.apache.phoenix.util.TestUtil; @@ -70,7 +71,8 @@ public class SystemCatalogWALEntryFilterIT extends ParallelStatsDisabledIT { private static final String DROP_NONTENANT_VIEW_SQL = "DROP VIEW IF EXISTS " + NONTENANT_VIEW_NAME; private static PTable catalogTable; private static PTable childLinkTable; - private static WALKey walKey = null; + private static WALKey walKeyCatalog = null; + private static WALKey walKeyChildLink = null; private static TableName systemCatalogTableName = TableName.valueOf(PhoenixDatabaseMetaData.SYSTEM_CATALOG_NAME); private static TableName systemChildLinkTableName = @@ -89,7 +91,10 @@ public static synchronized void setup() throws Exception { connection.createStatement().execute(CREATE_TENANT_VIEW_SQL); catalogTable = PhoenixRuntime.getTable(connection, PhoenixDatabaseMetaData.SYSTEM_CATALOG_NAME); childLinkTable = PhoenixRuntime.getTable(connection, PhoenixDatabaseMetaData.SYSTEM_CHILD_LINK_NAME); - walKey = new WALKey(REGION, TableName.valueOf(PhoenixDatabaseMetaData.SYSTEM_CATALOG_NAME), 0, 0, uuid); + walKeyCatalog = new WALKey(REGION, TableName.valueOf( + PhoenixDatabaseMetaData.SYSTEM_CATALOG_NAME), 0, 0, uuid); + walKeyChildLink = new WALKey(REGION, TableName.valueOf( + PhoenixDatabaseMetaData.SYSTEM_CHILD_LINK_NAME), 0, 0, uuid); }; Assert.assertNotNull(catalogTable); try (java.sql.Connection connection = @@ -124,42 +129,88 @@ public void testOtherTablesAutoPass() throws Exception { @Test public void testSystemCatalogWALEntryFilter() throws Exception { + // now create WAL.Entry objects that refer to cells in those view rows in + // System.Catalog + Get tenantGetCatalog = getGet(catalogTable, TENANT_BYTES, TENANT_VIEW_NAME); + Get nonTenantGetCatalog = getGet(catalogTable, DEFAULT_TENANT_BYTES, NONTENANT_VIEW_NAME); - //now create WAL.Entry objects that refer to cells in those view rows in System.Catalog - - Get tenantGet = getGet(catalogTable, TENANT_BYTES, TENANT_VIEW_NAME); - Get nonTenantGet = getGet(catalogTable, DEFAULT_TENANT_BYTES, NONTENANT_VIEW_NAME); - - WAL.Entry nonTenantEntry = getEntry(systemCatalogTableName, nonTenantGet); - WAL.Entry tenantEntry = getEntry(systemCatalogTableName, tenantGet); + WAL.Entry nonTenantEntryCatalog = getEntry(systemCatalogTableName, nonTenantGetCatalog); + WAL.Entry tenantEntryCatalog = getEntry(systemCatalogTableName, tenantGetCatalog); + int tenantRowCount = getAndAssertTenantCountInEdit(tenantEntryCatalog); + Assert.assertTrue(tenantRowCount > 0); //verify that the tenant view WAL.Entry passes the filter and the non-tenant view does not SystemCatalogWALEntryFilter filter = new SystemCatalogWALEntryFilter(); // Chain the system catalog WAL entry filter to ChainWALEntryFilter ChainWALEntryFilter chainWALEntryFilter = new ChainWALEntryFilter(filter); // Asserting the WALEdit for non tenant has cells before getting filtered - Assert.assertTrue(nonTenantEntry.getEdit().size() > 0); + Assert.assertTrue(nonTenantEntryCatalog.getEdit().size() > 0); // All the cells will get removed by the filter since they do not belong to tenant Assert.assertTrue("Non tenant edits for system catalog should not get filtered", - chainWALEntryFilter.filter(nonTenantEntry).getEdit().isEmpty()); - WAL.Entry filteredTenantEntry = chainWALEntryFilter.filter(tenantEntry); - Assert.assertNotNull("Tenant view was filtered when it shouldn't be!", filteredTenantEntry); - Assert.assertEquals("filtered entry is not correct", - tenantEntry.getEdit().size(), filteredTenantEntry.getEdit().size()); + chainWALEntryFilter.filter(nonTenantEntryCatalog).getEdit().isEmpty()); + + WAL.Entry filteredTenantEntryCatalog = chainWALEntryFilter.filter(tenantEntryCatalog); + Assert.assertNotNull("Tenant view was filtered when it shouldn't be!", + filteredTenantEntryCatalog); + Assert.assertEquals("Not all data for replicated for tenant", tenantRowCount, + getAndAssertTenantCountInEdit(filteredTenantEntryCatalog)); //now check that a WAL.Entry with cells from both a tenant and a non-tenant //catalog row only allow the tenant cells through WALEdit comboEdit = new WALEdit(); - comboEdit.getCells().addAll(nonTenantEntry.getEdit().getCells()); - comboEdit.getCells().addAll(tenantEntry.getEdit().getCells()); - WAL.Entry comboEntry = new WAL.Entry(walKey, comboEdit); + nonTenantEntryCatalog = getEntry(systemCatalogTableName, nonTenantGetCatalog); + tenantEntryCatalog = getEntry(systemCatalogTableName, tenantGetCatalog); + comboEdit.getCells().addAll(nonTenantEntryCatalog.getEdit().getCells()); + comboEdit.getCells().addAll(tenantEntryCatalog.getEdit().getCells()); + WAL.Entry comboEntry = new WAL.Entry(walKeyCatalog, comboEdit); - Assert.assertEquals(tenantEntry.getEdit().size() + nonTenantEntry.getEdit().size() + Assert.assertEquals(tenantEntryCatalog.getEdit().size() + nonTenantEntryCatalog.getEdit().size() , comboEntry.getEdit().size()); - Assert.assertEquals(tenantEntry.getEdit().size(), + Assert.assertEquals(tenantEntryCatalog.getEdit().size(), chainWALEntryFilter.filter(comboEntry).getEdit().size()); } + @Test + public void testSystemChildLinkWALEntryFilter() throws Exception { + // now create WAL.Entry objects that refer to cells in those view rows in + // System.Child_Link + Get tenantGetChildLink = getGetChildLink(childLinkTable, TENANT_BYTES, TENANT_VIEW_NAME); + Get nonTenantGetChildLink = getGetChildLink(childLinkTable, DEFAULT_TENANT_BYTES, NONTENANT_VIEW_NAME); + + WAL.Entry tenantEntryChildLink = getEntry(systemChildLinkTableName, tenantGetChildLink); + WAL.Entry nonTenantEntryChildLink = getEntry(systemChildLinkTableName, nonTenantGetChildLink); + int tenantRowCount = getAndAssertTenantCountInEdit(tenantEntryChildLink); + Assert.assertTrue(tenantRowCount > 0); + + //verify that the tenant view WAL.Entry passes the filter and the non-tenant view does not + SystemCatalogWALEntryFilter filter = new SystemCatalogWALEntryFilter(); + // Chain the system catalog WAL entry filter to ChainWALEntryFilter + ChainWALEntryFilter chainWALEntryFilter = new ChainWALEntryFilter(filter); + // Asserting the WALEdit for non tenant has cells before getting filtered + Assert.assertTrue(nonTenantEntryChildLink.getEdit().size() > 0); + // All the cells will get removed by the filter since they do not belong to tenant + Assert.assertTrue("Non tenant edits for system child link should not get filtered", + chainWALEntryFilter.filter(nonTenantEntryChildLink).getEdit().isEmpty()); + + WAL.Entry filteredTenantEntryChildLink = chainWALEntryFilter.filter(tenantEntryChildLink); + Assert.assertNotNull("Tenant view was filtered when it shouldn't be!", + filteredTenantEntryChildLink); + Assert.assertEquals("Not all data for replicated for tenant", tenantRowCount, + getAndAssertTenantCountInEdit(filteredTenantEntryChildLink)); + + //now check that a WAL.Entry with cells from both a tenant and a non-tenant + // child link row only allow the tenant cells through + WALEdit comboEdit = new WALEdit(); + comboEdit.getCells().addAll(nonTenantEntryChildLink.getEdit().getCells()); + comboEdit.getCells().addAll(tenantEntryChildLink.getEdit().getCells()); + WAL.Entry comboEntry = new WAL.Entry(walKeyChildLink, comboEdit); + + Assert.assertEquals(tenantEntryChildLink.getEdit().size() + nonTenantEntryChildLink.getEdit().size() + , comboEntry.getEdit().size()); + Assert.assertEquals(tenantEntryChildLink.getEdit().size(), + chainWALEntryFilter.filter(comboEntry).getEdit().size()); + } + public Get getGet(PTable catalogTable, byte[] tenantId, String viewName) { byte[][] tenantKeyParts = new byte[5][]; tenantKeyParts[0] = tenantId; @@ -174,6 +225,41 @@ public Get getGet(PTable catalogTable, byte[] tenantId, String viewName) { return new Get(key.copyBytes()); } + public Get getGetChildLink(PTable catalogTable, byte[] tenantId, String viewName) { + byte[][] tenantKeyParts = new byte[5][]; + tenantKeyParts[0] = ByteUtil.EMPTY_BYTE_ARRAY; + tenantKeyParts[1] = ByteUtil.EMPTY_BYTE_ARRAY; + tenantKeyParts[2] = Bytes.toBytes(TestUtil.ENTITY_HISTORY_TABLE_NAME); + tenantKeyParts[3] = tenantId; + tenantKeyParts[4] = Bytes.toBytes(SCHEMA_NAME + "." +viewName.toUpperCase());; + ImmutableBytesWritable key = new ImmutableBytesWritable(); + catalogTable.newKey(key, tenantKeyParts); + //the backing byte array of key might have extra space at the end. + // need to just slice "the good parts" which we do by calling copyBytes + return new Get(key.copyBytes()); + } + + private boolean isTenantOwnedCell(Cell cell, String tenantId) { + String row = Bytes.toString(cell.getRowArray(), cell.getRowOffset(), + cell.getRowLength()); + boolean isTenantIdLeading = row.startsWith(tenantId); + boolean isChildLinkForTenantId = row.contains(tenantId) + && CellUtil.matchingQualifier(cell, + PhoenixDatabaseMetaData.LINK_TYPE_BYTES); + return isTenantIdLeading || isChildLinkForTenantId; + } + + private int getAndAssertTenantCountInEdit(WAL.Entry entry) { + int count = 0; + for (Cell cell : entry.getEdit().getCells()) { + if (isTenantOwnedCell(cell, TENANT_ID)) { + count = count + 1; + } + } + Assert.assertTrue(count > 0); + return count; + } + public WAL.Entry getEntry(TableName tableName, Get get) throws IOException { WAL.Entry entry = null; try(Connection conn = ConnectionFactory.createConnection(getUtility().getConfiguration())){ @@ -182,12 +268,14 @@ public WAL.Entry getEntry(TableName tableName, Get get) throws IOException { WALEdit edit = new WALEdit(); if (result != null) { List cellList = result.listCells(); - Assert.assertNotNull("Didn't retrieve any cells from SYSTEM.CATALOG", cellList); + Assert.assertNotNull(String.format("Didn't retrieve any cells from table %s", + tableName.getNameAsString()), cellList); for (Cell c : cellList) { edit.add(c); } } - Assert.assertTrue("Didn't retrieve any cells from SYSTEM.CATALOG", edit.getCells().size() > 0); + Assert.assertTrue(String.format("Didn't retrieve any cells from table %s", + tableName.getNameAsString()), edit.getCells().size() > 0); WALKey key = new WALKey(REGION, tableName, 0, 0, uuid); entry = new WAL.Entry(key, edit); } diff --git a/phoenix-core/src/main/java/org/apache/phoenix/replication/SystemCatalogWALEntryFilter.java b/phoenix-core/src/main/java/org/apache/phoenix/replication/SystemCatalogWALEntryFilter.java index f6929f6d0fc..3f29e8ee456 100644 --- a/phoenix-core/src/main/java/org/apache/phoenix/replication/SystemCatalogWALEntryFilter.java +++ b/phoenix-core/src/main/java/org/apache/phoenix/replication/SystemCatalogWALEntryFilter.java @@ -18,17 +18,23 @@ package org.apache.phoenix.replication; import org.apache.hadoop.hbase.Cell; +import org.apache.hadoop.hbase.CellUtil; +import org.apache.hadoop.hbase.io.ImmutableBytesWritable; import org.apache.hadoop.hbase.replication.WALCellFilter; import org.apache.hadoop.hbase.replication.WALEntryFilter; import org.apache.hadoop.hbase.wal.WAL; +import org.apache.phoenix.jdbc.PhoenixDatabaseMetaData; import org.apache.phoenix.query.QueryConstants; +import org.apache.phoenix.schema.PTable; import org.apache.phoenix.util.SchemaUtil; +import static org.apache.phoenix.jdbc.PhoenixDatabaseMetaData.COLUMN_NAME_INDEX; /** - * Standard replication of the SYSTEM.CATALOG table can be dangerous because schemas - * may change between the source and target clusters at different times, in particular - * during cluster upgrades. However, tenant-owned data such as tenant-owned views need to - * be copied. This WALEntryFilter will only allow tenant-owned rows in SYSTEM.CATALOG to + * Standard replication of the SYSTEM.CATALOG and SYSTEM.CHILD_LINK table can + * be dangerous because schemas may change between the source and target + * clusters at different times, in particular during cluster upgrades. + * However, tenant-owned data such as tenant-owned views need to be copied. + * This WALEntryFilter will only allow tenant-owned rows in SYSTEM.CATALOG to * be replicated. Data from all other tables is automatically passed. */ public class SystemCatalogWALEntryFilter implements @@ -38,6 +44,9 @@ public class SystemCatalogWALEntryFilter implements * about cell filter for certain WALEdits. */ private boolean skipCellFilter; + private static final byte[] CHILD_TABLE_BYTES = + new byte[]{PTable.LinkType.CHILD_TABLE.getSerializedValue()}; + private final int NUM_COLUMNS_PRIMARY_KEY = 5; @Override public WAL.Entry filter(WAL.Entry entry) { @@ -45,11 +54,9 @@ public WAL.Entry filter(WAL.Entry entry) { // should not block anything // if the WAL.Entry's table isn't System.Catalog or System.Child_Link, // it auto-passes this filter - if (!SchemaUtil.isMetaTable(entry.getKey().getTablename().getName())){ - skipCellFilter = true; - } else { - skipCellFilter = false; - } + skipCellFilter = + !(SchemaUtil.isMetaTable(entry.getKey().getTablename().getName()) + || SchemaUtil.isChildLinkTable(entry.getKey().getTablename().getName())); return entry; } @@ -58,13 +65,58 @@ public Cell filterCell(final WAL.Entry entry, final Cell cell) { if (skipCellFilter) { return cell; } - return isTenantRowCell(cell) ? cell : null; + + if (SchemaUtil.isMetaTable(entry.getKey().getTablename().getName())) { + return doesKeyHaveLeadingSeparatorByte(cell) ? cell : null; + } else { + return isTenantRowCellSystemChildLink(cell) ? cell : null; + } } - private boolean isTenantRowCell(Cell cell) { - // rows in system.catalog that aren't tenant-owned + /** + * does the cell key have leading separator byte + * @param cell hbase cell + * @return true if the cell has leading separator byte + */ + private boolean doesKeyHaveLeadingSeparatorByte(final Cell cell) { + // rows in system.catalog or system child that aren't tenant-owned // will have a leading separator byte return cell.getRowArray()[cell.getRowOffset()] - != QueryConstants.SEPARATOR_BYTE; + != QueryConstants.SEPARATOR_BYTE; + } + + /** + * is the cell for system child link a tenant owned. Besides the non empty + * tenant id, system.child_link table have tenant owned data for parent child + * links. In this case, the column qualifier is + * {@code PhoenixDatabaseMetaData#LINK_TYPE_BYTES} and value is + * {@code PTable.LinkType.CHILD_TABLE}. + * @param cell hbase cell + * @return true if the cell is tenant owned + */ + private boolean isTenantRowCellSystemChildLink(final Cell cell) { + boolean isTenantRowCell = doesKeyHaveLeadingSeparatorByte(cell); + + ImmutableBytesWritable key = new ImmutableBytesWritable( + cell.getRowArray(), cell.getRowOffset(), cell.getRowLength()); + boolean isChildLinkToTenantView = false; + if (!isTenantRowCell) { + boolean isChildLink = CellUtil.matchingQualifier( + cell, PhoenixDatabaseMetaData.LINK_TYPE_BYTES); + if (isChildLink) { + if (CellUtil.matchingValue(cell, CHILD_TABLE_BYTES)) { + byte[][] rowViewKeyMetadata = new byte[NUM_COLUMNS_PRIMARY_KEY][]; + SchemaUtil.getVarChars(key.get(), key.getOffset(), + key.getLength(), 0, rowViewKeyMetadata); + // if the child link is to a tenant-owned view, + // the COLUMN_NAME field will be the byte[] of the tenant + //otherwise, it will be an empty byte array + // (NOT QueryConstants.SEPARATOR_BYTE, but a byte[0]) + isChildLinkToTenantView = + rowViewKeyMetadata[COLUMN_NAME_INDEX].length != 0; + } + } + } + return isTenantRowCell || isChildLinkToTenantView; } }