This is an automated email from the ASF dual-hosted git repository.

tkhurana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/phoenix.git


The following commit(s) were added to refs/heads/master by this push:
     new 72865c53f4 PHOENIX-8023 SYSTEM.CATALOG close stalls on TTL lookup 
self-scan (#2639)
72865c53f4 is described below

commit 72865c53f4db961177b0767174dc9ab30ca4c947
Author: tkhurana <[email protected]>
AuthorDate: Wed Oct 21 07:50:40 2026 -0700

    PHOENIX-8023 SYSTEM.CATALOG close stalls on TTL lookup self-scan (#2639)
---
 .../org/apache/phoenix/query/QueryServices.java    |   5 +
 .../apache/phoenix/query/QueryServicesOptions.java |   4 +
 .../phoenix/coprocessor/MetaDataEndpointImpl.java  | 194 ++++++++----
 .../java/org/apache/phoenix/util/ServerUtil.java   |  60 ++++
 .../phoenix/end2end/SystemCatalogTTLLookupIT.java  | 351 +++++++++++++++++++++
 .../end2end/SystemCatalogTTLLookupSplitIT.java     | 167 ++++++++++
 .../org/apache/phoenix/util/ServerUtilTest.java    | 117 +++++++
 7 files changed, 828 insertions(+), 70 deletions(-)

diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/query/QueryServices.java 
b/phoenix-core-client/src/main/java/org/apache/phoenix/query/QueryServices.java
index 085ac34a64..01640f5203 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/query/QueryServices.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/query/QueryServices.java
@@ -228,6 +228,11 @@ public interface QueryServices extends SQLCloseable {
   public static final String METADATA_WRITE_RETRIES_NUMBER = 
"phoenix.metadata.rpc.retries.number";
   public static final String METADATA_WRITE_RETRY_PAUSE = 
"phoenix.metadata.rpc.pause";
 
+  // Timeout (also bounding retries) for server side client scans of 
SYSTEM.CATALOG issued while
+  // building a PTable
+  public static final String METADATA_SERVER_LOOKUP_TIMEOUT_MS =
+    "phoenix.metadata.server.lookup.timeout.ms";
+
   // Config parameters for for configuring tracing
   public static final String TRACING_FREQ_ATTRIB = "phoenix.trace.frequency";
   public static final String TRACING_PAGE_SIZE_ATTRIB = 
"phoenix.trace.read.pagesize";
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/query/QueryServicesOptions.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/query/QueryServicesOptions.java
index 4e3c29b6c3..4b20fb9da5 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/query/QueryServicesOptions.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/query/QueryServicesOptions.java
@@ -281,6 +281,10 @@ public class QueryServicesOptions {
   public static final int DEFAULT_METADATA_WRITE_RETRIES_NUMBER = 20;
   public static final int DEFAULT_METADATA_WRITE_RETRY_PAUSE = 100;
 
+  // Kept short because these reads run while holding a SYSTEM.CATALOG region 
operation, at the
+  // cost of failing the lookup sooner against a slow region server
+  public static final int DEFAULT_METADATA_SERVER_LOOKUP_TIMEOUT_MS = 10000;
+
   public static final int DEFAULT_TRACING_PAGE_SIZE = 100;
   /**
    * Configuration key to overwrite the tablename that should be used as the 
target table
diff --git 
a/phoenix-core-server/src/main/java/org/apache/phoenix/coprocessor/MetaDataEndpointImpl.java
 
b/phoenix-core-server/src/main/java/org/apache/phoenix/coprocessor/MetaDataEndpointImpl.java
index 053976b586..966fce460b 100644
--- 
a/phoenix-core-server/src/main/java/org/apache/phoenix/coprocessor/MetaDataEndpointImpl.java
+++ 
b/phoenix-core-server/src/main/java/org/apache/phoenix/coprocessor/MetaDataEndpointImpl.java
@@ -154,6 +154,7 @@ import org.apache.hadoop.hbase.client.Result;
 import org.apache.hadoop.hbase.client.ResultScanner;
 import org.apache.hadoop.hbase.client.Scan;
 import org.apache.hadoop.hbase.client.Table;
+import org.apache.hadoop.hbase.client.metrics.ScanMetrics;
 import org.apache.hadoop.hbase.coprocessor.CoprocessorException;
 import org.apache.hadoop.hbase.coprocessor.CoreCoprocessor;
 import org.apache.hadoop.hbase.coprocessor.RegionCoprocessor;
@@ -1847,56 +1848,54 @@ public class MetaDataEndpointImpl extends 
MetaDataProtocol implements RegionCopr
   private TTLExpression getTTLFromHierarchy(byte[] viewKey, long 
clientTimeStamp,
     boolean checkForMappedView) throws IOException, SQLException {
     Scan scan = MetaDataUtil.newTableRowsScan(viewKey, MIN_TABLE_TIMESTAMP, 
clientTimeStamp);
-    Table sysCat = ServerUtil.getHTableForCoprocessorScan(this.env,
-      SchemaUtil.getPhysicalTableName(SYSTEM_CATALOG_NAME_BYTES, 
env.getConfiguration()));
-    ResultScanner scanner = sysCat.getScanner(scan);
-    Result result = scanner.next();
-
     byte[] tableKey = null;
-    do {
-
-      if (result == null) {
-        return TTL_EXPRESSION_NOT_DEFINED;
-      }
+    try (ResultScanner scanner = getSysCatScanner(viewKey, scan)) {
+      Result result = scanner.next();
+      do {
 
-      // return TTL_NOT_DEFINED for Index on a Mapped View.
-      if (checkForMappedView && checkIfViewIsMappedView(result)) {
-        return TTL_EXPRESSION_NOT_DEFINED;
-      }
+        if (result == null) {
+          return TTL_EXPRESSION_NOT_DEFINED;
+        }
 
-      byte[] linkTypeBytes = result.getValue(TABLE_FAMILY_BYTES, 
LINK_TYPE_BYTES);
-      byte[][] rowKeyMetaData = new byte[5][];
-      getVarChars(result.getRow(), 5, rowKeyMetaData);
-      // Check if TTL is defined at the current given level
-      if (result.getValue(TABLE_FAMILY_BYTES, TTL_BYTES) != null) {
-        String ttlStr = (String) PVarchar.INSTANCE
-          .toObject(result.getValue(DEFAULT_COLUMN_FAMILY_BYTES, TTL_BYTES));
-        return TTLExpressionFactory.create(ttlStr);
-      } else if (linkTypeBytes != null) {
-        String parentSchema = SchemaUtil
-          
.getSchemaNameFromFullName(rowKeyMetaData[PhoenixDatabaseMetaData.FAMILY_NAME_INDEX]);
-        byte[] parentViewSchemaName =
-          parentSchema != null ? parentSchema.getBytes(StandardCharsets.UTF_8) 
: null;
-        byte[] parentViewName = SchemaUtil
-          
.getTableNameFromFullName(rowKeyMetaData[PhoenixDatabaseMetaData.FAMILY_NAME_INDEX])
-          .getBytes(StandardCharsets.UTF_8);
-        // Get TTL from up the hierarchy, Checking for Parent view link and 
getting TTL from it.
-        if (LinkType.fromSerializedValue(linkTypeBytes[0]) == PARENT_TABLE) {
-          byte[] parentViewTenantId = result.getValue(TABLE_FAMILY_BYTES, 
PARENT_TENANT_ID_BYTES);
-          byte[] parentViewKey =
-            SchemaUtil.getTableKey(parentViewTenantId, parentViewSchemaName, 
parentViewName);
-          return getTTLFromHierarchy(parentViewKey, clientTimeStamp, false);
+        // return TTL_NOT_DEFINED for Index on a Mapped View.
+        if (checkForMappedView && checkIfViewIsMappedView(result)) {
+          return TTL_EXPRESSION_NOT_DEFINED;
         }
 
-        // Store tableKey to use if we don't find TTL at current level and from
-        // parent views above the hierarchy
-        if (LinkType.fromSerializedValue(linkTypeBytes[0]) == PHYSICAL_TABLE) {
-          tableKey = SchemaUtil.getTableKey(null, parentViewSchemaName, 
parentViewName);
+        byte[] linkTypeBytes = result.getValue(TABLE_FAMILY_BYTES, 
LINK_TYPE_BYTES);
+        byte[][] rowKeyMetaData = new byte[5][];
+        getVarChars(result.getRow(), 5, rowKeyMetaData);
+        // Check if TTL is defined at the current given level
+        if (result.getValue(TABLE_FAMILY_BYTES, TTL_BYTES) != null) {
+          String ttlStr = (String) PVarchar.INSTANCE
+            .toObject(result.getValue(DEFAULT_COLUMN_FAMILY_BYTES, TTL_BYTES));
+          return TTLExpressionFactory.create(ttlStr);
+        } else if (linkTypeBytes != null) {
+          String parentSchema = SchemaUtil
+            
.getSchemaNameFromFullName(rowKeyMetaData[PhoenixDatabaseMetaData.FAMILY_NAME_INDEX]);
+          byte[] parentViewSchemaName =
+            parentSchema != null ? 
parentSchema.getBytes(StandardCharsets.UTF_8) : null;
+          byte[] parentViewName = SchemaUtil
+            
.getTableNameFromFullName(rowKeyMetaData[PhoenixDatabaseMetaData.FAMILY_NAME_INDEX])
+            .getBytes(StandardCharsets.UTF_8);
+          // Get TTL from up the hierarchy, Checking for Parent view link and 
getting TTL from it.
+          if (LinkType.fromSerializedValue(linkTypeBytes[0]) == PARENT_TABLE) {
+            byte[] parentViewTenantId = result.getValue(TABLE_FAMILY_BYTES, 
PARENT_TENANT_ID_BYTES);
+            byte[] parentViewKey =
+              SchemaUtil.getTableKey(parentViewTenantId, parentViewSchemaName, 
parentViewName);
+            return getTTLFromHierarchy(parentViewKey, clientTimeStamp, false);
+          }
+
+          // Store tableKey to use if we don't find TTL at current level and 
from
+          // parent views above the hierarchy
+          if (LinkType.fromSerializedValue(linkTypeBytes[0]) == 
PHYSICAL_TABLE) {
+            tableKey = SchemaUtil.getTableKey(null, parentViewSchemaName, 
parentViewName);
+          }
         }
-      }
 
-      result = scanner.next();
-    } while (result != null);
+        result = scanner.next();
+      } while (result != null);
+    }
 
     // Return TTL defined at Table level for the given hierarchy as we didn't 
find TTL any of the
     // views.
@@ -1925,39 +1924,94 @@ public class MetaDataEndpointImpl extends 
MetaDataProtocol implements RegionCopr
   private TTLExpression getTTLForParentTable(byte[] tableKey, long 
clientTimeStamp,
     String indexName, IndexType indexType) throws IOException {
     Scan scan = MetaDataUtil.newTableRowsScan(tableKey, MIN_TABLE_TIMESTAMP, 
clientTimeStamp);
-    Table sysCat = ServerUtil.getHTableForCoprocessorScan(this.env,
-      SchemaUtil.getPhysicalTableName(SYSTEM_CATALOG_NAME_BYTES, 
env.getConfiguration()));
-    ResultScanner scanner = sysCat.getScanner(scan);
-    Result result = scanner.next();
-    do {
-      if (result == null) {
-        return TTL_EXPRESSION_NOT_DEFINED;
-      }
-      if (result.getValue(TABLE_FAMILY_BYTES, TTL_BYTES) != null) {
-        String ttlStr = (String) PVarchar.INSTANCE
-          .toObject(result.getValue(DEFAULT_COLUMN_FAMILY_BYTES, TTL_BYTES));
-        TTLExpression ttl = TTLExpressionFactory.create(ttlStr);
-        if (UNCOVERED_GLOBAL.equals(indexType)) {
-          if (CDCUtil.isCDCIndex(indexName)) {
-            if (!ttl.equals(TTL_EXPRESSION_NOT_DEFINED)) {
-              return TTL_EXPRESSION_FOREVER;
-            }
-          } else if (ttl instanceof ConditionalTTLExpression) {
-            byte[] isStrictTTLBytes = result.getValue(TABLE_FAMILY_BYTES, 
IS_STRICT_TTL_BYTES);
-            boolean isStrictTTL = isStrictTTLBytes != null
-              && 
Boolean.TRUE.equals(PBoolean.INSTANCE.toObject(isStrictTTLBytes));
-            if (!isStrictTTL) {
-              return TTL_EXPRESSION_NOT_DEFINED;
+    try (ResultScanner scanner = getSysCatScanner(tableKey, scan)) {
+      Result result = scanner.next();
+      do {
+        if (result == null) {
+          return TTL_EXPRESSION_NOT_DEFINED;
+        }
+        if (result.getValue(TABLE_FAMILY_BYTES, TTL_BYTES) != null) {
+          String ttlStr = (String) PVarchar.INSTANCE
+            .toObject(result.getValue(DEFAULT_COLUMN_FAMILY_BYTES, TTL_BYTES));
+          TTLExpression ttl = TTLExpressionFactory.create(ttlStr);
+          if (UNCOVERED_GLOBAL.equals(indexType)) {
+            if (CDCUtil.isCDCIndex(indexName)) {
+              if (!ttl.equals(TTL_EXPRESSION_NOT_DEFINED)) {
+                return TTL_EXPRESSION_FOREVER;
+              }
+            } else if (ttl instanceof ConditionalTTLExpression) {
+              byte[] isStrictTTLBytes = result.getValue(TABLE_FAMILY_BYTES, 
IS_STRICT_TTL_BYTES);
+              boolean isStrictTTL = isStrictTTLBytes != null
+                && 
Boolean.TRUE.equals(PBoolean.INSTANCE.toObject(isStrictTTLBytes));
+              if (!isStrictTTL) {
+                return TTL_EXPRESSION_NOT_DEFINED;
+              }
             }
           }
+          return ttl;
         }
-        return ttl;
-      }
-      result = scanner.next();
-    } while (result != null);
+        result = scanner.next();
+      } while (result != null);
+    }
     return TTL_EXPRESSION_NOT_DEFINED;
   }
 
+  /**
+   * Opens a scanner over SYSTEM.CATALOG rows for the given table key. Reads 
the local region when
+   * it hosts the key, so that no client RPC is issued to a region whose 
operation the caller may
+   * hold (a closing region would otherwise stall in client retries). 
Otherwise falls back to a
+   * client scan with bounded retries and timeouts.
+   */
+  private ResultScanner getSysCatScanner(byte[] key, Scan scan) throws 
IOException {
+    Region region = env.getRegion();
+    if (region.getRegionInfo().containsRow(key)) {
+      return new RegionResultScanner(region.getScanner(scan));
+    }
+    try (Table sysCat = ServerUtil.ConnectionFactory
+      .getConnection(ServerUtil.ConnectionType.METADATA_LOOKUP_CONNECTION, 
env).getTable(
+        SchemaUtil.getPhysicalTableName(SYSTEM_CATALOG_NAME_BYTES, 
env.getConfiguration()))) {
+      return sysCat.getScanner(scan);
+    }
+  }
+
+  /** Adapts a {@link RegionScanner} to a {@link ResultScanner}, one row per 
{@link Result}. */
+  private static class RegionResultScanner implements ResultScanner {
+    private final RegionScanner scanner;
+    private boolean hasMore = true;
+
+    RegionResultScanner(RegionScanner scanner) {
+      this.scanner = scanner;
+    }
+
+    @Override
+    public Result next() throws IOException {
+      List<Cell> cells = new ArrayList<>();
+      while (hasMore && cells.isEmpty()) {
+        hasMore = scanner.next(cells);
+      }
+      return cells.isEmpty() ? null : Result.create(cells);
+    }
+
+    @Override
+    public void close() {
+      try {
+        scanner.close();
+      } catch (IOException e) {
+        LOGGER.warn("Failed to close SYSTEM.CATALOG region scanner", e);
+      }
+    }
+
+    @Override
+    public boolean renewLease() {
+      return false;
+    }
+
+    @Override
+    public ScanMetrics getScanMetrics() {
+      return null;
+    }
+  }
+
   private Long getViewIndexId(Cell[] tableKeyValues, PDataType 
viewIndexIdType) {
     Cell viewIndexIdKv = tableKeyValues[VIEW_INDEX_ID_INDEX];
     return viewIndexIdKv == null ? null : decodeViewIndexId(viewIndexIdKv, 
viewIndexIdType);
diff --git 
a/phoenix-core-server/src/main/java/org/apache/phoenix/util/ServerUtil.java 
b/phoenix-core-server/src/main/java/org/apache/phoenix/util/ServerUtil.java
index 4a409ac69e..6f5a3b1190 100644
--- a/phoenix-core-server/src/main/java/org/apache/phoenix/util/ServerUtil.java
+++ b/phoenix-core-server/src/main/java/org/apache/phoenix/util/ServerUtil.java
@@ -54,6 +54,13 @@ import org.slf4j.LoggerFactory;
 public class ServerUtil {
   private static final Logger LOGGER = 
LoggerFactory.getLogger(ServerUtil.class);
   private static final String FORMAT_FOR_TIMESTAMP = ",serverTimestamp=%d,";
+  // HBase client keys not available as constants in all supported HBase 
versions
+  private static final String HBASE_CLIENT_META_READ_RPC_TIMEOUT_KEY =
+    "hbase.client.meta.read.rpc.timeout";
+  private static final String HBASE_CLIENT_META_SCANNER_TIMEOUT =
+    "hbase.client.meta.scanner.timeout.period";
+  private static final String HBASE_CLIENT_PAUSE_FOR_SERVER_OVERLOADED =
+    "hbase.client.pause.server.overloaded";
 
   public static boolean hasCoprocessor(RegionCoprocessorEnvironment env,
     String CoprocessorClassName) {
@@ -167,6 +174,7 @@ public class ServerUtil {
     INDEX_WRITER_CONNECTION,
     INDEX_WRITER_CONNECTION_WITH_CUSTOM_THREADS,
     INDEX_WRITER_CONNECTION_WITH_CUSTOM_THREADS_NO_RETRIES,
+    METADATA_LOOKUP_CONNECTION,
     DEFAULT_SERVER_CONNECTION;
   }
 
@@ -203,6 +211,8 @@ public class ServerUtil {
           return getIndexWriterConfigurationWithCustomThreads(conf);
         case INDEX_WRITER_CONNECTION_WITH_CUSTOM_THREADS_NO_RETRIES:
           return getNoRetriesIndexWriterConfigurationWithCustomThreads(conf);
+        case METADATA_LOOKUP_CONNECTION:
+          return getMetadataConfig(conf);
         default:
           return conf;
       }
@@ -240,6 +250,56 @@ public class ServerUtil {
     return compactionConfig;
   }
 
+  public static Configuration getMetadataConfig(Configuration conf) {
+    Configuration metadataConfig = PropertiesUtil.cloneConfig(conf);
+    // fail fast, as the caller holds a SYSTEM.CATALOG region operation that 
blocks its close
+    int timeoutMs = 
conf.getInt(QueryServices.METADATA_SERVER_LOOKUP_TIMEOUT_MS,
+      QueryServicesOptions.DEFAULT_METADATA_SERVER_LOOKUP_TIMEOUT_MS);
+    if (timeoutMs <= 0) {
+      // HBase treats a non-positive timeout as no timeout
+      LOGGER.warn("Ignoring non-positive {}={}, using {}",
+        QueryServices.METADATA_SERVER_LOOKUP_TIMEOUT_MS, timeoutMs,
+        QueryServicesOptions.DEFAULT_METADATA_SERVER_LOOKUP_TIMEOUT_MS);
+      timeoutMs = 
QueryServicesOptions.DEFAULT_METADATA_SERVER_LOOKUP_TIMEOUT_MS;
+    }
+    metadataConfig.setInt(HConstants.HBASE_CLIENT_SCANNER_TIMEOUT_PERIOD, 
timeoutMs);
+    metadataConfig.setInt(HConstants.HBASE_CLIENT_OPERATION_TIMEOUT, 
timeoutMs);
+    metadataConfig.setInt(HConstants.HBASE_RPC_READ_TIMEOUT_KEY, timeoutMs);
+    // region location lookups use the meta timeouts, which default to much 
longer ones
+    metadataConfig.setInt(HConstants.HBASE_CLIENT_META_OPERATION_TIMEOUT, 
timeoutMs);
+    metadataConfig.setInt(HBASE_CLIENT_META_READ_RPC_TIMEOUT_KEY, timeoutMs);
+    metadataConfig.setInt(HBASE_CLIENT_META_SCANNER_TIMEOUT, timeoutMs);
+    // region location lookups retry without checking the timeout, so bound 
the retries too,
+    // using the larger pause that is applied when the server is overloaded
+    long pauseMs =
+      conf.getLong(HConstants.HBASE_CLIENT_PAUSE, 
HConstants.DEFAULT_HBASE_CLIENT_PAUSE);
+    long overloadedPauseMs = 
conf.getLong(HBASE_CLIENT_PAUSE_FOR_SERVER_OVERLOADED,
+      conf.getLong(HConstants.HBASE_CLIENT_PAUSE_FOR_CQTBE, pauseMs));
+    int retries = getRetriesWithinTimeout(timeoutMs, Math.max(pauseMs, 
overloadedPauseMs));
+    metadataConfig.setInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER, retries);
+    LOGGER.info("SYSTEM.CATALOG lookup connection timeout={} ms, retries={}", 
timeoutMs, retries);
+    return metadataConfig;
+  }
+
+  /**
+   * Returns the largest number of retries whose total nominal client backoff 
(see
+   * {@link HConstants#RETRY_BACKOFF}, excluding jitter and RPC time) fits 
within the given timeout.
+   */
+  static int getRetriesWithinTimeout(long timeoutMs, long pauseMs) {
+    long pause = Math.max(1, pauseMs);
+    int retries = 0;
+    long totalBackoff = 0;
+    while (true) {
+      long backoff =
+        pause * HConstants.RETRY_BACKOFF[Math.min(retries, 
HConstants.RETRY_BACKOFF.length - 1)];
+      if (totalBackoff + backoff > timeoutMs) {
+        return retries;
+      }
+      totalBackoff += backoff;
+      retries++;
+    }
+  }
+
   public static Configuration getIndexWriterConnection(Configuration conf) {
     Configuration clonedConfig = PropertiesUtil.cloneConfig(conf);
     /*
diff --git 
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/SystemCatalogTTLLookupIT.java
 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/SystemCatalogTTLLookupIT.java
new file mode 100644
index 0000000000..0b5654d20f
--- /dev/null
+++ 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/SystemCatalogTTLLookupIT.java
@@ -0,0 +1,351 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.phoenix.end2end;
+
+import static org.apache.phoenix.query.BaseTest.generateUniqueName;
+import static org.apache.phoenix.query.BaseTest.setUpConfigForMiniCluster;
+import static 
org.apache.phoenix.schema.LiteralTTLExpression.TTL_EXPRESSION_NOT_DEFINED;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.util.List;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.HBaseConfiguration;
+import org.apache.hadoop.hbase.HBaseTestingUtility;
+import org.apache.hadoop.hbase.HConstants;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.client.Admin;
+import org.apache.hadoop.hbase.client.ResultScanner;
+import org.apache.hadoop.hbase.client.Scan;
+import org.apache.hadoop.hbase.client.Table;
+import org.apache.hadoop.hbase.coprocessor.ObserverContext;
+import org.apache.hadoop.hbase.coprocessor.RegionCoprocessor;
+import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment;
+import org.apache.hadoop.hbase.coprocessor.RegionObserver;
+import org.apache.hadoop.hbase.regionserver.HRegion;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.phoenix.jdbc.PhoenixConnection;
+import org.apache.phoenix.jdbc.PhoenixDatabaseMetaData;
+import org.apache.phoenix.jdbc.PhoenixTestDriver;
+import org.apache.phoenix.schema.PTable;
+import org.apache.phoenix.util.PhoenixRuntime;
+import org.apache.phoenix.util.QueryUtil;
+import org.apache.phoenix.util.SchemaUtil;
+import org.apache.phoenix.util.TestUtil;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+/**
+ * Verifies that resolving TTL from the view/table/index hierarchy while 
building a PTable reads
+ * SYSTEM.CATALOG from the local region instead of issuing a client scan to 
it. Such a client scan
+ * targets the region whose operation the outer getTable holds, and stalls the 
region's close until
+ * the client scanner timeout.
+ */
+@Category(NeedsOwnMiniClusterTest.class)
+public class SystemCatalogTTLLookupIT {
+  private static final TableName SYSTEM_CATALOG =
+    TableName.valueOf(PhoenixDatabaseMetaData.SYSTEM_CATALOG_NAME);
+  private static final long MAX_CLOSE_MILLIS = 10000;
+  private static final long WAIT_MILLIS = 120000;
+
+  private static HBaseTestingUtility utility;
+  private static String url;
+
+  @BeforeClass
+  public static synchronized void doSetup() throws Exception {
+    Configuration conf = HBaseConfiguration.create();
+    setUpConfigForMiniCluster(conf);
+    // Restore the default client retries and scanner timeout (the test config 
lowers the former and
+    // raises the latter) so that a client scan of a closing region retries 
until the scanner
+    // timeout, as it does in production.
+    conf.setInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER,
+      HConstants.DEFAULT_HBASE_CLIENT_RETRIES_NUMBER);
+    conf.setInt(HConstants.HBASE_CLIENT_SCANNER_TIMEOUT_PERIOD,
+      HConstants.DEFAULT_HBASE_CLIENT_SCANNER_TIMEOUT_PERIOD);
+    utility = new HBaseTestingUtility(conf);
+    utility.startMiniCluster();
+    // Same test driver URL as the server side connections (e.g. for index 
creation) build.
+    url = QueryUtil.getConnectionUrl(new Properties(), 
utility.getConfiguration())
+      + PhoenixRuntime.PHOENIX_TEST_DRIVER_URL_PARAM;
+    DriverManager.registerDriver(new PhoenixTestDriver());
+  }
+
+  @AfterClass
+  public static synchronized void tearDown() throws Exception {
+    if (utility != null) {
+      utility.shutdownMiniCluster();
+    }
+  }
+
+  /**
+   * Counts client scans (RPC path only) of SYSTEM.CATALOG starting at any 
watched row key, and
+   * optionally runs a hook on the first one.
+   */
+  public static class ScanCountingObserver implements RegionCoprocessor, 
RegionObserver {
+    static final List<byte[]> WATCHED_KEYS = new CopyOnWriteArrayList<>();
+    static final AtomicInteger SCANS = new AtomicInteger();
+    static final AtomicReference<Runnable> ON_FIRST_SCAN = new 
AtomicReference<>();
+
+    @Override
+    public Optional<RegionObserver> getRegionObserver() {
+      return Optional.of(this);
+    }
+
+    @Override
+    public void preScannerOpen(ObserverContext<RegionCoprocessorEnvironment> 
c, Scan scan)
+      throws IOException {
+      for (byte[] key : WATCHED_KEYS) {
+        if (Bytes.equals(key, scan.getStartRow())) {
+          SCANS.incrementAndGet();
+          Runnable hook = ON_FIRST_SCAN.getAndSet(null);
+          if (hook != null) {
+            hook.run();
+          }
+        }
+      }
+    }
+  }
+
+  @Test
+  public void testTableWithIndexesDoesNotScanSystemCatalogViaRpc() throws 
Exception {
+    String tableName = generateUniqueName();
+    String indexName1 = generateUniqueName();
+    String indexName2 = generateUniqueName();
+    try (Connection conn = DriverManager.getConnection(url)) {
+      createTableWithTTL(conn, tableName);
+      conn.createStatement()
+        .execute("CREATE INDEX " + indexName1 + " ON " + tableName + " (V1) 
INCLUDE (V2)");
+      conn.createStatement().execute("CREATE INDEX " + indexName2 + " ON " + 
tableName + " (V2)");
+    }
+    watchTableKeys(tableName);
+
+    try (Connection conn = DriverManager.getConnection(url)) {
+      PhoenixConnection pconn = clearCachesAndResetCount(conn);
+      // Building the table also builds each of its indexes, which resolve TTL 
from the table.
+      PTable table = pconn.getTableNoCache(tableName);
+
+      assertNotEquals(TTL_EXPRESSION_NOT_DEFINED, table.getTTLExpression());
+      assertEquals(2, table.getIndexes().size());
+      for (PTable index : table.getIndexes()) {
+        assertEquals(table.getTTLExpression(), index.getTTLExpression());
+      }
+      assertNoSystemCatalogClientScans();
+    }
+  }
+
+  @Test
+  public void testViewHierarchyDoesNotScanSystemCatalogViaRpc() throws 
Exception {
+    String tableName = generateUniqueName();
+    String parentViewName = generateUniqueName();
+    String childViewName = generateUniqueName();
+    String viewIndexName = generateUniqueName();
+    createViewHierarchy(tableName, parentViewName, childViewName);
+    try (Connection conn = DriverManager.getConnection(url)) {
+      conn.createStatement()
+        .execute("CREATE INDEX " + viewIndexName + " ON " + childViewName + " 
(V1)");
+    }
+    watchTableKeys(tableName, parentViewName, childViewName);
+
+    try (Connection conn = DriverManager.getConnection(url)) {
+      PhoenixConnection pconn = clearCachesAndResetCount(conn);
+      PTable table = pconn.getTableNoCache(tableName);
+      // Building the view also builds its index, which resolves TTL from the 
view hierarchy.
+      PTable childView = pconn.getTableNoCache(childViewName);
+
+      assertNotEquals(TTL_EXPRESSION_NOT_DEFINED, table.getTTLExpression());
+      assertEquals(table.getTTLExpression(), childView.getTTLExpression());
+      assertEquals(1, childView.getIndexes().size());
+      assertEquals(table.getTTLExpression(), 
childView.getIndexes().get(0).getTTLExpression());
+      assertNoSystemCatalogClientScans();
+    }
+  }
+
+  /**
+   * Closes SYSTEM.CATALOG while a getTable is resolving TTL. If the TTL 
lookup issues a client
+   * scan, the close is started from that scan's preScannerOpen hook, so the 
scan then finds the
+   * region closing and retries for the scanner timeout while the outer 
getTable holds the region
+   * operation. Otherwise the region is closed once the getTable completes. 
Either way the close
+   * must not stall.
+   */
+  @Test
+  public void testSystemCatalogCloseIsNotStalledByTTLLookup() throws Exception 
{
+    String tableName = generateUniqueName();
+    String parentViewName = generateUniqueName();
+    String childViewName = generateUniqueName();
+    createViewHierarchy(tableName, parentViewName, childViewName);
+    watchTableKeys(tableName, parentViewName, childViewName);
+
+    HRegion sysCatRegion = 
utility.getHBaseCluster().getRegions(SYSTEM_CATALOG).get(0);
+    byte[] regionName = sysCatRegion.getRegionInfo().getRegionName();
+    ExecutorService executor = Executors.newFixedThreadPool(2);
+    AtomicReference<Future<Long>> close = new AtomicReference<>();
+    AtomicBoolean closeStarted = new AtomicBoolean();
+    AtomicBoolean closingSeen = new AtomicBoolean();
+    try (Admin admin = utility.getAdmin()) {
+      Throwable failure = null;
+      try {
+        ScanCountingObserver.ON_FIRST_SCAN.set(() -> {
+          close.set(executor.submit(() -> timedUnassign(admin, regionName)));
+          closingSeen.set(waitUntilClosing(sysCatRegion));
+          closeStarted.set(true);
+        });
+        Future<?> lookup = executor.submit(() -> {
+          try (Connection conn = DriverManager.getConnection(url)) {
+            clearCachesAndResetCount(conn).getTableNoCache(childViewName);
+          }
+          return null;
+        });
+        // Wait for either the hook to start the close, or the lookup to 
complete without it.
+        long deadline = System.currentTimeMillis() + WAIT_MILLIS;
+        while (!closeStarted.get() && !lookup.isDone()) {
+          assertTrue("TTL lookup did not complete", System.currentTimeMillis() 
< deadline);
+          Thread.sleep(10);
+        }
+        ScanCountingObserver.ON_FIRST_SCAN.set(null);
+        if (closeStarted.get()) {
+          assertTrue("SYSTEM.CATALOG did not start closing", 
closingSeen.get());
+        } else {
+          lookup.get(WAIT_MILLIS, TimeUnit.MILLISECONDS);
+          close.set(executor.submit(() -> timedUnassign(admin, regionName)));
+        }
+        long closeMillis = close.get().get(WAIT_MILLIS, TimeUnit.MILLISECONDS);
+        assertTrue("SYSTEM.CATALOG close took " + closeMillis + " ms",
+          closeMillis < MAX_CLOSE_MILLIS);
+      } catch (Throwable t) {
+        failure = t;
+        throw t;
+      } finally {
+        ScanCountingObserver.ON_FIRST_SCAN.set(null);
+        executor.shutdownNow();
+        executor.awaitTermination(WAIT_MILLIS, TimeUnit.MILLISECONDS);
+        restoreSystemCatalog(admin, regionName, close.get(), failure);
+      }
+    }
+  }
+
+  /**
+   * Brings SYSTEM.CATALOG back for the other tests if it was closed, without 
masking the failure of
+   * the test, if any.
+   */
+  private static void restoreSystemCatalog(Admin admin, byte[] regionName, 
Future<Long> close,
+    Throwable failure) throws Exception {
+    if (close == null) {
+      return;
+    }
+    try {
+      try {
+        close.get(WAIT_MILLIS, TimeUnit.MILLISECONDS);
+      } catch (ExecutionException | TimeoutException e) {
+        // The close failed or is still running; assign below only if the 
region is offline.
+      }
+      if (utility.getHBaseCluster().getRegions(SYSTEM_CATALOG).isEmpty()) {
+        admin.assign(regionName);
+      }
+      utility.waitUntilAllRegionsAssigned(SYSTEM_CATALOG);
+    } catch (Exception e) {
+      if (failure == null) {
+        throw e;
+      }
+      failure.addSuppressed(e);
+    }
+  }
+
+  private static long timedUnassign(Admin admin, byte[] regionName) throws 
IOException {
+    long start = System.currentTimeMillis();
+    admin.unassign(regionName);
+    return System.currentTimeMillis() - start;
+  }
+
+  private static boolean waitUntilClosing(HRegion region) {
+    long deadline = System.currentTimeMillis() + MAX_CLOSE_MILLIS;
+    while (!region.isClosing() && !region.isClosed()) {
+      if (System.currentTimeMillis() >= deadline) {
+        return false;
+      }
+      try {
+        Thread.sleep(10);
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private static void createTableWithTTL(Connection conn, String tableName) 
throws Exception {
+    conn.createStatement().execute("CREATE TABLE " + tableName
+      + " (ID VARCHAR NOT NULL PRIMARY KEY, V1 VARCHAR, V2 VARCHAR) 
TTL=86400");
+    TestUtil.addCoprocessor(conn, PhoenixDatabaseMetaData.SYSTEM_CATALOG_NAME,
+      ScanCountingObserver.class);
+  }
+
+  private static void createViewHierarchy(String tableName, String 
parentViewName,
+    String childViewName) throws Exception {
+    try (Connection conn = DriverManager.getConnection(url)) {
+      createTableWithTTL(conn, tableName);
+      conn.createStatement()
+        .execute("CREATE VIEW " + parentViewName + " AS SELECT * FROM " + 
tableName);
+      conn.createStatement()
+        .execute("CREATE VIEW " + childViewName + " AS SELECT * FROM " + 
parentViewName);
+    }
+  }
+
+  private static void watchTableKeys(String... names) {
+    ScanCountingObserver.WATCHED_KEYS.clear();
+    for (String name : names) {
+      ScanCountingObserver.WATCHED_KEYS.add(SchemaUtil.getTableKey(null, null, 
name));
+    }
+  }
+
+  private static PhoenixConnection clearCachesAndResetCount(Connection conn) 
throws Exception {
+    PhoenixConnection pconn = conn.unwrap(PhoenixConnection.class);
+    // Clear the client and server metadata caches so that getTable rebuilds 
the PTables.
+    pconn.getQueryServices().clearCache();
+    ScanCountingObserver.SCANS.set(0);
+    return pconn;
+  }
+
+  private static void assertNoSystemCatalogClientScans() throws IOException {
+    assertEquals("TTL lookup must not issue client scans to SYSTEM.CATALOG", 0,
+      ScanCountingObserver.SCANS.get());
+    // Positive control: a client scan of a watched key is counted.
+    try (Table sysCat = utility.getConnection().getTable(SYSTEM_CATALOG); 
ResultScanner scanner =
+      sysCat.getScanner(new 
Scan().withStartRow(ScanCountingObserver.WATCHED_KEYS.get(0)))) {
+      scanner.next();
+    }
+    assertEquals(1, ScanCountingObserver.SCANS.get());
+  }
+}
diff --git 
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/SystemCatalogTTLLookupSplitIT.java
 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/SystemCatalogTTLLookupSplitIT.java
new file mode 100644
index 0000000000..fcdcc33431
--- /dev/null
+++ 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/SystemCatalogTTLLookupSplitIT.java
@@ -0,0 +1,167 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.phoenix.end2end;
+
+import static 
org.apache.phoenix.schema.LiteralTTLExpression.TTL_EXPRESSION_NOT_DEFINED;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.util.Properties;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.client.RegionLocator;
+import 
org.apache.phoenix.end2end.SystemCatalogTTLLookupIT.ScanCountingObserver;
+import org.apache.phoenix.jdbc.PhoenixConnection;
+import org.apache.phoenix.jdbc.PhoenixDatabaseMetaData;
+import org.apache.phoenix.schema.PTable;
+import org.apache.phoenix.util.PhoenixRuntime;
+import org.apache.phoenix.util.SchemaUtil;
+import org.apache.phoenix.util.TestUtil;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+/**
+ * Verifies that resolving TTL from a view hierarchy spread over several 
SYSTEM.CATALOG regions
+ * reads the ancestors held by other regions through client scans (and only 
those), and resolves the
+ * table's TTL.
+ */
+@Category(NeedsOwnMiniClusterTest.class)
+public class SystemCatalogTTLLookupSplitIT extends SplitSystemCatalogIT {
+  private static final TableName SYSTEM_CATALOG =
+    TableName.valueOf(PhoenixDatabaseMetaData.SYSTEM_CATALOG_NAME);
+
+  @Test
+  public void testTenantViewOnTableInAnotherRegion() throws Exception {
+    String tableName = SchemaUtil.getTableName(SCHEMA1, generateUniqueName());
+    String viewName = SchemaUtil.getTableName(SCHEMA2, generateUniqueName());
+    createTableWithTTL(tableName);
+    createTenantView(viewName, tableName);
+    byte[] tableKey = SchemaUtil.getTableKeyFromFullName(tableName);
+    byte[] viewKey = getTenantTableKey(viewName);
+    assertInDifferentRegions(tableKey, viewKey);
+
+    try (Connection conn = getTenantConnection()) {
+      // The view is read from the local region.
+      assertScansWhileBuilding(conn, viewName, viewKey, 0);
+      // The table is client-scanned in its own region.
+      PTable view = assertScansWhileBuilding(conn, viewName, tableKey, 1);
+      assertTTLOf(tableName, view);
+    }
+  }
+
+  @Test
+  public void testTenantViewHierarchyAcrossRegions() throws Exception {
+    String tableName = SchemaUtil.getTableName(SCHEMA1, generateUniqueName());
+    String parentViewName = SchemaUtil.getTableName(SCHEMA2, 
generateUniqueName());
+    String childViewName = SchemaUtil.getTableName(SCHEMA3, 
generateUniqueName());
+    createTableWithTTL(tableName);
+    createTenantView(parentViewName, tableName);
+    createTenantView(childViewName, parentViewName);
+    byte[] tableKey = SchemaUtil.getTableKeyFromFullName(tableName);
+    byte[] parentViewKey = getTenantTableKey(parentViewName);
+    byte[] childViewKey = getTenantTableKey(childViewName);
+    assertInDifferentRegions(tableKey, parentViewKey, childViewKey);
+
+    try (Connection conn = getTenantConnection()) {
+      // The child view is read from the local region.
+      assertScansWhileBuilding(conn, childViewName, childViewKey, 0);
+      // The parent view is client-scanned in the parent's region, only by the 
child view's TTL
+      // recursion. (Without the fix, building the parent view, which the 
client resolves along
+      // with the child view, would also client-scan it.)
+      assertScansWhileBuilding(conn, childViewName, parentViewKey, 1);
+      // The table is client-scanned in its own region, by the child view's 
TTL recursion and,
+      // depending on how the client resolves the parent view, by building the 
parent view.
+      ScanCountingObserver.WATCHED_KEYS.clear();
+      ScanCountingObserver.WATCHED_KEYS.add(tableKey);
+      PTable childView = 
clearCachesAndResetCount(conn).getTableNoCache(childViewName);
+      assertTrue(ScanCountingObserver.SCANS.get() >= 1);
+      assertTTLOf(tableName, childView);
+    }
+  }
+
+  /**
+   * Rebuilds the given table or view and asserts the number of client scans 
of SYSTEM.CATALOG
+   * starting at the given row key.
+   */
+  private static PTable assertScansWhileBuilding(Connection conn, String name, 
byte[] key,
+    int expectedScans) throws Exception {
+    ScanCountingObserver.WATCHED_KEYS.clear();
+    ScanCountingObserver.WATCHED_KEYS.add(key);
+    PTable table = clearCachesAndResetCount(conn).getTableNoCache(name);
+    assertEquals(expectedScans, ScanCountingObserver.SCANS.get());
+    return table;
+  }
+
+  private static void createTableWithTTL(String tableName) throws Exception {
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      conn.createStatement()
+        .execute("CREATE TABLE " + tableName
+          + " (TENANT_ID VARCHAR NOT NULL, ID VARCHAR NOT NULL, V1 VARCHAR"
+          + " CONSTRAINT PK PRIMARY KEY (TENANT_ID, ID)) MULTI_TENANT=true, 
TTL=86400");
+      TestUtil.addCoprocessor(conn, 
PhoenixDatabaseMetaData.SYSTEM_CATALOG_NAME,
+        ScanCountingObserver.class);
+    }
+  }
+
+  private static void createTenantView(String viewName, String parentName) 
throws Exception {
+    try (Connection conn = getTenantConnection()) {
+      conn.createStatement().execute("CREATE VIEW " + viewName + " AS SELECT * 
FROM " + parentName);
+    }
+  }
+
+  private static Connection getTenantConnection() throws Exception {
+    Properties props = new Properties();
+    props.setProperty(PhoenixRuntime.TENANT_ID_ATTRIB, TENANT1);
+    return DriverManager.getConnection(getUrl(), props);
+  }
+
+  private static byte[] getTenantTableKey(String fullName) {
+    return SchemaUtil.getTableKey(TENANT1, 
SchemaUtil.getSchemaNameFromFullName(fullName),
+      SchemaUtil.getTableNameFromFullName(fullName));
+  }
+
+  private static void assertInDifferentRegions(byte[]... keys) throws 
Exception {
+    try (RegionLocator locator = 
getUtility().getConnection().getRegionLocator(SYSTEM_CATALOG)) {
+      for (int i = 0; i < keys.length; i++) {
+        for (int j = i + 1; j < keys.length; j++) {
+          assertNotEquals("Expected the keys in different SYSTEM.CATALOG 
regions",
+            locator.getRegionLocation(keys[i], true).getRegion(),
+            locator.getRegionLocation(keys[j], true).getRegion());
+        }
+      }
+    }
+  }
+
+  private static PhoenixConnection clearCachesAndResetCount(Connection conn) 
throws Exception {
+    PhoenixConnection pconn = conn.unwrap(PhoenixConnection.class);
+    // Clear the client and server metadata caches so that getTable rebuilds 
the PTables.
+    pconn.getQueryServices().clearCache();
+    ScanCountingObserver.SCANS.set(0);
+    return pconn;
+  }
+
+  private static void assertTTLOf(String tableName, PTable view) throws 
Exception {
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      PTable table = 
conn.unwrap(PhoenixConnection.class).getTableNoCache(tableName);
+      assertNotEquals(TTL_EXPRESSION_NOT_DEFINED, table.getTTLExpression());
+      assertEquals(table.getTTLExpression(), view.getTTLExpression());
+    }
+  }
+}
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/util/ServerUtilTest.java 
b/phoenix-core/src/test/java/org/apache/phoenix/util/ServerUtilTest.java
new file mode 100644
index 0000000000..ba8821f080
--- /dev/null
+++ b/phoenix-core/src/test/java/org/apache/phoenix/util/ServerUtilTest.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.phoenix.util;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.HConstants;
+import org.apache.phoenix.query.QueryServices;
+import org.apache.phoenix.query.QueryServicesOptions;
+import org.junit.Test;
+
+public class ServerUtilTest {
+
+  @Test
+  public void testMetadataConfigDefaults() {
+    Configuration conf = new Configuration(false);
+    conf.setInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER, 150);
+    conf.setInt(HConstants.HBASE_CLIENT_SCANNER_TIMEOUT_PERIOD, 60000);
+    conf.set("some.other.key", "value");
+
+    Configuration metadataConf = ServerUtil.getMetadataConfig(conf);
+
+    int timeoutMs = 
QueryServicesOptions.DEFAULT_METADATA_SERVER_LOOKUP_TIMEOUT_MS;
+    assertEquals(timeoutMs,
+      metadataConf.getInt(HConstants.HBASE_CLIENT_SCANNER_TIMEOUT_PERIOD, -1));
+    assertEquals(timeoutMs, 
metadataConf.getInt(HConstants.HBASE_CLIENT_OPERATION_TIMEOUT, -1));
+    assertEquals(timeoutMs, 
metadataConf.getInt(HConstants.HBASE_RPC_READ_TIMEOUT_KEY, -1));
+    assertEquals(timeoutMs,
+      metadataConf.getInt(HConstants.HBASE_CLIENT_META_OPERATION_TIMEOUT, -1));
+    assertEquals(timeoutMs, 
metadataConf.getInt("hbase.client.meta.read.rpc.timeout", -1));
+    assertEquals(timeoutMs, 
metadataConf.getInt("hbase.client.meta.scanner.timeout.period", -1));
+    assertEquals("value", metadataConf.get("some.other.key"));
+    // backoffs of 0.1+0.2+0.3+0.5+1+2+4 = 8.1s fit in 10s, the next 10s 
backoff does not
+    assertEquals(7, 
metadataConf.getInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER, -1));
+    // the source configuration is not modified
+    assertEquals(150, conf.getInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER, -1));
+    assertEquals(60000, 
conf.getInt(HConstants.HBASE_CLIENT_SCANNER_TIMEOUT_PERIOD, -1));
+  }
+
+  @Test
+  public void testMetadataConfigOverrides() {
+    Configuration conf = new Configuration(false);
+    conf.setInt(QueryServices.METADATA_SERVER_LOOKUP_TIMEOUT_MS, 2500);
+
+    Configuration metadataConf = ServerUtil.getMetadataConfig(conf);
+
+    assertEquals(2500, 
metadataConf.getInt(HConstants.HBASE_CLIENT_SCANNER_TIMEOUT_PERIOD, -1));
+    assertEquals(2500, 
metadataConf.getInt(HConstants.HBASE_CLIENT_OPERATION_TIMEOUT, -1));
+    assertEquals(2500, 
metadataConf.getInt(HConstants.HBASE_RPC_READ_TIMEOUT_KEY, -1));
+    // backoffs of 0.1+0.2+0.3+0.5+1 = 2.1s fit in 2.5s
+    assertEquals(5, 
metadataConf.getInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER, -1));
+  }
+
+  @Test
+  public void testMetadataConfigRetriesUseLargerPause() {
+    Configuration conf = new Configuration(false);
+    conf.setLong(HConstants.HBASE_CLIENT_PAUSE, 1000);
+    // backoffs of 1+2+3 = 6s fit in 10s
+    assertEquals(3,
+      
ServerUtil.getMetadataConfig(conf).getInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER,
 -1));
+
+    conf.setLong(HConstants.HBASE_CLIENT_PAUSE, 100);
+    conf.setLong("hbase.client.pause.server.overloaded", 1000);
+    assertEquals(3,
+      
ServerUtil.getMetadataConfig(conf).getInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER,
 -1));
+  }
+
+  @Test
+  public void testMetadataConfigNonPositiveTimeoutUsesDefault() {
+    Configuration conf = new Configuration(false);
+    conf.setInt(QueryServices.METADATA_SERVER_LOOKUP_TIMEOUT_MS, 0);
+
+    Configuration metadataConf = ServerUtil.getMetadataConfig(conf);
+
+    
assertEquals(QueryServicesOptions.DEFAULT_METADATA_SERVER_LOOKUP_TIMEOUT_MS,
+      metadataConf.getInt(HConstants.HBASE_CLIENT_SCANNER_TIMEOUT_PERIOD, -1));
+    assertEquals(7, 
metadataConf.getInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER, -1));
+  }
+
+  @Test
+  public void testRetriesWithinTimeout() {
+    assertEquals(0, ServerUtil.getRetriesWithinTimeout(50, 100));
+    assertEquals(1, ServerUtil.getRetriesWithinTimeout(100, 100));
+    assertEquals(7, ServerUtil.getRetriesWithinTimeout(10000, 100));
+    // a larger pause allows fewer retries in the same time
+    assertEquals(3, ServerUtil.getRetriesWithinTimeout(10000, 1000));
+    // a zero pause must not loop forever
+    assertEquals(13, ServerUtil.getRetriesWithinTimeout(1000, 0));
+  }
+
+  @Test
+  public void testMetadataLookupConnectionUsesMetadataConfig() {
+    Configuration conf = new Configuration(false);
+    conf.setInt(QueryServices.METADATA_SERVER_LOOKUP_TIMEOUT_MS, 2500);
+
+    Configuration connConf = ServerUtil.ConnectionFactory
+      
.getTypeSpecificConfiguration(ServerUtil.ConnectionType.METADATA_LOOKUP_CONNECTION,
 conf);
+
+    assertEquals(2500, 
connConf.getInt(HConstants.HBASE_CLIENT_SCANNER_TIMEOUT_PERIOD, -1));
+  }
+}

Reply via email to