This is an automated email from the ASF dual-hosted git repository.
virajjasani pushed a commit to branch 5.3
in repository https://gitbox.apache.org/repos/asf/phoenix.git
The following commit(s) were added to refs/heads/5.3 by this push:
new 048ab25f46 PHOENIX-8007 Perf bottleneck - Server side Index metadata
lookup overhead (#2625)
048ab25f46 is described below
commit 048ab25f462712ef6fb90474387e9ad10a1abe3c
Author: Viraj Jasani <[email protected]>
AuthorDate: Sun Oct 4 17:20:27 2026 -0700
PHOENIX-8007 Perf bottleneck - Server side Index metadata lookup overhead
(#2625)
---
.../phoenix/index/PhoenixIndexMetaDataBuilder.java | 26 ++-
.../phoenix/end2end/UCFWithServerMetadataIT.java | 256 +++++++++++++++++++++
2 files changed, 281 insertions(+), 1 deletion(-)
diff --git
a/phoenix-core-server/src/main/java/org/apache/phoenix/index/PhoenixIndexMetaDataBuilder.java
b/phoenix-core-server/src/main/java/org/apache/phoenix/index/PhoenixIndexMetaDataBuilder.java
index 3d38a5a394..ad3d1b611e 100644
---
a/phoenix-core-server/src/main/java/org/apache/phoenix/index/PhoenixIndexMetaDataBuilder.java
+++
b/phoenix-core-server/src/main/java/org/apache/phoenix/index/PhoenixIndexMetaDataBuilder.java
@@ -19,6 +19,7 @@ package org.apache.phoenix.index;
import java.io.IOException;
import java.sql.Connection;
+import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
@@ -53,6 +54,12 @@ public class PhoenixIndexMetaDataBuilder {
private static final Logger LOGGER =
LoggerFactory.getLogger(PhoenixIndexMetaDataBuilder.class);
+ /**
+ * Lazily-computed, cached JDBC URL used to open server-side connections for
index metadata
+ * lookups.
+ */
+ private static volatile String serverConnectionUrl;
+
private final RegionCoprocessorEnvironment env;
PhoenixIndexMetaDataBuilder(RegionCoprocessorEnvironment env) {
@@ -154,7 +161,8 @@ public class PhoenixIndexMetaDataBuilder {
if (tenantId != null) {
props.setProperty(PhoenixRuntime.TENANT_ID_ATTRIB, tenantId);
}
- try (Connection conn = QueryUtil.getConnectionOnServer(props,
env.getConfiguration())) {
+ QueryUtil.setServerConnection(props);
+ try (Connection conn =
DriverManager.getConnection(getServerConnectionUrl(env), props)) {
PhoenixConnection pconn = conn.unwrap(PhoenixConnection.class);
PTable dataTable = pconn.getTable(tenantId, fullTableName);
final List<IndexMaintainer> indexMaintainers =
@@ -213,4 +221,20 @@ public class PhoenixIndexMetaDataBuilder {
}
return indexMaintainers;
}
+
+ private static String getServerConnectionUrl(RegionCoprocessorEnvironment
env)
+ throws SQLException {
+ String url = serverConnectionUrl;
+ if (url == null) {
+ synchronized (PhoenixIndexMetaDataBuilder.class) {
+ url = serverConnectionUrl;
+ if (url == null) {
+ url = QueryUtil.getConnectionUrl(new Properties(),
env.getConfiguration());
+ LOGGER.info("Cached server-side JDBC url for index metadata lookups:
{}", url);
+ serverConnectionUrl = url;
+ }
+ }
+ }
+ return url;
+ }
}
diff --git
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/UCFWithServerMetadataIT.java
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/UCFWithServerMetadataIT.java
index fe29929456..62e6844931 100644
---
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/UCFWithServerMetadataIT.java
+++
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/UCFWithServerMetadataIT.java
@@ -19,12 +19,16 @@ package org.apache.phoenix.end2end;
import static
org.apache.phoenix.hbase.index.IndexRegionObserver.PHOENIX_INDEX_CDC_CONSUMER_ENABLED;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
import com.google.protobuf.RpcCallback;
import com.google.protobuf.RpcController;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
@@ -37,8 +41,11 @@ import
org.apache.phoenix.coprocessor.generated.MetaDataProtos.GetTableRequest;
import
org.apache.phoenix.coprocessor.generated.MetaDataProtos.MetaDataResponse;
import
org.apache.phoenix.coprocessor.generated.ServerCachingProtos.AddServerCacheRequest;
import
org.apache.phoenix.coprocessor.generated.ServerCachingProtos.AddServerCacheResponse;
+import org.apache.phoenix.jdbc.PhoenixPreparedStatement;
import org.apache.phoenix.query.BaseTest;
import org.apache.phoenix.query.QueryServices;
+import org.apache.phoenix.util.MetaDataUtil;
+import org.apache.phoenix.util.PhoenixRuntime;
import org.apache.phoenix.util.PropertiesUtil;
import org.apache.phoenix.util.ReadOnlyProps;
import org.apache.phoenix.util.TestUtil;
@@ -205,6 +212,255 @@ public class UCFWithServerMetadataIT extends BaseTest {
}
}
+ @Test
+ public void testTenantViewIndexServerMetadata() throws Exception {
+ String baseTableName = generateUniqueName();
+ String tenant1 = generateUniqueName();
+ String tenant2 = generateUniqueName();
+ String viewName1 = "V1_" + generateUniqueName();
+ String viewName2 = "V2_" + generateUniqueName();
+ String coveredIndex1 = "TCI1_" + generateUniqueName();
+ String uncoveredIndex1 = "TUI1_" + generateUniqueName();
+ String coveredIndex2 = "TCI2_" + generateUniqueName();
+ String viewIndexPhysical = MetaDataUtil.VIEW_INDEX_TABLE_PREFIX +
baseTableName;
+
+ Properties props = PropertiesUtil.deepCopy(TestUtil.TEST_PROPERTIES);
+ props.setProperty(QueryServices.INDEX_USE_SERVER_METADATA_ATTRIB,
+ Boolean.toString(useServerMetadata));
+ try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
+ createMultiTenantBaseTable(conn, baseTableName);
+ attachCustomCoprocessor(conn, baseTableName);
+ }
+
+ try (Connection tenant1Conn = getTenantConnection(tenant1)) {
+ tenant1Conn.createStatement()
+ .execute(String.format("CREATE VIEW %s AS SELECT * FROM %s",
viewName1, baseTableName));
+ tenant1Conn.createStatement().execute(String
+ .format("CREATE INDEX %s ON %s (name) INCLUDE (age, city)",
coveredIndex1, viewName1));
+ tenant1Conn.createStatement()
+ .execute(String.format("CREATE INDEX %s ON %s (salary)",
uncoveredIndex1, viewName1));
+
+ PreparedStatement upsert = tenant1Conn.prepareStatement(String.format(
+ "UPSERT INTO %s (id, name, age, city, salary, department) VALUES (?,
?, ?, ?, ?, ?)",
+ viewName1));
+
+ // Warm-up (row id=1) primes both the client and the server PTable
caches so that, under a
+ // large UPDATE_CACHE_FREQUENCY, the measured window observes no further
getTable() RPCs.
+ upsertTenantViewRow(upsert, 1);
+ tenant1Conn.commit();
+ assertCoveredIndexRow(tenant1Conn, viewName1, viewIndexPhysical, 1);
+ assertUncoveredIndexRow(tenant1Conn, viewName1, viewIndexPhysical, 1);
+
+ long startGetTableCalls = getTableCallCount.get();
+ long startAddServerCacheCalls = addServerCacheCallCount.get();
+
+ // Measured window: upsert rows 2..6 through the tenant connection,
single-row or batched
+ // consistent with the singleRowUpdate dimension (a batch of 5 exceeds
the mutate-batch
+ // threshold so the without-server-metadata batch case uses
addServerCache()).
+ for (int i = 2; i <= 6; i++) {
+ upsertTenantViewRow(upsert, i);
+ if (singleRowUpdate) {
+ tenant1Conn.commit();
+ }
+ }
+ if (!singleRowUpdate) {
+ tenant1Conn.commit();
+ }
+ // Correctness: each view index must return exactly the right
rows/columns for this tenant.
+ assertCoveredIndexRow(tenant1Conn, viewName1, viewIndexPhysical, 3);
+ assertUncoveredIndexRow(tenant1Conn, viewName1, viewIndexPhysical, 5);
+
+ long getTableDelta = getTableCallCount.get() - startGetTableCalls;
+ long addServerCacheDelta = addServerCacheCallCount.get() -
startAddServerCacheCalls;
+
+ // With server metadata, index writes must never trigger a client
addServerCache() RPC. This
+ // is the counting assertion the production change (global vs per-tenant
connection) could
+ // regress. Without server metadata, only batched writes above the
threshold use it.
+ if (useServerMetadata) {
+ assertEquals("Tenant view index writes must not use addServerCache()
with server metadata",
+ 0L, addServerCacheDelta);
+ } else if (singleRowUpdate) {
+ assertEquals("Single-row writes stay below the mutate-batch
threshold", 0L,
+ addServerCacheDelta);
+ } else {
+ assertTrue("Batched writes without server metadata should use
addServerCache()",
+ addServerCacheDelta >= 1);
+ }
+
+ // getTable() behavior on the tenant-view path. The server-metadata path
now resolves tenant
+ // views/indexes with a per-tenant-scoped reusable server connection, so
metadata resolution
+ // honors UPDATE_CACHE_FREQUENCY exactly as it does for non-tenant
tables. With UCF=ALWAYS
+ // every batch re-resolves the tenant view (on the client for the
non-server path, on the
+ // RegionServer for the server-metadata path), so the count grows. With
a large UCF, once the
+ // warm-up row has populated the shared server/client PTable caches, the
measured window
+ // re-resolves nothing and issues no getTable() RPCs. Exact counts under
ALWAYS depend on the
+ // number of batches, so we assert the robust direction invariant.
+ if ("ALWAYS".equals(updateCacheFrequency)) {
+ assertTrue("With UCF=ALWAYS the tenant view path must re-resolve
metadata (getTable grows)",
+ getTableDelta > 0);
+ } else {
+ assertEquals("With a large UCF the warm tenant view path must not
issue getTable() RPCs",
+ 0L, getTableDelta);
+ }
+
+ // Tenant isolation: a second tenant's view on the same base table with
a same-named row must
+ // not leak across the shared view-index physical table.
+ try (Connection tenant2Conn = getTenantConnection(tenant2)) {
+ tenant2Conn.createStatement()
+ .execute(String.format("CREATE VIEW %s AS SELECT * FROM %s",
viewName2, baseTableName));
+ tenant2Conn.createStatement().execute(String
+ .format("CREATE INDEX %s ON %s (name) INCLUDE (age, city)",
coveredIndex2, viewName2));
+ PreparedStatement upsert2 = tenant2Conn.prepareStatement(String.format(
+ "UPSERT INTO %s (id, name, age, city, salary, department) VALUES (?,
?, ?, ?, ?, ?)",
+ viewName2));
+ // Same id/name as tenant1's row id=3 but a distinct age/city.
+ upsert2.setInt(1, 3);
+ upsert2.setString(2, "Name3");
+ upsert2.setInt(3, 999);
+ upsert2.setString(4, "CityZ");
+ upsert2.setInt(5, 999000);
+ upsert2.setString(6, "DeptZ");
+ upsert2.executeUpdate();
+ tenant2Conn.commit();
+
+ try (ResultSet rs = tenant2Conn.createStatement().executeQuery(
+ String.format("SELECT age, city FROM %s WHERE name = 'Name3'",
viewName2))) {
+ assertTrue(rs.next());
+ assertEquals(999, rs.getInt(1));
+ assertEquals("CityZ", rs.getString(2));
+ assertFalse("tenant2 covered index must return exactly its own row",
rs.next());
+ }
+ }
+
+ // tenant1's covered-index query for the same name must still return
only tenant1's own data.
+ try (ResultSet rs = tenant1Conn.createStatement()
+ .executeQuery(String.format("SELECT age, city FROM %s WHERE name =
'Name3'", viewName1))) {
+ assertTrue(rs.next());
+ assertEquals("tenant1 must not see tenant2's index entries", 23,
rs.getInt(1));
+ assertEquals("City3", rs.getString(2));
+ assertFalse("tenant1 covered index must return exactly its own row",
rs.next());
+ }
+ }
+ }
+
+ @Test
+ public void testPartialIndexOnTenantView() throws Exception {
+ String baseTableName = generateUniqueName();
+ String tenant = generateUniqueName();
+ String viewName = "PV_" + generateUniqueName();
+ String partialIndex = "PI_" + generateUniqueName();
+ String viewIndexPhysical = MetaDataUtil.VIEW_INDEX_TABLE_PREFIX +
baseTableName;
+
+ Properties props = PropertiesUtil.deepCopy(TestUtil.TEST_PROPERTIES);
+ props.setProperty(QueryServices.INDEX_USE_SERVER_METADATA_ATTRIB,
+ Boolean.toString(useServerMetadata));
+ try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
+ createMultiTenantBaseTable(conn, baseTableName);
+ }
+
+ try (Connection tenantConn = getTenantConnection(tenant)) {
+ tenantConn.createStatement()
+ .execute(String.format("CREATE VIEW %s AS SELECT * FROM %s", viewName,
baseTableName));
+ // Partial index on the leading key column (age), covering city, for
rows with age >= 50.
+ tenantConn.createStatement().execute(String.format(
+ "CREATE INDEX %s ON %s (age) INCLUDE (city) WHERE age >= 50",
partialIndex, viewName));
+
+ PreparedStatement upsert = tenantConn.prepareStatement(String.format(
+ "UPSERT INTO %s (id, name, age, city, salary, department) VALUES (?,
?, ?, ?, ?, ?)",
+ viewName));
+ // Rows id=1..6 with age=10*id: ids 5 and 6 match the predicate (age 50,
60); ids 1..4 do
+ // not. Maintenance of the partial index runs on the server-metadata
path for these upserts.
+ for (int id = 1; id <= 6; id++) {
+ upsert.setInt(1, id);
+ upsert.setString(2, "Name" + id);
+ upsert.setInt(3, 10 * id);
+ upsert.setString(4, "City" + id);
+ upsert.setInt(5, 1000 * id);
+ upsert.setString(6, "Dept" + (id % 5));
+ upsert.executeUpdate();
+ if (singleRowUpdate) {
+ tenantConn.commit();
+ }
+ }
+ if (!singleRowUpdate) {
+ tenantConn.commit();
+ }
+
+ String query =
+ String.format("SELECT age, city FROM %s WHERE age >= 50 ORDER BY age",
viewName);
+ assertEquals("Partial view index should be used", viewIndexPhysical,
+
tenantConn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class).optimizeQuery()
+ .getExplainPlan().getPlanStepsAsAttributes().getTableName());
+ try (ResultSet rs = tenantConn.createStatement().executeQuery(query)) {
+ assertTrue(rs.next());
+ assertEquals(50, rs.getInt(1));
+ assertEquals("City5", rs.getString(2));
+ assertTrue(rs.next());
+ assertEquals(60, rs.getInt(1));
+ assertEquals("City6", rs.getString(2));
+ assertFalse("Partial index must contain only predicate-matching rows",
rs.next());
+ }
+ }
+ }
+
+ private void createMultiTenantBaseTable(Connection conn, String
baseTableName)
+ throws SQLException {
+ conn.createStatement().execute(String.format(
+ "CREATE TABLE %s (tenant_id VARCHAR NOT NULL, id INTEGER NOT NULL, name
VARCHAR(50), "
+ + "age INTEGER, city VARCHAR(50), salary INTEGER, department
VARCHAR(50) "
+ + "CONSTRAINT pk PRIMARY KEY (tenant_id, id)) MULTI_TENANT=true,
UPDATE_CACHE_FREQUENCY=%s",
+ baseTableName, updateCacheFrequency));
+ }
+
+ private Connection getTenantConnection(String tenantId) throws SQLException {
+ Properties props = PropertiesUtil.deepCopy(TestUtil.TEST_PROPERTIES);
+ props.setProperty(PhoenixRuntime.TENANT_ID_ATTRIB, tenantId);
+ props.setProperty(QueryServices.INDEX_USE_SERVER_METADATA_ATTRIB,
+ Boolean.toString(useServerMetadata));
+ return DriverManager.getConnection(getUrl(), props);
+ }
+
+ private static void upsertTenantViewRow(PreparedStatement upsert, int id)
throws SQLException {
+ upsert.setInt(1, id);
+ upsert.setString(2, "Name" + id);
+ upsert.setInt(3, 20 + id);
+ upsert.setString(4, "City" + id);
+ upsert.setInt(5, 1000 * id);
+ upsert.setString(6, "Dept" + (id % 5));
+ upsert.executeUpdate();
+ }
+
+ private void assertCoveredIndexRow(Connection conn, String viewName, String
viewIndexPhysical,
+ int id) throws SQLException {
+ String query =
+ String.format("SELECT name, age, city FROM %s WHERE name = 'Name%d'",
viewName, id);
+ assertEquals("Covered view index should be used", viewIndexPhysical,
+
conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class).optimizeQuery()
+ .getExplainPlan().getPlanStepsAsAttributes().getTableName());
+ try (ResultSet rs = conn.createStatement().executeQuery(query)) {
+ assertTrue(rs.next());
+ assertEquals("Name" + id, rs.getString(1));
+ assertEquals(20 + id, rs.getInt(2));
+ assertEquals("City" + id, rs.getString(3));
+ assertFalse("Covered index query must return exactly one row",
rs.next());
+ }
+ }
+
+ private void assertUncoveredIndexRow(Connection conn, String viewName,
String viewIndexPhysical,
+ int id) throws SQLException {
+ String query =
+ String.format("SELECT id, salary FROM %s WHERE salary = %d", viewName,
1000 * id);
+ assertEquals("Uncovered view index should be used", viewIndexPhysical,
+
conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class).optimizeQuery()
+ .getExplainPlan().getPlanStepsAsAttributes().getTableName());
+ try (ResultSet rs = conn.createStatement().executeQuery(query)) {
+ assertTrue(rs.next());
+ assertEquals(id, rs.getInt(1));
+ assertEquals(1000 * id, rs.getInt(2));
+ assertFalse("Uncovered index query must return exactly one row",
rs.next());
+ }
+ }
+
private void attachCustomCoprocessor(Connection conn, String dataTableName)
throws Exception {
TestUtil.removeCoprocessor(conn, "SYSTEM.CATALOG",
MetaDataEndpointImpl.class);
TestUtil.addCoprocessor(conn, "SYSTEM.CATALOG",
TrackingMetaDataEndpointImpl.class);