Skip to content

Commit 8b40f9d

Browse files
committed
HBASE-26783 ScannerCallable doubly clears meta cache on retries
1 parent acc7299 commit 8b40f9d

File tree

5 files changed

+320
-73
lines changed

5 files changed

+320
-73
lines changed

hbase-client/src/main/java/org/apache/hadoop/hbase/client/ReversedScannerCallable.java

Lines changed: 54 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@
2323

2424
import java.io.IOException;
2525
import java.io.InterruptedIOException;
26-
import java.util.ArrayList;
27-
import java.util.List;
2826

2927
import org.apache.hadoop.hbase.DoNotRetryIOException;
3028
import org.apache.hadoop.hbase.HConstants;
3129
import org.apache.hadoop.hbase.HRegionLocation;
3230
import org.apache.hadoop.hbase.RegionLocations;
3331
import org.apache.hadoop.hbase.TableName;
3432
import org.apache.hadoop.hbase.classification.InterfaceAudience;
33+
import org.apache.hadoop.hbase.TableNotEnabledException;
34+
import org.apache.hadoop.hbase.util.Pair;
3535
import org.apache.hadoop.hbase.client.metrics.ScanMetrics;
3636
import org.apache.hadoop.hbase.ipc.RpcControllerFactory;
3737
import org.apache.hadoop.hbase.util.Bytes;
@@ -43,6 +43,8 @@
4343
@InterfaceAudience.Private
4444
public class ReversedScannerCallable extends ScannerCallable {
4545

46+
private byte[] locationSearchKey;
47+
4648
/**
4749
* @param connection
4850
* @param tableName
@@ -70,6 +72,18 @@ public ReversedScannerCallable(ClusterConnection connection, TableName tableName
7072
super(connection, tableName, scan, scanMetrics, rpcFactory, replicaId);
7173
}
7274

75+
@Override
76+
public void throwable(Throwable t, boolean retrying) {
77+
// for reverse scans, we need to update cache using the search key found for the reverse scan
78+
// range in prepare. Otherwise, we will see weird behavior at the table boundaries,
79+
// when trying to clear cache for an empty row.
80+
if (location != null && locationSearchKey != null) {
81+
getConnection().updateCachedLocations(getTableName(),
82+
location.getRegionInfo().getRegionName(),
83+
locationSearchKey, t, location.getServerName());
84+
}
85+
}
86+
7387
/**
7488
* @param reload force reload of server location
7589
* @throws IOException
@@ -79,34 +93,37 @@ public void prepare(boolean reload) throws IOException {
7993
if (Thread.interrupted()) {
8094
throw new InterruptedIOException();
8195
}
96+
97+
if (reload && getTableName() != null && !getTableName().equals(TableName.META_TABLE_NAME)
98+
&& getConnection().isTableDisabled(getTableName())) {
99+
throw new TableNotEnabledException(getTableName().getNameAsString() + " is disabled.");
100+
}
101+
82102
if (!instantiated || reload) {
83103
// we should use range locate if
84104
// 1. we do not want the start row
85105
// 2. the start row is empty which means we need to locate to the last region.
86106
if (scan.includeStartRow() && !isEmptyStartRow(getRow())) {
87107
// Just locate the region with the row
88-
RegionLocations rl = RpcRetryingCallerWithReadReplicas.getRegionLocations(!reload, id,
89-
getConnection(), getTableName(), getRow());
90-
this.location = id < rl.size() ? rl.getRegionLocation(id) : null;
91-
if (location == null || location.getServerName() == null) {
92-
throw new IOException("Failed to find location, tableName="
93-
+ tableName + ", row=" + Bytes.toStringBinary(row) + ", reload="
94-
+ reload);
95-
}
108+
RegionLocations rl = getRegionLocationsForPrepare(getRow());
109+
this.location = getLocationForReplica(rl);
110+
this.locationSearchKey = getRow();
96111
} else {
97-
// Need to locate the regions with the range, and the target location is
98-
// the last one which is the previous region of last region scanner
112+
// The locateStart row is an approximation. So we need to search between
113+
// that and the actual row in order to really find the last region
99114
byte[] locateStartRow = createCloseRowBefore(getRow());
100-
List<HRegionLocation> locatedRegions = locateRegionsInRange(
101-
locateStartRow, row, reload);
102-
if (locatedRegions.isEmpty()) {
103-
throw new DoNotRetryIOException(
104-
"Does hbase:meta exist hole? Couldn't get regions for the range from "
105-
+ Bytes.toStringBinary(locateStartRow) + " to "
106-
+ Bytes.toStringBinary(row));
107-
}
108-
this.location = locatedRegions.get(locatedRegions.size() - 1);
115+
Pair<HRegionLocation, byte[]> lastRegionAndKey = locateLastRegionInRange(
116+
locateStartRow, getRow());
117+
this.location = lastRegionAndKey.getFirst();
118+
this.locationSearchKey = lastRegionAndKey.getSecond();
109119
}
120+
121+
if (location == null || location.getServerName() == null) {
122+
throw new IOException("Failed to find location, tableName="
123+
+ getTableName() + ", row=" + Bytes.toStringBinary(getRow()) + ", reload="
124+
+ reload);
125+
}
126+
110127
setStub(getConnection().getClient(getLocation().getServerName()));
111128
checkIfRegionServerIsRemote();
112129
instantiated = true;
@@ -124,33 +141,32 @@ public void prepare(boolean reload) throws IOException {
124141
}
125142

126143
/**
127-
* Get the corresponding regions for an arbitrary range of keys.
144+
* Get the last region before the endkey, which will be used to execute the reverse scan
128145
* @param startKey Starting row in range, inclusive
129146
* @param endKey Ending row in range, exclusive
130-
* @param reload force reload of server location
131-
* @return A list of HRegionLocation corresponding to the regions that contain
132-
* the specified range
133-
* @throws IOException
147+
* @return The last location, and the rowKey used to find it. May be null,
148+
* if a region could not be found.
134149
*/
135-
@edu.umd.cs.findbugs.annotations.SuppressWarnings(value="NP_NULL_ON_SOME_PATH",
136-
justification="I thought I'd fixed it but FB still complains; see below")
137-
private List<HRegionLocation> locateRegionsInRange(byte[] startKey,
138-
byte[] endKey, boolean reload) throws IOException {
150+
private Pair<HRegionLocation, byte[]> locateLastRegionInRange(byte[] startKey, byte[] endKey)
151+
throws IOException {
139152
final boolean endKeyIsEndOfTable = Bytes.equals(endKey,
140153
HConstants.EMPTY_END_ROW);
141154
if ((Bytes.compareTo(startKey, endKey) > 0) && !endKeyIsEndOfTable) {
142155
throw new IllegalArgumentException("Invalid range: "
143156
+ Bytes.toStringBinary(startKey) + " > "
144157
+ Bytes.toStringBinary(endKey));
145158
}
146-
List<HRegionLocation> regionList = new ArrayList<HRegionLocation>();
159+
160+
HRegionLocation lastRegion = null;
161+
byte[] lastFoundKey = null;
147162
byte[] currentKey = startKey;
163+
148164
do {
149-
RegionLocations rl = RpcRetryingCallerWithReadReplicas.getRegionLocations(!reload, id,
150-
getConnection(), getTableName(), currentKey);
151-
HRegionLocation regionLocation = id < rl.size() ? rl.getRegionLocation(id) : null;
152-
if (regionLocation != null && regionLocation.getRegionInfo().containsRow(currentKey)) {
153-
regionList.add(regionLocation);
165+
RegionLocations rl = getRegionLocationsForPrepare(currentKey);
166+
HRegionLocation regionLocation = getLocationForReplica(rl);
167+
if (regionLocation.getRegionInfo().containsRow(currentKey)) {
168+
lastFoundKey = currentKey;
169+
lastRegion = regionLocation;
154170
} else {
155171
// FindBugs: NP_NULL_ON_SOME_PATH Complaining about regionLocation
156172
throw new DoNotRetryIOException("Does hbase:meta exist hole? Locating row "
@@ -160,7 +176,8 @@ private List<HRegionLocation> locateRegionsInRange(byte[] startKey,
160176
currentKey = regionLocation.getRegionInfo().getEndKey();
161177
} while (!Bytes.equals(currentKey, HConstants.EMPTY_END_ROW)
162178
&& (endKeyIsEndOfTable || Bytes.compareTo(currentKey, endKey) < 0));
163-
return regionList;
179+
180+
return new Pair<>(lastRegion, lastFoundKey);
164181
}
165182

166183
@Override

hbase-client/src/main/java/org/apache/hadoop/hbase/client/ScannerCallable.java

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.apache.hadoop.hbase.RegionLocations;
4141
import org.apache.hadoop.hbase.ServerName;
4242
import org.apache.hadoop.hbase.TableName;
43+
import org.apache.hadoop.hbase.TableNotEnabledException;
4344
import org.apache.hadoop.hbase.UnknownScannerException;
4445
import org.apache.hadoop.hbase.classification.InterfaceAudience;
4546
import org.apache.hadoop.hbase.client.metrics.ScanMetrics;
@@ -147,6 +148,32 @@ HBaseRpcController getController() {
147148
return controller;
148149
}
149150

151+
protected final HRegionLocation getLocationForReplica(RegionLocations locs)
152+
throws HBaseIOException {
153+
HRegionLocation loc = id < locs.size() ? locs.getRegionLocation(id) : null;
154+
if (loc == null || loc.getServerName() == null) {
155+
// With this exception, there will be a retry. The location can be null for a replica
156+
// when the table is created or after a split.
157+
throw new HBaseIOException("There is no location for replica id #" + id);
158+
}
159+
return loc;
160+
}
161+
162+
/**
163+
* Fetch region locations for the row. Since this is for prepare, we always useCache.
164+
* This is because we can be sure that RpcRetryingCaller will have cleared the cache
165+
* in error handling if this is a retry.
166+
*
167+
* @param row the row to look up region location for
168+
*/
169+
protected final RegionLocations getRegionLocationsForPrepare(byte[] row)
170+
throws IOException {
171+
// always use cache, because cache will have been cleared if necessary
172+
// in the try/catch before retrying
173+
return RpcRetryingCallerWithReadReplicas.getRegionLocations(true, id, getConnection(),
174+
getTableName(), row);
175+
}
176+
150177
/**
151178
* @param reload force reload of server location
152179
* @throws IOException
@@ -156,14 +183,14 @@ public void prepare(boolean reload) throws IOException {
156183
if (Thread.interrupted()) {
157184
throw new InterruptedIOException();
158185
}
159-
RegionLocations rl = RpcRetryingCallerWithReadReplicas.getRegionLocations(!reload,
160-
id, getConnection(), getTableName(), getRow());
161-
location = id < rl.size() ? rl.getRegionLocation(id) : null;
162-
if (location == null || location.getServerName() == null) {
163-
// With this exception, there will be a retry. The location can be null for a replica
164-
// when the table is created or after a split.
165-
throw new HBaseIOException("There is no location for replica id #" + id);
186+
187+
if (reload && getTableName() != null && !getTableName().equals(TableName.META_TABLE_NAME)
188+
&& getConnection().isTableDisabled(getTableName())) {
189+
throw new TableNotEnabledException(getTableName().getNameAsString() + " is disabled.");
166190
}
191+
192+
RegionLocations rl = getRegionLocationsForPrepare(getRow());
193+
location = getLocationForReplica(rl);
167194
ServerName dest = location.getServerName();
168195
setStub(super.getConnection().getClient(dest));
169196
if (!instantiated || reload) {

hbase-client/src/test/java/org/apache/hadoop/hbase/client/TestReversedScannerCallable.java

Lines changed: 65 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,22 @@
1717
*/
1818
package org.apache.hadoop.hbase.client;
1919

20+
import static org.junit.Assert.fail;
21+
import static org.mockito.Mockito.mock;
22+
import static org.mockito.Mockito.times;
23+
import static org.mockito.Mockito.verify;
24+
import static org.mockito.Mockito.when;
25+
26+
import java.io.IOException;
27+
2028
import org.apache.hadoop.conf.Configuration;
29+
import org.apache.hadoop.hbase.HConstants;
2130
import org.apache.hadoop.hbase.HRegionInfo;
2231
import org.apache.hadoop.hbase.HRegionLocation;
2332
import org.apache.hadoop.hbase.RegionLocations;
2433
import org.apache.hadoop.hbase.ServerName;
2534
import org.apache.hadoop.hbase.TableName;
35+
import org.apache.hadoop.hbase.TableNotEnabledException;
2636
import org.apache.hadoop.hbase.ipc.RpcControllerFactory;
2737
import org.apache.hadoop.hbase.testclassification.ClientTests;
2838
import org.apache.hadoop.hbase.testclassification.SmallTests;
@@ -32,64 +42,90 @@
3242
import org.junit.experimental.categories.Category;
3343
import org.junit.runner.RunWith;
3444
import org.mockito.Mock;
35-
import org.mockito.Mockito;
3645
import org.mockito.runners.MockitoJUnitRunner;
3746

3847
@RunWith(MockitoJUnitRunner.class)
3948
@Category({ ClientTests.class, SmallTests.class })
4049
public class TestReversedScannerCallable {
4150

51+
private static final TableName TABLE_NAME = TableName.valueOf("TestReversedScannerCallable");
52+
53+
private static final String HOSTNAME = "localhost";
54+
private static final ServerName SERVERNAME = ServerName.valueOf(HOSTNAME, 60030, 123);
55+
private static final byte[] ROW = Bytes.toBytes("row1");
56+
private static final Scan DEFAULT_SCAN = new Scan().withStartRow(ROW, true).setReversed(true);
57+
4258
@Mock
4359
private ClusterConnection connection;
4460
@Mock
45-
private Scan scan;
46-
@Mock
4761
private RpcControllerFactory rpcFactory;
4862
@Mock
4963
private RegionLocations regionLocations;
50-
51-
private final byte[] ROW = Bytes.toBytes("row1");
64+
@Mock
65+
private HRegionLocation regionLocation;
5266

5367
@Before
5468
public void setUp() throws Exception {
55-
byte[] ROW_BEFORE = ConnectionUtils.createCloseRowBefore(ROW);
56-
HRegionLocation regionLocation = Mockito.mock(HRegionLocation.class);
57-
ServerName serverName = Mockito.mock(ServerName.class);
58-
HRegionInfo regionInfo = Mockito.mock(HRegionInfo.class);
59-
60-
Mockito.when(connection.getConfiguration()).thenReturn(new Configuration());
61-
Mockito.when(regionLocations.size()).thenReturn(1);
62-
Mockito.when(regionLocations.getRegionLocation(0)).thenReturn(regionLocation);
63-
Mockito.when(regionLocation.getHostname()).thenReturn("localhost");
64-
Mockito.when(regionLocation.getRegionInfo()).thenReturn(regionInfo);
65-
Mockito.when(regionLocation.getServerName()).thenReturn(serverName);
66-
Mockito.when(regionInfo.containsRow(ROW_BEFORE)).thenReturn(true);
67-
Mockito.when(scan.includeStartRow()).thenReturn(true);
68-
Mockito.when(scan.getStartRow()).thenReturn(ROW);
69+
when(connection.getConfiguration()).thenReturn(new Configuration());
70+
when(regionLocations.size()).thenReturn(1);
71+
when(regionLocations.getRegionLocation(0)).thenReturn(regionLocation);
72+
when(regionLocation.getHostname()).thenReturn(HOSTNAME);
73+
when(regionLocation.getServerName()).thenReturn(SERVERNAME);
6974
}
7075

7176
@Test
72-
public void testPrepareDoesNotUseCache() throws Exception {
73-
TableName tableName = TableName.valueOf("MyTable");
74-
Mockito.when(connection.relocateRegion(tableName, ROW, 0)).thenReturn(regionLocations);
77+
public void testPrepareAlwaysUsesCache() throws Exception {
78+
when(connection.locateRegion(TABLE_NAME, ROW, true, true, 0))
79+
.thenReturn(regionLocations);
7580

7681
ReversedScannerCallable callable =
77-
new ReversedScannerCallable(connection, tableName, scan, null, rpcFactory);
82+
new ReversedScannerCallable(connection, TABLE_NAME, DEFAULT_SCAN, null, rpcFactory, 0);
83+
callable.prepare(false);
7884
callable.prepare(true);
7985

80-
Mockito.verify(connection).relocateRegion(tableName, ROW, 0);
86+
verify(connection, times(2)).locateRegion(TABLE_NAME, ROW, true, true, 0);
87+
}
88+
89+
@Test
90+
public void testHandleDisabledTable() throws IOException {
91+
when(connection.isTableDisabled(TABLE_NAME)).thenReturn(true);
92+
93+
ReversedScannerCallable callable =
94+
new ReversedScannerCallable(connection, TABLE_NAME, DEFAULT_SCAN, null, rpcFactory, 0);
95+
96+
try {
97+
callable.prepare(true);
98+
fail("should have thrown TableNotEnabledException");
99+
} catch (TableNotEnabledException e) {
100+
// pass
101+
}
81102
}
82103

83104
@Test
84-
public void testPrepareUsesCache() throws Exception {
85-
TableName tableName = TableName.valueOf("MyTable");
86-
Mockito.when(connection.locateRegion(tableName, ROW, true, true, 0))
105+
public void testUpdateSearchKeyCacheLocation() throws IOException {
106+
byte[] regionName = HRegionInfo.createRegionName(TABLE_NAME,
107+
ConnectionUtils.createCloseRowBefore(ConnectionUtils.MAX_BYTE_ARRAY), "123", false);
108+
HRegionInfo mockRegionInfo = mock(HRegionInfo.class);
109+
when(mockRegionInfo.containsRow(ConnectionUtils.MAX_BYTE_ARRAY)).thenReturn(true);
110+
when(mockRegionInfo.getEndKey()).thenReturn(HConstants.EMPTY_END_ROW);
111+
when(mockRegionInfo.getRegionName()).thenReturn(regionName);
112+
when(regionLocation.getRegionInfo()).thenReturn(mockRegionInfo);
113+
114+
IOException testThrowable = new IOException("test throwable");
115+
116+
when(connection.locateRegion(TABLE_NAME, ConnectionUtils.MAX_BYTE_ARRAY, true, true, 0))
87117
.thenReturn(regionLocations);
88118

119+
Scan scan = new Scan().setReversed(true);
89120
ReversedScannerCallable callable =
90-
new ReversedScannerCallable(connection, tableName, scan, null, rpcFactory);
121+
new ReversedScannerCallable(connection, TABLE_NAME, scan, null, rpcFactory, 0);
122+
91123
callable.prepare(false);
92124

93-
Mockito.verify(connection).locateRegion(tableName, ROW, true, true, 0);
125+
callable.throwable(testThrowable, true);
126+
127+
verify(connection).updateCachedLocations(TABLE_NAME, regionName, ConnectionUtils.MAX_BYTE_ARRAY, testThrowable, SERVERNAME);
128+
129+
94130
}
95131
}

0 commit comments

Comments
 (0)