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

virajjasani pushed a commit to branch branch-2.6
in repository https://gitbox.apache.org/repos/asf/hbase.git


The following commit(s) were added to refs/heads/branch-2.6 by this push:
     new 077fd5cd36e HBASE-30161 Add paginated, single-RPC 
RegionLocator.getRegionLocationsPage(startKey, limit) API for bulk meta-cache 
warmup (#8265) (#8237)
077fd5cd36e is described below

commit 077fd5cd36eee6558f9eb41a95f68ef076fe7206
Author: sanjeet006py <[email protected]>
AuthorDate: Wed May 27 05:50:03 2026 +0530

    HBASE-30161 Add paginated, single-RPC 
RegionLocator.getRegionLocationsPage(startKey, limit) API for bulk meta-cache 
warmup (#8265) (#8237)
    
    Signed-off-by: Viraj Jasani <[email protected]>
    
    Generated-by: Claude Opus 4.7 <[email protected]>
---
 .../org/apache/hadoop/hbase/MetaTableAccessor.java |  45 ++++-
 .../hbase/client/ConnectionImplementation.java     |  19 +-
 .../apache/hadoop/hbase/client/HRegionLocator.java |  58 +++++++
 .../apache/hadoop/hbase/client/RegionLocator.java  |  49 ++++++
 .../TestMetaTableAccessorPagedScanCaching.java     | 107 ++++++++++++
 .../hadoop/hbase/client/TestRegionLocator.java     | 192 +++++++++++++++++++++
 6 files changed, 459 insertions(+), 11 deletions(-)

diff --git 
a/hbase-client/src/main/java/org/apache/hadoop/hbase/MetaTableAccessor.java 
b/hbase-client/src/main/java/org/apache/hadoop/hbase/MetaTableAccessor.java
index 1819dda048d..e1fd74922f0 100644
--- a/hbase-client/src/main/java/org/apache/hadoop/hbase/MetaTableAccessor.java
+++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/MetaTableAccessor.java
@@ -356,7 +356,7 @@ public class MetaTableAccessor {
     throws IOException {
     RowFilter rowFilter =
       new RowFilter(CompareOperator.EQUAL, new 
SubstringComparator(regionEncodedName));
-    Scan scan = getMetaScan(connection.getConfiguration(), 1);
+    Scan scan = getMetaScan(connection.getConfiguration(), 1, false);
     scan.setFilter(rowFilter);
     try (Table table = getMetaHTable(connection);
       ResultScanner resultScanner = table.getScanner(scan)) {
@@ -558,13 +558,13 @@ public class MetaTableAccessor {
     // Stop key appends the smallest possible char to the table name
     byte[] stopKey = getTableStopRowForMeta(tableName, QueryType.REGION);
 
-    Scan scan = getMetaScan(conf, -1);
+    Scan scan = getMetaScan(conf, -1, false);
     scan.setStartRow(startKey);
     scan.setStopRow(stopKey);
     return scan;
   }
 
-  private static Scan getMetaScan(Configuration conf, int rowUpperLimit) {
+  private static Scan getMetaScan(Configuration conf, int rowUpperLimit, 
boolean isPagedScan) {
     Scan scan = new Scan();
     int scannerCaching = conf.getInt(HConstants.HBASE_META_SCANNER_CACHING,
       HConstants.DEFAULT_HBASE_META_SCANNER_CACHING);
@@ -575,7 +575,14 @@ public class MetaTableAccessor {
       scan.setLimit(rowUpperLimit);
       scan.setReadType(Scan.ReadType.PREAD);
     }
-    scan.setCaching(scannerCaching);
+    if (isPagedScan) {
+      // Caller is doing a bounded paged scan and expects the whole slice back 
in one ScannerNext
+      // RPC. Size caching to the slice. Trade-off: a single larger response 
uses more
+      // RegionServer heap, fine for meta rows (small).
+      scan.setCaching(rowUpperLimit);
+    } else {
+      scan.setCaching(scannerCaching);
+    }
     scan.setPriority(HConstants.INTERNAL_READ_QOS);
     return scan;
   }
@@ -706,6 +713,25 @@ public class MetaTableAccessor {
     scanMetaForTableRegions(connection, visitor, tableName, 
CatalogReplicaMode.NONE);
   }
 
+  /**
+   * Scan meta for regions of {@code tableName}, starting at the meta row 
derived from
+   * {@code startRow} and returning at most {@code rowLimit} rows. {@code 
startRow} must be a region
+   * start-key boundary (e.g. the end key of the previously visited region), 
or {@code null}/empty
+   * to start at the first region. The scan is sized so that the whole {@code 
rowLimit}-row slice
+   * comes back in a single ScannerNext RPC, regardless of the configured
+   * {@code hbase.meta.scanner.caching}.
+   */
+  public static void scanMetaForTableRegions(Connection connection, Visitor 
visitor,
+    TableName tableName, byte[] startRow, int rowLimit, CatalogReplicaMode 
metaReplicaMode)
+    throws IOException {
+    byte[] metaStart = (startRow == null || startRow.length == 0)
+      ? getTableStartRowForMeta(tableName, QueryType.REGION)
+      : RegionInfo.createRegionName(tableName, startRow, HConstants.ZEROES, 
false);
+    byte[] metaStop = getTableStopRowForMeta(tableName, QueryType.REGION);
+    scanMeta(connection, metaStart, metaStop, QueryType.REGION, null, 
rowLimit, true, visitor,
+      metaReplicaMode);
+  }
+
   private static void scanMeta(Connection connection, TableName table, 
QueryType type, int maxRows,
     final Visitor visitor, CatalogReplicaMode metaReplicaMode) throws 
IOException {
     scanMeta(connection, getTableStartRowForMeta(table, type), 
getTableStopRowForMeta(table, type),
@@ -760,8 +786,15 @@ public class MetaTableAccessor {
   private static void scanMeta(Connection connection, @Nullable final byte[] 
startRow,
     @Nullable final byte[] stopRow, QueryType type, @Nullable Filter filter, 
int maxRows,
     final Visitor visitor, CatalogReplicaMode metaReplicaMode) throws 
IOException {
+    scanMeta(connection, startRow, stopRow, type, filter, maxRows, false, 
visitor, metaReplicaMode);
+  }
+
+  private static void scanMeta(Connection connection, @Nullable final byte[] 
startRow,
+    @Nullable final byte[] stopRow, QueryType type, @Nullable Filter filter, 
int maxRows,
+    boolean isPagedScan, final Visitor visitor, CatalogReplicaMode 
metaReplicaMode)
+    throws IOException {
     int rowUpperLimit = maxRows > 0 ? maxRows : Integer.MAX_VALUE;
-    Scan scan = getMetaScan(connection.getConfiguration(), rowUpperLimit);
+    Scan scan = getMetaScan(connection.getConfiguration(), rowUpperLimit, 
isPagedScan);
 
     for (byte[] family : type.getFamilies()) {
       scan.addFamily(family);
@@ -830,7 +863,7 @@ public class MetaTableAccessor {
   private static RegionInfo getClosestRegionInfo(Connection connection,
     @NonNull final TableName tableName, @NonNull final byte[] row) throws 
IOException {
     byte[] searchRow = RegionInfo.createRegionName(tableName, row, 
HConstants.NINES, false);
-    Scan scan = getMetaScan(connection.getConfiguration(), 1);
+    Scan scan = getMetaScan(connection.getConfiguration(), 1, false);
     scan.setReversed(true);
     scan.withStartRow(searchRow);
     try (ResultScanner resultScanner = 
getMetaHTable(connection).getScanner(scan)) {
diff --git 
a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/ConnectionImplementation.java
 
b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/ConnectionImplementation.java
index 28b26ba648b..d3f1c091715 100644
--- 
a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/ConnectionImplementation.java
+++ 
b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/ConnectionImplementation.java
@@ -1146,11 +1146,7 @@ public class ConnectionImplementation implements 
ClusterConnection, Closeable {
         }
       } finally {
         if (lockedUserRegion) {
-          userRegionLock.unlock();
-          // update duration of the lock being held
-          if (metrics != null) {
-            
metrics.updateUserRegionLockHeld(EnvironmentEdgeManager.currentTime() - 
lockStartTime);
-          }
+          releaseUserRegionLock(lockStartTime);
         }
       }
       try {
@@ -1185,6 +1181,19 @@ public class ConnectionImplementation implements 
ClusterConnection, Closeable {
     }
   }
 
+  /**
+   * Release {@link #userRegionLock} previously acquired via {@link 
#takeUserRegionLock()} and
+   * record the held duration in metrics.
+   * @param lockStartTimeMs value of {@link 
EnvironmentEdgeManager#currentTime()} captured
+   *                        immediately after {@link #takeUserRegionLock()} 
returned
+   */
+  void releaseUserRegionLock(long lockStartTimeMs) {
+    userRegionLock.unlock();
+    if (metrics != null) {
+      metrics.updateUserRegionLockHeld(EnvironmentEdgeManager.currentTime() - 
lockStartTimeMs);
+    }
+  }
+
   /**
    * Put a newly discovered HRegionLocation into the cache.
    * @param tableName The table name.
diff --git 
a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HRegionLocator.java 
b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HRegionLocator.java
index 50e4a26c07c..ee6c36e4c8a 100644
--- 
a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HRegionLocator.java
+++ 
b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HRegionLocator.java
@@ -39,6 +39,7 @@ import org.apache.hadoop.hbase.RegionLocations;
 import org.apache.hadoop.hbase.TableName;
 import org.apache.hadoop.hbase.client.trace.TableSpanBuilder;
 import org.apache.hadoop.hbase.trace.TraceUtil;
+import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
 import org.apache.yetus.audience.InterfaceAudience;
 
 import 
org.apache.hbase.thirdparty.org.apache.commons.collections4.CollectionUtils;
@@ -111,6 +112,63 @@ public class HRegionLocator implements RegionLocator {
     }, HRegionLocator::getRegionNames, supplier);
   }
 
+  @Override
+  public List<HRegionLocation> getRegionLocationsPage(byte[] startKey, int 
limit)
+    throws IOException {
+    if (TableName.isMetaTableName(tableName)) {
+      throw new IOException(
+        "getRegionLocationsPage(startKey, limit) is not supported for 
hbase:meta;"
+          + " use getRegionLocation(EMPTY_START_ROW) instead.");
+    }
+    final int effectiveLimit = limit > 0
+      ? limit
+      : 
connection.getConfiguration().getInt(HConstants.HBASE_META_SCANNER_CACHING,
+        HConstants.DEFAULT_HBASE_META_SCANNER_CACHING);
+    final byte[] effectiveStart = startKey == null ? 
HConstants.EMPTY_START_ROW : startKey;
+    final CatalogReplicaMode metaReplicaMode = 
CatalogReplicaMode.fromString(connection
+      .getConfiguration().get(LOCATOR_META_REPLICAS_MODE, 
CatalogReplicaMode.NONE.toString()));
+
+    final Supplier<Span> supplier = new TableSpanBuilder(connection)
+      
.setName("HRegionLocator.getRegionLocationsPage").setTableName(tableName);
+    return tracedLocationFuture(() -> {
+      final List<HRegionLocation> out = new ArrayList<>(effectiveLimit);
+      MetaTableAccessor.Visitor visitor = new 
MetaTableAccessor.TableVisitorBase(tableName) {
+        @Override
+        public boolean visitInternal(Result result) throws IOException {
+          RegionLocations locs = MetaTableAccessor.getRegionLocations(result);
+          if (locs == null) {
+            return true;
+          }
+          for (HRegionLocation loc : locs.getRegionLocations()) {
+            if (loc != null) {
+              out.add(loc);
+            }
+          }
+          RegionLocations cleaned = locs.removeElementsWithNullLocation();
+          if (cleaned != null) {
+            connection.cacheLocation(tableName, cleaned);
+          }
+          return true;
+        }
+      };
+
+      boolean locked = false;
+      long lockStart = 0;
+      try {
+        connection.takeUserRegionLock();
+        lockStart = EnvironmentEdgeManager.currentTime();
+        locked = true;
+        MetaTableAccessor.scanMetaForTableRegions(connection, visitor, 
tableName, effectiveStart,
+          effectiveLimit, metaReplicaMode);
+      } finally {
+        if (locked) {
+          connection.releaseUserRegionLock(lockStart);
+        }
+      }
+      return out;
+    }, HRegionLocator::getRegionNames, supplier);
+  }
+
   private static List<String> getRegionNames(List<HRegionLocation> locations) {
     if (CollectionUtils.isEmpty(locations)) {
       return Collections.emptyList();
diff --git 
a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/RegionLocator.java 
b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/RegionLocator.java
index 40f31b06f25..40a5678f3b7 100644
--- 
a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/RegionLocator.java
+++ 
b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/RegionLocator.java
@@ -21,6 +21,7 @@ import java.io.Closeable;
 import java.io.IOException;
 import java.util.List;
 import java.util.stream.Collectors;
+import org.apache.hadoop.hbase.HConstants;
 import org.apache.hadoop.hbase.HRegionLocation;
 import org.apache.hadoop.hbase.TableName;
 import org.apache.hadoop.hbase.util.Pair;
@@ -130,6 +131,54 @@ public interface RegionLocator extends Closeable {
    */
   List<HRegionLocation> getAllRegionLocations() throws IOException;
 
+  /**
+   * Bulk lookup of region locations from {@code hbase:meta} in a single RPC, 
starting at
+   * {@code startKey} (region start-key boundary, inclusive) and returning at 
most {@code limit}
+   * regions in start-key order.
+   * <p/>
+   * The returned list includes all replicas of each region (matching
+   * {@link #getAllRegionLocations()}), and the result is also written to the 
connection's region
+   * location cache.
+   * <p/>
+   * Ordering: regions are returned in ascending region start-key order (the 
natural order of
+   * {@code hbase:meta} rows for a single table). Within each region, replicas 
are returned in
+   * ascending replica-id order (replica 0, then 1, then 2, ...). Split 
parents and offline regions
+   * are filtered out, which may cause a page to contain fewer than {@code 
limit} regions but never
+   * disturbs ordering of the survivors.
+   * <p/>
+   * To page through all regions of a table, call repeatedly passing
+   * {@code last.getRegion().getEndKey()} as the next {@code startKey}, where 
{@code last} is the
+   * final element of the previous response. All replicas of a region share 
the same
+   * {@link RegionInfo}, so the last entry's end key is the correct cursor 
regardless of which
+   * replica it is. Pass {@code null} for the first call. Stop paging when the 
returned list is
+   * empty or when the last region's end key is {@link 
HConstants#EMPTY_END_ROW} (zero-length) -
+   * that signals the end of the table; passing it back in would re-scan from 
the beginning since by
+   * convention an empty start key means "from the first region".
+   * <p/>
+   * Unlike {@link #getAllRegionLocations()}, this method performs at most one 
RPC against
+   * {@code hbase:meta} per invocation, so its latency is bounded by {@code 
limit} rather than table
+   * size. Suitable for callers that wrap meta lookups in a lock with a fixed 
timeout, e.g. for bulk
+   * region-cache warmup.
+   * <p/>
+   * This method is optional. Implementations that cannot support paginated 
lookups should throw
+   * {@link UnsupportedOperationException} (the default behavior); callers 
should fall back to
+   * {@link #getAllRegionLocations()} in that case.
+   * @param startKey region start-key to begin scanning from (inclusive); 
{@code null} or empty
+   *                 starts from the first region
+   * @param limit    maximum number of regions to return; if &lt;= 0, falls 
back to
+   *                 {@code hbase.meta.scanner.caching}
+   * @return up to {@code limit} {@link HRegionLocation}s in start-key order, 
possibly empty when no
+   *         more regions exist
+   * @throws IOException                   if a remote or network exception 
occurs
+   * @throws UnsupportedOperationException if this implementation does not 
support paginated lookups
+   */
+  default List<HRegionLocation> getRegionLocationsPage(byte[] startKey, int 
limit)
+    throws IOException {
+    throw new UnsupportedOperationException(
+      "getRegionLocationsPage(byte[], int) is not supported by this 
RegionLocator;"
+        + " fall back to getAllRegionLocations()");
+  }
+
   /**
    * Gets the starting row key for every region in the currently open table.
    * <p>
diff --git 
a/hbase-client/src/test/java/org/apache/hadoop/hbase/TestMetaTableAccessorPagedScanCaching.java
 
b/hbase-client/src/test/java/org/apache/hadoop/hbase/TestMetaTableAccessorPagedScanCaching.java
new file mode 100644
index 00000000000..e87b0f439cf
--- /dev/null
+++ 
b/hbase-client/src/test/java/org/apache/hadoop/hbase/TestMetaTableAccessorPagedScanCaching.java
@@ -0,0 +1,107 @@
+/*
+ * 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.hadoop.hbase;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.client.Connection;
+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.testclassification.ClientTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+/**
+ * Asserts the single-RPC promise of the paginated meta-scan path
+ * ({@link MetaTableAccessor#scanMetaForTableRegions(Connection, 
MetaTableAccessor.Visitor, TableName, byte[], int, CatalogReplicaMode)})
+ * by capturing the {@link Scan} dispatched against the meta {@link Table} and 
asserting
+ * {@code scan.getCaching() == rowLimit}. ScannerNext RPC count is
+ * {@code ceil(rowsRequested / scan.getCaching())}, so {@code caching == 
rowLimit} is sufficient to
+ * prove a single ScannerNext RPC.
+ * <p/>
+ * The configured {@code hbase.meta.scanner.caching} is set to a value smaller 
than {@code rowLimit}
+ * so the paged-vs-unbounded branches in {@code MetaTableAccessor#getMetaScan} 
are distinguishable.
+ */
+@Tag(ClientTests.TAG)
+@Tag(SmallTests.TAG)
+public class TestMetaTableAccessorPagedScanCaching {
+
+  private static final TableName USER_TABLE = 
TableName.valueOf("LocatorPaged");
+  private static final int META_CACHING = 2;
+  private static final int NUM_REGIONS = 5;
+
+  private static final MetaTableAccessor.Visitor NOOP_VISITOR = result -> true;
+
+  private Connection connection;
+  private Table metaTable;
+
+  @BeforeEach
+  public void setUp() throws IOException {
+    Configuration conf = HBaseConfiguration.create();
+    conf.setInt(HConstants.HBASE_META_SCANNER_CACHING, META_CACHING);
+
+    connection = mock(Connection.class);
+    metaTable = mock(Table.class);
+    ResultScanner scanner = mock(ResultScanner.class);
+
+    when(connection.getConfiguration()).thenReturn(conf);
+    when(connection.getTable(TableName.META_TABLE_NAME)).thenReturn(metaTable);
+    when(metaTable.getScanner(any(Scan.class))).thenReturn(scanner);
+    when(scanner.next()).thenReturn(null);
+  }
+
+  @Test
+  public void testPagedScanCachingEqualsLimitWhenLimitWithinCaching() throws 
IOException {
+    int rowLimit = META_CACHING;
+    MetaTableAccessor.scanMetaForTableRegions(connection, NOOP_VISITOR, 
USER_TABLE, null, rowLimit,
+      CatalogReplicaMode.NONE);
+    assertEquals(rowLimit, capturedScan().getCaching());
+  }
+
+  @Test
+  public void testPagedScanCachingEqualsLimitWhenLimitExceedsCaching() throws 
IOException {
+    int rowLimit = NUM_REGIONS;
+    MetaTableAccessor.scanMetaForTableRegions(connection, NOOP_VISITOR, 
USER_TABLE, null, rowLimit,
+      CatalogReplicaMode.NONE);
+    assertEquals(rowLimit, capturedScan().getCaching());
+  }
+
+  @Test
+  public void testUnboundedPathStillUsesConfiguredCaching() throws IOException 
{
+    MetaTableAccessor.scanMetaForTableRegions(connection, NOOP_VISITOR, 
USER_TABLE);
+    Scan scan = capturedScan();
+    assertEquals(META_CACHING, scan.getCaching());
+    assertEquals(Integer.MAX_VALUE, scan.getLimit());
+  }
+
+  private Scan capturedScan() throws IOException {
+    ArgumentCaptor<Scan> scanCaptor = ArgumentCaptor.forClass(Scan.class);
+    verify(metaTable).getScanner(scanCaptor.capture());
+    return scanCaptor.getValue();
+  }
+}
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestRegionLocator.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestRegionLocator.java
index e464344c0c6..6deed9b97d4 100644
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestRegionLocator.java
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestRegionLocator.java
@@ -17,16 +17,28 @@
  */
 package org.apache.hadoop.hbase.client;
 
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
 import java.io.IOException;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.HConstants;
 import org.apache.hadoop.hbase.HRegionLocation;
+import org.apache.hadoop.hbase.RegionLocations;
 import org.apache.hadoop.hbase.TableName;
 import org.apache.hadoop.hbase.testclassification.ClientTests;
 import org.apache.hadoop.hbase.testclassification.MediumTests;
+import org.apache.hadoop.hbase.util.Bytes;
 import org.apache.hadoop.hbase.util.Pair;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
 
 @Tag(MediumTests.TAG)
 @Tag(ClientTests.TAG)
@@ -92,4 +104,184 @@ public class TestRegionLocator extends 
AbstractTestRegionLocator {
       locator.clearRegionLocationCache();
     }
   }
+
+  @Test
+  public void testGetRegionLocationsFirstPage() throws IOException {
+    try (RegionLocator locator = 
UTIL.getConnection().getRegionLocator(TABLE_NAME)) {
+      List<HRegionLocation> page = 
locator.getRegionLocationsPage(HConstants.EMPTY_START_ROW, 3);
+      assertEquals(3 * REGION_REPLICATION, page.size());
+      // Contract: regions in ascending start-key order, replicas in ascending 
replicaId order
+      // within each region.
+      byte[][] expectedStartKeys =
+        new byte[][] { HConstants.EMPTY_START_ROW, SPLIT_KEYS[0], 
SPLIT_KEYS[1] };
+      for (int i = 0; i < 3; i++) {
+        for (int replicaId = 0; replicaId < REGION_REPLICATION; replicaId++) {
+          HRegionLocation loc = page.get(i * REGION_REPLICATION + replicaId);
+          assertArrayEquals(expectedStartKeys[i], 
loc.getRegion().getStartKey(),
+            "region " + i + " replica " + replicaId + " start key");
+          assertEquals(replicaId, loc.getRegion().getReplicaId(),
+            "region " + i + " replica id at index " + (i * REGION_REPLICATION 
+ replicaId));
+        }
+      }
+    }
+  }
+
+  @Test
+  public void testGetRegionLocationsPagination() throws IOException {
+    try (RegionLocator locator = 
UTIL.getConnection().getRegionLocator(TABLE_NAME)) {
+      List<HRegionLocation> all = locator.getAllRegionLocations();
+      Set<String> expectedRegionNames = new HashSet<>();
+      for (HRegionLocation l : all) {
+        expectedRegionNames.add(l.getRegion().getRegionNameAsString());
+      }
+
+      Set<String> seen = new HashSet<>();
+      byte[] cursor = null;
+      int pages = 0;
+      while (true) {
+        List<HRegionLocation> page = locator.getRegionLocationsPage(cursor, 4);
+        if (page.isEmpty()) {
+          break;
+        }
+        pages++;
+        for (HRegionLocation l : page) {
+          seen.add(l.getRegion().getRegionNameAsString());
+        }
+        byte[] lastEnd = page.get(page.size() - 1).getRegion().getEndKey();
+        if (lastEnd.length == 0) {
+          break;
+        }
+        cursor = lastEnd;
+      }
+      assertEquals(expectedRegionNames, seen);
+      // 10 regions, page size 4 → exactly 3 pages: [reg0..reg3], 
[reg4..reg7], [reg8..reg9].
+      assertEquals(3, pages);
+    }
+  }
+
+  @Test
+  public void testGetRegionLocationsEmptyAfterEnd() throws IOException {
+    try (RegionLocator locator = 
UTIL.getConnection().getRegionLocator(TABLE_NAME)) {
+      // Use a startKey lexicographically after all split keys: SPLIT_KEYS go 
"1".."9", so "z".
+      List<HRegionLocation> page = 
locator.getRegionLocationsPage(Bytes.toBytes("z"), 5);
+      assertTrue(page.isEmpty(),
+        "expected empty page past the last region; got " + page.size() + " 
entries");
+    }
+  }
+
+  @Test
+  public void testGetRegionLocationsCursorMatchesAllReplicas() throws 
IOException {
+    try (RegionLocator locator = 
UTIL.getConnection().getRegionLocator(TABLE_NAME)) {
+      List<HRegionLocation> page = 
locator.getRegionLocationsPage(HConstants.EMPTY_START_ROW, 2);
+      assertEquals(2 * REGION_REPLICATION, page.size());
+      // Last REGION_REPLICATION entries are all replicas of the last region — 
same RegionInfo,
+      // so same end key regardless of which one the caller picks as the 
cursor.
+      byte[] expectedCursor = page.get(page.size() - 
1).getRegion().getEndKey();
+      for (int i = 1; i <= REGION_REPLICATION; i++) {
+        byte[] cursor = page.get(page.size() - i).getRegion().getEndKey();
+        assertArrayEquals(expectedCursor, cursor, "replica " + i + " end key 
disagrees");
+      }
+    }
+  }
+
+  @Test
+  public void testGetRegionLocationsLimitFallsBackToConfig() throws 
IOException {
+    // Default HBASE_META_SCANNER_CACHING is 100, table has 10 regions; 
limit=0 must fall back
+    // to the config and return everything in one shot.
+    try (RegionLocator locator = 
UTIL.getConnection().getRegionLocator(TABLE_NAME)) {
+      List<HRegionLocation> page = 
locator.getRegionLocationsPage(HConstants.EMPTY_START_ROW, 0);
+      assertEquals(REGION_REPLICATION * (SPLIT_KEYS.length + 1), page.size());
+    }
+  }
+
+  @Test
+  public void testGetRegionLocationsHoldsUserRegionLock() throws IOException {
+    Configuration conf = new Configuration(UTIL.getConfiguration());
+    conf.setBoolean(MetricsConnection.CLIENT_SIDE_METRICS_ENABLED_KEY, true);
+    try (
+      ConnectionImplementation conn =
+        (ConnectionImplementation) ConnectionFactory.createConnection(conf);
+      RegionLocator locator = conn.getRegionLocator(TABLE_NAME)) {
+      MetricsConnection metrics = conn.getConnectionMetrics();
+      long before = metrics.getUserRegionLockHeldTimer().getCount();
+      locator.getRegionLocationsPage(HConstants.EMPTY_START_ROW, 3);
+      long after = metrics.getUserRegionLockHeldTimer().getCount();
+      assertEquals(before + 1, after,
+        "userRegionLock held-timer should have incremented exactly once for 
the bulk" + " lookup");
+    }
+  }
+
+  /**
+   * Directly verify that the new API writes into the same {@code metaCache} 
that
+   * {@code ConnectionImplementation.locateRegionInMeta} reads from: after the 
bulk call, looking up
+   * each returned region's start key via the package-private cache accessor 
must return non-null.
+   */
+  @Test
+  public void testGetRegionLocationsPopulatesMetaCacheDirect() throws 
IOException {
+    ConnectionImplementation conn = (ConnectionImplementation) 
UTIL.getConnection();
+    conn.clearRegionCache(TABLE_NAME);
+    try (RegionLocator locator = conn.getRegionLocator(TABLE_NAME)) {
+      List<HRegionLocation> page = 
locator.getRegionLocationsPage(HConstants.EMPTY_START_ROW, 4);
+      assertEquals(4 * REGION_REPLICATION, page.size());
+      for (HRegionLocation loc : page) {
+        byte[] startKey = loc.getRegion().getStartKey();
+        RegionLocations cached = conn.getCachedLocation(TABLE_NAME, startKey);
+        assertNotNull(cached,
+          "metaCache miss for region starting at " + 
Bytes.toStringBinary(startKey)
+            + " — bulk API did not populate the same cache locateRegionInMeta 
uses");
+        HRegionLocation cachedLoc = 
cached.getRegionLocation(loc.getRegion().getReplicaId());
+        assertNotNull(cachedLoc,
+          "metaCache had region but missing replica " + 
loc.getRegion().getReplicaId());
+        assertEquals(loc.getServerName(), cachedLoc.getServerName(),
+          "cached server differs from server returned by bulk API");
+      }
+    }
+  }
+
+  /**
+   * Indirect verification of the same property: after the bulk call,
+   * {@code RegionLocator.getRegionLocation(row, useCache=true)} for a row 
inside any returned
+   * region must be served from cache — i.e. it must NOT acquire the 
user-region lock (which is only
+   * taken when {@code locateRegionInMeta} actually issues a meta RPC). This 
is the end-to-end proof
+   * that the bulk API and the single-region API share the cache.
+   */
+  @Test
+  public void testGetRegionLocationsAvoidsMetaRpcForCachedRows() throws 
IOException {
+    Configuration conf = new Configuration(UTIL.getConfiguration());
+    conf.setBoolean(MetricsConnection.CLIENT_SIDE_METRICS_ENABLED_KEY, true);
+    try (
+      ConnectionImplementation conn =
+        (ConnectionImplementation) ConnectionFactory.createConnection(conf);
+      RegionLocator locator = conn.getRegionLocator(TABLE_NAME)) {
+      conn.clearRegionCache(TABLE_NAME);
+      MetricsConnection metrics = conn.getConnectionMetrics();
+
+      List<HRegionLocation> page = 
locator.getRegionLocationsPage(HConstants.EMPTY_START_ROW, 4);
+      long afterBulk = metrics.getUserRegionLockHeldTimer().getCount();
+
+      // For each returned region, look up a row inside it via the 
single-region API. Each lookup
+      // must be a cache hit (no user-region lock acquired) because the bulk 
call already
+      // populated the shared metaCache.
+      for (HRegionLocation loc : page) {
+        // Skip non-default replicas — getRegionLocation(row) without a 
replicaId resolves only
+        // the default replica, and the cache check in locateRegionInMeta is 
per replicaId.
+        if (loc.getRegion().getReplicaId() != RegionInfo.DEFAULT_REPLICA_ID) {
+          continue;
+        }
+        byte[] startKey = loc.getRegion().getStartKey();
+        // EMPTY_START_ROW belongs to the first region; any byte works.
+        byte[] probe = startKey.length == 0 ? new byte[] { 0x00 } : startKey;
+        HRegionLocation viaCache = locator.getRegionLocation(probe, false);
+        assertEquals(loc.getServerName(), viaCache.getServerName(),
+          "single-region lookup returned a different server than the bulk API 
for "
+            + Bytes.toStringBinary(startKey));
+      }
+
+      long afterPointLookups = metrics.getUserRegionLockHeldTimer().getCount();
+      assertEquals(afterBulk, afterPointLookups,
+        "user-region lock held-timer should not have advanced — every point 
lookup should have"
+          + " been a metaCache hit, but " + (afterPointLookups - afterBulk)
+          + " meta RPCs were issued");
+    }
+  }
 }

Reply via email to