This is an automated email from the ASF dual-hosted git repository.
virajjasani pushed a commit to branch branch-2
in repository https://gitbox.apache.org/repos/asf/hbase.git
The following commit(s) were added to refs/heads/branch-2 by this push:
new 30a8ecca7f7 HBASE-30161 Add paginated, single-RPC
RegionLocator.getRegionLocations(startKey, limit) API for bulk meta-cache
warmup (#8236) (#8237)
30a8ecca7f7 is described below
commit 30a8ecca7f760eb4757bb3a18e587f6f0a48ea17
Author: sanjeet006py <[email protected]>
AuthorDate: Wed May 27 05:49:28 2026 +0530
HBASE-30161 Add paginated, single-RPC
RegionLocator.getRegionLocations(startKey, limit) API for bulk meta-cache
warmup (#8236) (#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 e50f57b9fca..66975cde622 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 <= 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");
+ }
+ }
}