This is an automated email from the ASF dual-hosted git repository.
virajjasani pushed a commit to branch branch-3
in repository https://gitbox.apache.org/repos/asf/hbase.git
The following commit(s) were added to refs/heads/branch-3 by this push:
new 54ccfeca7fb HBASE-30161 Add paginated, single-RPC
RegionLocator.getRegionLocations(startKey, limit) API for bulk meta-cache
warmup (#8237)
54ccfeca7fb is described below
commit 54ccfeca7fbd820731ebf80b1fc91b08ec27b2de
Author: sanjeet006py <[email protected]>
AuthorDate: Wed May 27 05:47:39 2026 +0530
HBASE-30161 Add paginated, single-RPC
RegionLocator.getRegionLocations(startKey, limit) API for bulk meta-cache
warmup (#8237)
Signed-off-by: Viraj Jasani <[email protected]>
Generated-by: Claude Opus 4.7 <[email protected]>
---
.../hadoop/hbase/ClientMetaTableAccessor.java | 98 +++++++++--
.../hbase/client/AsyncNonMetaRegionLocator.java | 5 +
.../hbase/client/AsyncTableRegionLocator.java | 56 ++++++
.../hbase/client/AsyncTableRegionLocatorImpl.java | 35 ++++
.../client/ConnectionOverAsyncConnection.java | 7 +
.../apache/hadoop/hbase/client/RegionLocator.java | 54 ++++++
.../RegionLocatorOverAsyncTableRegionLocator.java | 6 +
.../hbase/client/AbstractTestRegionLocator.java | 104 +++++++++++
.../hbase/client/TestAsyncTableRegionLocator.java | 13 ++
.../hadoop/hbase/client/TestRegionLocator.java | 16 ++
.../client/TestRegionLocatorPagedScanRpcCount.java | 190 +++++++++++++++++++++
11 files changed, 570 insertions(+), 14 deletions(-)
diff --git
a/hbase-client/src/main/java/org/apache/hadoop/hbase/ClientMetaTableAccessor.java
b/hbase-client/src/main/java/org/apache/hadoop/hbase/ClientMetaTableAccessor.java
index 42bfd757e0d..6e6ddb51e02 100644
---
a/hbase-client/src/main/java/org/apache/hadoop/hbase/ClientMetaTableAccessor.java
+++
b/hbase-client/src/main/java/org/apache/hadoop/hbase/ClientMetaTableAccessor.java
@@ -168,8 +168,34 @@ public final class ClientMetaTableAccessor {
*/
public static CompletableFuture<List<HRegionLocation>>
getTableHRegionLocations(
AsyncTable<AdvancedScanResultConsumer> metaTable, TableName tableName) {
+ return toHRegionLocations(getTableRegionsAndLocations(metaTable,
tableName, true));
+ }
+
+ /**
+ * Used to get a single-RPC, paginated slice of region locations for the
specific table, starting
+ * at the meta row derived from {@code startKey} and capped at {@code
rowLimit} regions.
+ * {@code startKey} 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.
+ * @param metaTable scanner over meta table
+ * @param tableName table we're looking for
+ * @param startKey region start-key to begin scanning from (inclusive);
{@code null} or empty
+ * starts from the first region
+ * @param rowLimit maximum number of meta rows to return; if {@code <= 0},
the underlying scan is
+ * unbounded
+ * @return the list of region locations. The return value will be wrapped by
a
+ * {@link CompletableFuture}.
+ */
+ public static CompletableFuture<List<HRegionLocation>>
getTableHRegionLocations(
+ AsyncTable<AdvancedScanResultConsumer> metaTable, TableName tableName,
byte[] startKey,
+ int rowLimit) {
+ return toHRegionLocations(
+ getTableRegionsAndLocations(metaTable, tableName, true, startKey,
rowLimit));
+ }
+
+ private static CompletableFuture<List<HRegionLocation>>
+ toHRegionLocations(CompletableFuture<List<Pair<RegionInfo, ServerName>>>
source) {
CompletableFuture<List<HRegionLocation>> future = new
CompletableFuture<>();
- addListener(getTableRegionsAndLocations(metaTable, tableName, true),
(locations, err) -> {
+ addListener(source, (locations, err) -> {
if (err != null) {
future.completeExceptionally(err);
} else if (locations == null || locations.isEmpty()) {
@@ -215,6 +241,39 @@ public final class ClientMetaTableAccessor {
return future;
}
+ /**
+ * Variant of {@link #getTableRegionsAndLocations} that scans a bounded
slice of meta starting at
+ * the row derived from {@code startKey} and stopping after at most {@code
rowLimit} rows.
+ */
+ private static CompletableFuture<List<Pair<RegionInfo, ServerName>>>
getTableRegionsAndLocations(
+ final AsyncTable<AdvancedScanResultConsumer> metaTable, final TableName
tableName,
+ final boolean excludeOfflinedSplitParents, final byte[] startKey, final
int rowLimit) {
+ CompletableFuture<List<Pair<RegionInfo, ServerName>>> future = new
CompletableFuture<>();
+ if (TableName.META_TABLE_NAME.equals(tableName)) {
+ future.completeExceptionally(new IOException(
+ "This method can't be used to locate meta regions;" + " use
MetaTableLocator instead"));
+ return future;
+ }
+
+ CollectRegionLocationsVisitor visitor =
+ new CollectRegionLocationsVisitor(excludeOfflinedSplitParents);
+
+ byte[] metaStart = (startKey == null || startKey.length == 0)
+ ? getTableStartRowForMeta(tableName, QueryType.REGION)
+ : RegionInfo.createRegionName(tableName, startKey, HConstants.ZEROES,
false);
+ byte[] metaStop = getTableStopRowForMeta(tableName, QueryType.REGION);
+
+ addListener(scanMeta(metaTable, metaStart, metaStop, QueryType.REGION,
rowLimit, true, visitor),
+ (v, error) -> {
+ if (error != null) {
+ future.completeExceptionally(error);
+ return;
+ }
+ future.complete(visitor.getResults());
+ });
+ return future;
+ }
+
/**
* Performs a scan of META table for given table.
* @param metaTable scanner over meta table
@@ -225,22 +284,26 @@ public final class ClientMetaTableAccessor {
private static CompletableFuture<Void>
scanMeta(AsyncTable<AdvancedScanResultConsumer> metaTable,
TableName tableName, QueryType type, final Visitor visitor) {
return scanMeta(metaTable, getTableStartRowForMeta(tableName, type),
- getTableStopRowForMeta(tableName, type), type, Integer.MAX_VALUE,
visitor);
+ getTableStopRowForMeta(tableName, type), type, Integer.MAX_VALUE, false,
visitor);
}
/**
* Performs a scan of META table for given table.
- * @param metaTable scanner over meta table
- * @param startRow Where to start the scan
- * @param stopRow Where to stop the scan
- * @param type scanned part of meta
- * @param maxRows maximum rows to return
- * @param visitor Visitor invoked against each row
+ * @param metaTable scanner over meta table
+ * @param startRow Where to start the scan
+ * @param stopRow Where to stop the scan
+ * @param type scanned part of meta
+ * @param maxRows maximum rows to return
+ * @param isPagedScan when {@code true}, the scan is sized so the whole
slice (up to
+ * {@code maxRows}) returns in a single ScannerNext RPC.
When {@code false},
+ * uses the configured {@code hbase.meta.scanner.caching}.
+ * @param visitor Visitor invoked against each row
*/
private static CompletableFuture<Void>
scanMeta(AsyncTable<AdvancedScanResultConsumer> metaTable,
- byte[] startRow, byte[] stopRow, QueryType type, int maxRows, final
Visitor visitor) {
+ byte[] startRow, byte[] stopRow, QueryType type, int maxRows, boolean
isPagedScan,
+ final Visitor visitor) {
int rowUpperLimit = maxRows > 0 ? maxRows : Integer.MAX_VALUE;
- Scan scan = getMetaScan(metaTable, rowUpperLimit);
+ Scan scan = getMetaScan(metaTable, rowUpperLimit, isPagedScan);
for (byte[] family : type.getFamilies()) {
scan.addFamily(family);
}
@@ -437,7 +500,7 @@ public final class ClientMetaTableAccessor {
}
}
- private static Scan getMetaScan(AsyncTable<?> metaTable, int rowUpperLimit) {
+ private static Scan getMetaScan(AsyncTable<?> metaTable, int rowUpperLimit,
boolean isPagedScan) {
Scan scan = new Scan();
int scannerCaching =
metaTable.getConfiguration().getInt(HConstants.HBASE_META_SCANNER_CACHING,
HConstants.DEFAULT_HBASE_META_SCANNER_CACHING);
@@ -447,11 +510,18 @@ public final class ClientMetaTableAccessor {
) {
scan.setConsistency(Consistency.TIMELINE);
}
- if (rowUpperLimit <= 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.setLimit(rowUpperLimit);
+ scan.setCaching(rowUpperLimit);
+ } else {
+ if (rowUpperLimit <= scannerCaching) {
+ scan.setLimit(rowUpperLimit);
+ }
+ scan.setCaching(Math.min(rowUpperLimit, scannerCaching));
}
- int rows = Math.min(rowUpperLimit, scannerCaching);
- scan.setCaching(rows);
return scan;
}
diff --git
a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncNonMetaRegionLocator.java
b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncNonMetaRegionLocator.java
index e26fb837b89..29ad79fc2ea 100644
---
a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncNonMetaRegionLocator.java
+++
b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncNonMetaRegionLocator.java
@@ -578,6 +578,11 @@ class AsyncNonMetaRegionLocator {
getTableCache(loc.getRegion().getTable()).regionLocationCache.add(createRegionLocations(loc));
}
+ RegionLocations getCachedLocation(TableName tableName, byte[] startKey) {
+ TableCache tableCache = cache.get(tableName);
+ return tableCache == null ? null :
tableCache.regionLocationCache.get(startKey);
+ }
+
private HRegionLocation getCachedLocation(HRegionLocation loc) {
TableCache tableCache = cache.get(loc.getRegion().getTable());
if (tableCache == null) {
diff --git
a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncTableRegionLocator.java
b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncTableRegionLocator.java
index fffca5c2dcc..5fa702d2351 100644
---
a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncTableRegionLocator.java
+++
b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncTableRegionLocator.java
@@ -20,6 +20,7 @@ package org.apache.hadoop.hbase.client;
import java.util.List;
import java.util.concurrent.CompletableFuture;
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;
@@ -112,6 +113,61 @@ public interface AsyncTableRegionLocator {
*/
CompletableFuture<List<HRegionLocation>> getAllRegionLocations();
+ /**
+ * 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 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. The single-RPC behavior is best-effort: if the response would exceed
+ * {@code hbase.client.scanner.max.result.size} (default 2 MB), the server
may split the slice
+ * across multiple {@code ScannerNext} RPCs. For typical meta row sizes and
default caching this
+ * rarely fires, but callers passing large {@code limit} values against
clusters with replicas or
+ * heavy meta rows should treat single-RPC as a soft guarantee, not
absolute. Note that this
+ * method does not coordinate with other in-flight meta lookups on the
connection - aggregate
+ * pacing across concurrent callers is the caller's responsibility.
+ * <p/>
+ * This method is optional. Implementations that cannot support paginated
lookups will return a
+ * future that completes exceptionally with {@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} - this is a SOFT cap
on a single page, NOT
+ * "all regions"; tables larger than the cap still require
the caller to keep
+ * paging via {@code last.getRegion().getEndKey()}.
+ * @return up to {@code limit} {@link HRegionLocation}s in start-key order,
possibly empty when no
+ * more regions exist; errors are reported via the returned future
+ */
+ default CompletableFuture<List<HRegionLocation>>
getRegionLocationsPage(byte[] startKey,
+ int limit) {
+ CompletableFuture<List<HRegionLocation>> failed = new
CompletableFuture<>();
+ failed.completeExceptionally(new UnsupportedOperationException(
+ "getRegionLocationsPage(byte[], int) is not supported by this
AsyncTableRegionLocator;"
+ + " fall back to getAllRegionLocations()"));
+ return failed;
+ }
+
/**
* Gets the starting row key for every region in the currently open table.
* <p>
diff --git
a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncTableRegionLocatorImpl.java
b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncTableRegionLocatorImpl.java
index b7ec7fcd872..1f05e5eb2a2 100644
---
a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncTableRegionLocatorImpl.java
+++
b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncTableRegionLocatorImpl.java
@@ -20,10 +20,12 @@ package org.apache.hadoop.hbase.client;
import static org.apache.hadoop.hbase.trace.TraceUtil.tracedFuture;
import static org.apache.hadoop.hbase.util.FutureUtils.addListener;
+import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.apache.hadoop.hbase.ClientMetaTableAccessor;
+import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.HRegionLocation;
import org.apache.hadoop.hbase.TableName;
import org.apache.yetus.audience.InterfaceAudience;
@@ -81,6 +83,39 @@ class AsyncTableRegionLocatorImpl implements
AsyncTableRegionLocator {
.thenApply(locs -> Arrays.asList(locs.getRegionLocations()));
}
+ @Override
+ public CompletableFuture<List<HRegionLocation>>
getRegionLocationsPage(byte[] startKey,
+ int limit) {
+ return tracedFuture(() -> {
+ if (TableName.isMetaTableName(tableName)) {
+ CompletableFuture<List<HRegionLocation>> failed = new
CompletableFuture<>();
+ failed.completeExceptionally(
+ new IOException("getRegionLocationsPage(startKey, limit) is not
supported for hbase:meta;"
+ + " use getRegionLocation(EMPTY_START_ROW) instead."));
+ return failed;
+ }
+ int effectiveLimit = limit > 0
+ ? limit
+ : conn.getConfiguration().getInt(HConstants.HBASE_META_SCANNER_CACHING,
+ HConstants.DEFAULT_HBASE_META_SCANNER_CACHING);
+ CompletableFuture<List<HRegionLocation>> future =
+
ClientMetaTableAccessor.getTableHRegionLocations(conn.getTable(TableName.META_TABLE_NAME),
+ tableName, startKey, effectiveLimit);
+ addListener(future, (locs, error) -> {
+ if (error != null || locs == null) {
+ return;
+ }
+ for (HRegionLocation loc : locs) {
+ // the cache assumes that all locations have a serverName. only add
if that's true
+ if (loc.getServerName() != null) {
+
conn.getLocator().getNonMetaRegionLocator().addLocationToCache(loc);
+ }
+ }
+ });
+ return future;
+ }, getClass().getSimpleName() + ".getRegionLocationsPage");
+ }
+
@Override
public void clearRegionLocationCache() {
conn.getLocator().clearCache(tableName);
diff --git
a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/ConnectionOverAsyncConnection.java
b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/ConnectionOverAsyncConnection.java
index 471cfa87445..72812f2cae3 100644
---
a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/ConnectionOverAsyncConnection.java
+++
b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/ConnectionOverAsyncConnection.java
@@ -17,6 +17,7 @@
*/
package org.apache.hadoop.hbase.client;
+import com.google.errorprone.annotations.RestrictedApi;
import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
@@ -63,6 +64,12 @@ class ConnectionOverAsyncConnection implements Connection {
this.connConf = new ConnectionConfiguration(conn.getConfiguration());
}
+ @RestrictedApi(explanation = "Should only be called in tests", link = "",
+ allowedOnPath = ".*/src/test/.*")
+ AsyncConnectionImpl getAsyncConnection() {
+ return conn;
+ }
+
@Override
public void abort(String why, Throwable error) {
if (error != null) {
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..3a197c16e1a 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,59 @@ 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 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. The single-RPC behavior is best-effort: if the response would exceed
+ * {@code hbase.client.scanner.max.result.size} (default 2 MB), the server
may split the slice
+ * across multiple {@code ScannerNext} RPCs. For typical meta row sizes and
default caching this
+ * rarely fires, but callers passing large {@code limit} values against
clusters with replicas or
+ * heavy meta rows should treat single-RPC as a soft guarantee, not absolute.
+ * <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} - this is a SOFT cap
on a single page, NOT
+ * "all regions"; tables larger than the cap still require
the caller to keep
+ * paging via {@code last.getRegion().getEndKey()}.
+ * @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/main/java/org/apache/hadoop/hbase/client/RegionLocatorOverAsyncTableRegionLocator.java
b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/RegionLocatorOverAsyncTableRegionLocator.java
index 0cf0e0b913f..3ec5f83db57 100644
---
a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/RegionLocatorOverAsyncTableRegionLocator.java
+++
b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/RegionLocatorOverAsyncTableRegionLocator.java
@@ -62,6 +62,12 @@ class RegionLocatorOverAsyncTableRegionLocator implements
RegionLocator {
return get(locator.getAllRegionLocations());
}
+ @Override
+ public List<HRegionLocation> getRegionLocationsPage(byte[] startKey, int
limit)
+ throws IOException {
+ return get(locator.getRegionLocationsPage(startKey, limit));
+ }
+
@Override
public TableName getName() {
return locator.getName();
diff --git
a/hbase-server/src/test/java/org/apache/hadoop/hbase/client/AbstractTestRegionLocator.java
b/hbase-server/src/test/java/org/apache/hadoop/hbase/client/AbstractTestRegionLocator.java
index b234071f3b3..515d078367c 100644
---
a/hbase-server/src/test/java/org/apache/hadoop/hbase/client/AbstractTestRegionLocator.java
+++
b/hbase-server/src/test/java/org/apache/hadoop/hbase/client/AbstractTestRegionLocator.java
@@ -19,13 +19,18 @@ 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.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.hadoop.hbase.HBaseTestingUtil;
import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.HRegionLocation;
+import org.apache.hadoop.hbase.RegionLocations;
import org.apache.hadoop.hbase.ServerName;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.regionserver.Region;
@@ -41,6 +46,13 @@ public abstract class AbstractTestRegionLocator {
protected static TableName TABLE_NAME = TableName.valueOf("Locator");
+ /**
+ * Single-replica companion table used only by tests that need to assert
per-replica cache
+ * population without tripping over the multi-replica
+ * {@link AsyncNonMetaRegionLocator#addLocationToCache(HRegionLocation)}
merge limitation.
+ */
+ protected static TableName TABLE_NAME_NO_REPLICA =
TableName.valueOf("LocatorNoReplica");
+
protected static byte[] FAMILY = Bytes.toBytes("family");
protected static int REGION_REPLICATION = 3;
@@ -59,6 +71,10 @@ public abstract class AbstractTestRegionLocator {
}
UTIL.getAdmin().createTable(td, SPLIT_KEYS);
UTIL.waitTableAvailable(TABLE_NAME);
+ TableDescriptor tdNoReplica =
TableDescriptorBuilder.newBuilder(TABLE_NAME_NO_REPLICA)
+ .setColumnFamily(ColumnFamilyDescriptorBuilder.of(FAMILY)).build();
+ UTIL.getAdmin().createTable(tdNoReplica, SPLIT_KEYS);
+ UTIL.waitTableAvailable(TABLE_NAME_NO_REPLICA);
try (ConnectionRegistry registry =
ConnectionRegistryFactory.create(UTIL.getConfiguration(),
User.getCurrent())) {
RegionReplicaTestHelper.waitUntilAllMetaReplicasAreReady(UTIL, registry);
@@ -69,6 +85,7 @@ public abstract class AbstractTestRegionLocator {
@AfterEach
public void tearDownAfterTest() throws IOException {
clearCache(TABLE_NAME);
+ clearCache(TABLE_NAME_NO_REPLICA);
clearCache(TableName.META_TABLE_NAME);
}
@@ -160,6 +177,87 @@ public abstract class AbstractTestRegionLocator {
}
}
+ @Test
+ public void testGetRegionLocationsFirstPage() throws IOException {
+ List<HRegionLocation> page = getRegionLocationsPage(TABLE_NAME, null, 2);
+ assertEquals(2 * REGION_REPLICATION, page.size());
+ for (int i = 0; i < 2; i++) {
+ for (int replicaId = 0; replicaId < REGION_REPLICATION; replicaId++) {
+ assertRegionLocation(page.get(i * REGION_REPLICATION + replicaId), i,
replicaId);
+ }
+ }
+ }
+
+ @Test
+ public void testGetRegionLocationsPagination() throws IOException {
+ int pageSize = 3;
+ int totalRegions = SPLIT_KEYS.length + 1;
+ List<HRegionLocation> all = new ArrayList<>();
+ byte[] cursor = null;
+ while (true) {
+ List<HRegionLocation> page = getRegionLocationsPage(TABLE_NAME, cursor,
pageSize);
+ if (page.isEmpty()) {
+ break;
+ }
+ all.addAll(page);
+ HRegionLocation last = page.get(page.size() - 1);
+ byte[] endKey = last.getRegion().getEndKey();
+ if (endKey.length == 0) {
+ break;
+ }
+ cursor = endKey;
+ }
+ assertEquals(totalRegions * REGION_REPLICATION, all.size());
+ for (int i = 0; i <= SPLIT_KEYS.length; i++) {
+ for (int replicaId = 0; replicaId < REGION_REPLICATION; replicaId++) {
+ assertRegionLocation(all.get(i * REGION_REPLICATION + replicaId), i,
replicaId);
+ }
+ }
+ }
+
+ @Test
+ public void testGetRegionLocationsEmptyAfterEnd() throws IOException {
+ // Use a startKey lexicographically after all split keys: SPLIT_KEYS go
"1".."9", so "z".
+ List<HRegionLocation> page = getRegionLocationsPage(TABLE_NAME,
Bytes.toBytes("z"), 5);
+ assertTrue(page.isEmpty(),
+ "expected empty page past the last region; got " + page.size() + "
entries");
+ }
+
+ @Test
+ public void testGetRegionLocationsLimitFallsBackToConfig() throws
IOException {
+ // mini-cluster default for hbase.meta.scanner.caching is well above
SPLIT_KEYS.length+1, so
+ // limit<=0 must return every region.
+ List<HRegionLocation> page = getRegionLocationsPage(TABLE_NAME, null, 0);
+ assertEquals((SPLIT_KEYS.length + 1) * REGION_REPLICATION, page.size());
+ page = getRegionLocationsPage(TABLE_NAME, null, -1);
+ assertEquals((SPLIT_KEYS.length + 1) * REGION_REPLICATION, page.size());
+ }
+
+ @Test
+ public void testGetRegionLocationsRejectsMeta() {
+ assertThrows(IOException.class,
+ () -> getRegionLocationsPage(TableName.META_TABLE_NAME,
HConstants.EMPTY_START_ROW, 1));
+ }
+
+ @Test
+ public void testGetRegionLocationsPopulatesCache() throws IOException {
+ // Use the single-replica companion table so the multi-replica
+ // addLocationToCache merge limitation does not interfere with this test.
+ clearCache(TABLE_NAME_NO_REPLICA);
+ List<HRegionLocation> page = getRegionLocationsPage(TABLE_NAME_NO_REPLICA,
null, 3);
+ assertEquals(3, page.size());
+ for (HRegionLocation loc : page) {
+ byte[] startKey = loc.getRegion().getStartKey();
+ RegionLocations cached = getCachedLocation(TABLE_NAME_NO_REPLICA,
startKey);
+ assertNotNull(cached, "metaCache miss for region starting at "
+ + Bytes.toStringBinary(startKey) + " — bulk API did not populate the
cache");
+ HRegionLocation cachedLoc =
cached.getRegionLocation(RegionInfo.DEFAULT_REPLICA_ID);
+ assertNotNull(cachedLoc, "metaCache had region but missing default
replica entry");
+ assertEquals(loc.getServerName(), cachedLoc.getServerName(),
+ "cached server differs from server returned by bulk API");
+ }
+ }
+
private void assertMetaStartOrEndKeys(byte[][] keys) {
assertEquals(1, keys.length);
assertArrayEquals(HConstants.EMPTY_BYTE_ARRAY, keys[0]);
@@ -215,5 +313,11 @@ public abstract class AbstractTestRegionLocator {
protected abstract List<HRegionLocation> getAllRegionLocations(TableName
tableName)
throws IOException;
+ protected abstract List<HRegionLocation> getRegionLocationsPage(TableName
tableName,
+ byte[] startKey, int limit) throws IOException;
+
+ protected abstract RegionLocations getCachedLocation(TableName tableName,
byte[] startKey)
+ throws IOException;
+
protected abstract void clearCache(TableName tableName) throws IOException;
}
diff --git
a/hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestAsyncTableRegionLocator.java
b/hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestAsyncTableRegionLocator.java
index 7f29fa5f665..7113f489c90 100644
---
a/hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestAsyncTableRegionLocator.java
+++
b/hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestAsyncTableRegionLocator.java
@@ -22,6 +22,7 @@ import static org.apache.hadoop.hbase.util.FutureUtils.get;
import java.io.IOException;
import java.util.List;
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;
@@ -91,6 +92,18 @@ public class TestAsyncTableRegionLocator extends
AbstractTestRegionLocator {
return get(CONN.getRegionLocator(tableName).getAllRegionLocations());
}
+ @Override
+ protected List<HRegionLocation> getRegionLocationsPage(TableName tableName,
byte[] startKey,
+ int limit) throws IOException {
+ return
get(CONN.getRegionLocator(tableName).getRegionLocationsPage(startKey, limit));
+ }
+
+ @Override
+ protected RegionLocations getCachedLocation(TableName tableName, byte[]
startKey) {
+ return ((AsyncConnectionImpl) CONN).getLocator().getNonMetaRegionLocator()
+ .getCachedLocation(tableName, startKey);
+ }
+
@Override
protected void clearCache(TableName tableName) throws IOException {
CONN.getRegionLocator(tableName).clearRegionLocationCache();
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..165b6624aef 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
@@ -20,6 +20,7 @@ package org.apache.hadoop.hbase.client;
import java.io.IOException;
import java.util.List;
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;
@@ -86,6 +87,21 @@ public class TestRegionLocator extends
AbstractTestRegionLocator {
}
}
+ @Override
+ protected List<HRegionLocation> getRegionLocationsPage(TableName tableName,
byte[] startKey,
+ int limit) throws IOException {
+ try (RegionLocator locator =
UTIL.getConnection().getRegionLocator(tableName)) {
+ return locator.getRegionLocationsPage(startKey, limit);
+ }
+ }
+
+ @Override
+ protected RegionLocations getCachedLocation(TableName tableName, byte[]
startKey)
+ throws IOException {
+ return ((ConnectionOverAsyncConnection)
UTIL.getConnection()).getAsyncConnection().getLocator()
+ .getNonMetaRegionLocator().getCachedLocation(tableName, startKey);
+ }
+
@Override
protected void clearCache(TableName tableName) throws IOException {
try (RegionLocator locator =
UTIL.getConnection().getRegionLocator(tableName)) {
diff --git
a/hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestRegionLocatorPagedScanRpcCount.java
b/hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestRegionLocatorPagedScanRpcCount.java
new file mode 100644
index 00000000000..6c63ec7cb03
--- /dev/null
+++
b/hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestRegionLocatorPagedScanRpcCount.java
@@ -0,0 +1,190 @@
+/*
+ * 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.client;
+
+import static org.apache.hadoop.hbase.util.FutureUtils.get;
+import static org.junit.Assert.assertEquals;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.hadoop.hbase.ClientMetaTableAccessor;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.HBaseTestingUtil;
+import org.apache.hadoop.hbase.HConstants;
+import org.apache.hadoop.hbase.HRegionLocation;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.client.metrics.ScanMetrics;
+import org.apache.hadoop.hbase.testclassification.ClientTests;
+import org.apache.hadoop.hbase.testclassification.MediumTests;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import org.apache.hbase.thirdparty.com.google.common.io.Closeables;
+
+/**
+ * Asserts the single-RPC promise of the paginated meta-scan path
+ * ({@link ClientMetaTableAccessor#getTableHRegionLocations(AsyncTable,
TableName, byte[], int)}) by
+ * wrapping the meta {@link AsyncTable} so we can count {@link
AdvancedScanResultConsumer#onNext}
+ * invocations - one per ScannerNext server response.
+ * <p/>
+ * Cluster runs with {@code hbase.meta.scanner.caching = 2} so the {@code
limit > caching} branch is
+ * exercised cheaply with a small table (5 user regions).
+ */
+@Category({ MediumTests.class, ClientTests.class })
+public class TestRegionLocatorPagedScanRpcCount {
+
+ @ClassRule
+ public static final HBaseClassTestRule CLASS_RULE =
+ HBaseClassTestRule.forClass(TestRegionLocatorPagedScanRpcCount.class);
+
+ private static final HBaseTestingUtil UTIL = new HBaseTestingUtil();
+ private static final TableName TABLE_NAME =
TableName.valueOf("LocatorPaged");
+ private static final byte[] FAMILY = Bytes.toBytes("family");
+
+ /** Caching is small enough that {@code limit > META_CACHING} is easy to set
up. */
+ private static final int META_CACHING = 2;
+ private static final int NUM_REGIONS = 5;
+
+ private static AsyncConnection CONN;
+
+ @BeforeClass
+ public static void setUp() throws Exception {
+ UTIL.getConfiguration().setInt(HConstants.HBASE_META_SCANNER_CACHING,
META_CACHING);
+ UTIL.startMiniCluster(1);
+ byte[][] splitKeys = new byte[NUM_REGIONS - 1][];
+ for (int i = 0; i < splitKeys.length; i++) {
+ splitKeys[i] = Bytes.toBytes(Integer.toString(i + 1));
+ }
+ TableDescriptor td = TableDescriptorBuilder.newBuilder(TABLE_NAME)
+ .setColumnFamily(ColumnFamilyDescriptorBuilder.of(FAMILY)).build();
+ UTIL.getAdmin().createTable(td, splitKeys);
+ UTIL.waitTableAvailable(TABLE_NAME);
+ UTIL.getAdmin().balancerSwitch(false, true);
+ CONN =
ConnectionFactory.createAsyncConnection(UTIL.getConfiguration()).get();
+ }
+
+ @AfterClass
+ public static void tearDown() throws Exception {
+ Closeables.close(CONN, true);
+ UTIL.shutdownMiniCluster();
+ }
+
+ @Test
+ public void testSingleRpcWhenLimitWithinCaching() throws Exception {
+ // limit (2) <= caching (2): trivially one ScannerNext. Baseline.
+ int rpcs = runPagedScanAndCountRpcs(2);
+ assertEquals("expected exactly one ScannerNext RPC for limit <= caching",
1, rpcs);
+ }
+
+ @Test
+ public void testSingleRpcWhenLimitExceedsCaching() throws Exception {
+ // limit (5) > caching (2): without the isPagedScan fix this would be
ceil(5/2) = 3
+ // ScannerNext RPCs. With the fix, getMetaScan sizes caching to limit -> 1
RPC.
+ int rpcs = runPagedScanAndCountRpcs(NUM_REGIONS);
+ assertEquals("expected exactly one ScannerNext RPC for paged scan even
when limit > caching", 1,
+ rpcs);
+ }
+
+ @Test
+ public void testUnboundedPathStillUsesConfiguredCaching() throws Exception {
+ // The unbounded getTableHRegionLocations(metaTable, tableName) overload
(no limit) must
+ // continue to use the configured caching (META_CACHING=2). For
NUM_REGIONS=5 user regions,
+ // expect ceil(5 / 2) = 3 ScannerNext batches plus possibly a final empty
batch when the
+ // server-side scan reaches the stopRow. Assert it is strictly more than
one to prove the
+ // isPagedScan flag did not bleed into the unbounded path.
+ AtomicInteger onNextCalls = new AtomicInteger();
+ AsyncTable<AdvancedScanResultConsumer> metaTable =
wrapMetaTable(onNextCalls);
+ List<HRegionLocation> all =
+ get(ClientMetaTableAccessor.getTableHRegionLocations(metaTable,
TABLE_NAME));
+ assertEquals(NUM_REGIONS, all.size());
+ int rpcs = onNextCalls.get();
+ assertEquals(
+ "unbounded scan should still split across "
+ + "ceil(NUM_REGIONS / caching) ScannerNext batches; got " + rpcs,
+ (int) Math.ceil((double) NUM_REGIONS / META_CACHING), rpcs);
+ }
+
+ private int runPagedScanAndCountRpcs(int limit) throws Exception {
+ AtomicInteger onNextCalls = new AtomicInteger();
+ AsyncTable<AdvancedScanResultConsumer> metaTable =
wrapMetaTable(onNextCalls);
+ List<HRegionLocation> page =
+ get(ClientMetaTableAccessor.getTableHRegionLocations(metaTable,
TABLE_NAME, null, limit));
+ assertEquals("paged call returned wrong number of regions", limit,
page.size());
+ return onNextCalls.get();
+ }
+
+ /**
+ * Returns a delegating proxy for the meta {@link AsyncTable} that intercepts
+ * {@code scan(Scan, AdvancedScanResultConsumer)} and wraps the supplied
consumer so every
+ * {@link AdvancedScanResultConsumer#onNext} invocation increments {@code
onNextCalls}.
+ */
+ @SuppressWarnings("unchecked")
+ private static AsyncTable<AdvancedScanResultConsumer>
wrapMetaTable(AtomicInteger onNextCalls) {
+ AsyncTable<AdvancedScanResultConsumer> delegate =
CONN.getTable(TableName.META_TABLE_NAME);
+ InvocationHandler handler = new InvocationHandler() {
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws
Throwable {
+ if (
+ "scan".equals(method.getName()) && args != null && args.length == 2
+ && args[1] instanceof AdvancedScanResultConsumer
+ ) {
+ Scan scan = (Scan) args[0];
+ AdvancedScanResultConsumer original = (AdvancedScanResultConsumer)
args[1];
+ AdvancedScanResultConsumer counting = new
AdvancedScanResultConsumer() {
+ @Override
+ public void onNext(Result[] results, ScanController controller) {
+ onNextCalls.incrementAndGet();
+ original.onNext(results, controller);
+ }
+
+ @Override
+ public void onError(Throwable error) {
+ original.onError(error);
+ }
+
+ @Override
+ public void onComplete() {
+ original.onComplete();
+ }
+
+ @Override
+ public void onHeartbeat(ScanController controller) {
+ original.onHeartbeat(controller);
+ }
+
+ @Override
+ public void onScanMetricsCreated(ScanMetrics scanMetrics) {
+ original.onScanMetricsCreated(scanMetrics);
+ }
+ };
+ return method.invoke(delegate, scan, counting);
+ }
+ return method.invoke(delegate, args);
+ }
+ };
+ return (AsyncTable<AdvancedScanResultConsumer>) Proxy.newProxyInstance(
+ AsyncTable.class.getClassLoader(), new Class<?>[] { AsyncTable.class },
handler);
+ }
+}