Skip to content

Commit 41be021

Browse files
committed
HBASE-30265 Fix flaky TestProcDispatcher.testRetryLimitOnConnClosedErrors (#8439) (#8490)
Signed-off-by: Duo Zhang <zhangduo@apache.org> (cherry picked from commit 872616e)
1 parent 0f8fbd2 commit 41be021

2 files changed

Lines changed: 108 additions & 33 deletions

File tree

hbase-server/src/test/java/org/apache/hadoop/hbase/util/RSProcDispatcher.java

Lines changed: 89 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@
2323
import java.util.Arrays;
2424
import java.util.List;
2525
import java.util.Set;
26+
import java.util.concurrent.atomic.AtomicBoolean;
2627
import java.util.concurrent.atomic.AtomicInteger;
2728
import org.apache.hadoop.hbase.ServerName;
29+
import org.apache.hadoop.hbase.TableName;
30+
import org.apache.hadoop.hbase.client.RegionInfo;
2831
import org.apache.hadoop.hbase.exceptions.ConnectionClosedException;
2932
import org.apache.hadoop.hbase.master.MasterServices;
3033
import org.apache.hadoop.hbase.master.procedure.RSProcedureDispatcher;
@@ -34,7 +37,9 @@
3437

3538
import org.apache.hbase.thirdparty.com.google.protobuf.ServiceException;
3639

40+
import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
3741
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos;
42+
import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos;
3843

3944
/**
4045
* Test implementation of RSProcedureDispatcher that throws desired errors for testing purpose.
@@ -43,7 +48,8 @@ public class RSProcDispatcher extends RSProcedureDispatcher {
4348

4449
private static final Logger LOG = LoggerFactory.getLogger(RSProcDispatcher.class);
4550

46-
private static final AtomicInteger I = new AtomicInteger();
51+
/** Config key for the fail-fast retry limit, shared with the test so the two cannot drift. */
52+
static final String FAIL_FAST_LIMIT_KEY = "hbase.master.rs.remote.proc.fail.fast.limit";
4753

4854
private static final List<IOException> ERRORS =
4955
Arrays.asList(new ConnectionClosedException("test connection closed error..."),
@@ -52,8 +58,38 @@ public class RSProcDispatcher extends RSProcedureDispatcher {
5258

5359
private static final AtomicInteger ERROR_IDX = new AtomicInteger();
5460

61+
// Injection is driven by the test and bound to a target table, not a global call count:
62+
// remoteDispatch() fires for every remote procedure in the cluster (startup, table creation,
63+
// chores, background assignments), so counting calls drifts and misses the operations under test.
64+
private static final AtomicBoolean INJECT = new AtomicBoolean(false);
65+
private static final AtomicInteger VICTIMS_REMAINING = new AtomicInteger(0);
66+
private static volatile TableName targetTable;
67+
68+
// Fail-fast retry limit after which the master schedules an SCP; read from conf to match test.
69+
private final int failFastLimit;
70+
71+
/**
72+
* Fails the next {@code n} open/close-region requests for {@code table} with connection errors
73+
* until the fail-fast retry limit is exhausted, so the master schedules an SCP. Call right before
74+
* the operations under test.
75+
*/
76+
static void injectErrorsForNextRequests(TableName table, int n) {
77+
ERROR_IDX.set(0);
78+
targetTable = table;
79+
VICTIMS_REMAINING.set(n);
80+
INJECT.set(true);
81+
}
82+
83+
/** Stops error injection. Safe to call unconditionally, e.g. from test teardown. */
84+
static void stopInjecting() {
85+
INJECT.set(false);
86+
VICTIMS_REMAINING.set(0);
87+
targetTable = null;
88+
}
89+
5590
public RSProcDispatcher(MasterServices master) {
5691
super(master);
92+
this.failFastLimit = master.getConfiguration().getInt(FAIL_FAST_LIMIT_KEY, 10);
5793
}
5894

5995
@Override
@@ -67,8 +103,42 @@ protected void remoteDispatch(final ServerName serverName,
67103
}
68104
}
69105

106+
/**
107+
* True if the request opens or closes a region of the injection target table. Open requests carry
108+
* a full RegionInfo; close requests carry a REGION_NAME specifier the table is parsed from.
109+
*/
110+
private static boolean targetsInjectionTable(AdminProtos.ExecuteProceduresRequest request) {
111+
TableName table = targetTable;
112+
if (table == null) {
113+
return false;
114+
}
115+
for (AdminProtos.OpenRegionRequest open : request.getOpenRegionList()) {
116+
for (AdminProtos.OpenRegionRequest.RegionOpenInfo info : open.getOpenInfoList()) {
117+
if (table.equals(ProtobufUtil.toTableName(info.getRegion().getTableName()))) {
118+
return true;
119+
}
120+
}
121+
}
122+
for (AdminProtos.CloseRegionRequest close : request.getCloseRegionList()) {
123+
HBaseProtos.RegionSpecifier region = close.getRegion();
124+
if (
125+
region.getType() == HBaseProtos.RegionSpecifier.RegionSpecifierType.REGION_NAME
126+
&& table.equals(RegionInfo.getTable(region.getValue().toByteArray()))
127+
) {
128+
return true;
129+
}
130+
}
131+
return false;
132+
}
133+
70134
class TestExecuteProceduresRemoteCall extends ExecuteProceduresRemoteCall {
71135

136+
// attempts: retries of this single request instance (mirrors the dispatcher's
137+
// numberOfAttemptsSoFar). injectErrors: whether this instance is failed with injected errors,
138+
// decided once on the first call and kept across its retries.
139+
private int attempts = 0;
140+
private Boolean injectErrors = null;
141+
72142
public TestExecuteProceduresRemoteCall(ServerName serverName,
73143
Set<RemoteProcedure> remoteProcedures) {
74144
super(serverName, remoteProcedures);
@@ -77,31 +147,30 @@ public TestExecuteProceduresRemoteCall(ServerName serverName,
77147
@Override
78148
public AdminProtos.ExecuteProceduresResponse sendRequest(final ServerName serverName,
79149
final AdminProtos.ExecuteProceduresRequest request) throws IOException {
80-
int j = I.addAndGet(1);
81-
LOG.info("sendRequest() req: {} , j: {}", request, j);
82-
if (j == 12 || j == 22) {
83-
// Execute the remote close and open region requests in the last (5th) retry before
84-
// throwing ConnectionClosedException. This is to ensure even if the region open/close
85-
// is successfully completed by regionserver, master still schedules SCP because
86-
// sendRequest() throws error which has retry-limit exhausted.
150+
if (injectErrors == null) {
151+
// Claim a slot only for a target-table open/close request, once per instance.
152+
injectErrors =
153+
INJECT.get() && targetsInjectionTable(request) && VICTIMS_REMAINING.getAndDecrement() > 0;
154+
}
155+
LOG.info("sendRequest() req: {}, attempts: {}, injectErrors: {}", request, attempts,
156+
injectErrors);
157+
if (!injectErrors) {
87158
try {
88-
getRsAdmin().executeProcedures(null, request);
159+
return getRsAdmin().executeProcedures(null, request);
89160
} catch (ServiceException e) {
90161
throw new RuntimeException(e);
91162
}
92163
}
93-
// For one of the close region requests and one of the open region requests,
94-
// throw ConnectionClosedException until retry limit is exhausted and master
95-
// schedules recoveries for the server.
96-
// We will have ABNORMALLY_CLOSED regions, and they are expected to recover on their own.
97-
if (j >= 10 && j <= 15 || j >= 18 && j <= 23) {
98-
throw ERRORS.get(ERROR_IDX.getAndIncrement() % ERRORS.size());
99-
}
100-
try {
101-
return getRsAdmin().executeProcedures(null, request);
102-
} catch (ServiceException e) {
103-
throw new RuntimeException(e);
164+
// Throw a connection error each attempt until the retry limit is exhausted (-> SCP). On the
165+
// last attempt run the real open/close first so the region still recovers.
166+
if (attempts++ >= failFastLimit - 1) {
167+
try {
168+
getRsAdmin().executeProcedures(null, request);
169+
} catch (ServiceException e) {
170+
throw new RuntimeException(e);
171+
}
104172
}
173+
throw ERRORS.get(ERROR_IDX.getAndIncrement() % ERRORS.size());
105174
}
106175

107176
private AdminProtos.AdminService.BlockingInterface getRsAdmin() throws IOException {
@@ -121,5 +190,4 @@ public void run() {
121190
new RegionServerStoppedException("Server " + getServerName() + " is not online"));
122191
}
123192
}
124-
125193
}

hbase-server/src/test/java/org/apache/hadoop/hbase/util/TestProcDispatcher.java

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import org.apache.hadoop.hbase.testclassification.LargeTests;
4343
import org.apache.hadoop.hbase.testclassification.MiscTests;
4444
import org.junit.jupiter.api.AfterAll;
45+
import org.junit.jupiter.api.AfterEach;
4546
import org.junit.jupiter.api.BeforeAll;
4647
import org.junit.jupiter.api.BeforeEach;
4748
import org.junit.jupiter.api.Tag;
@@ -68,7 +69,7 @@ public class TestProcDispatcher {
6869
public static void setUpBeforeClass() throws Exception {
6970
TEST_UTIL.getConfiguration().set(HBASE_MASTER_RSPROC_DISPATCHER_CLASS,
7071
RSProcDispatcher.class.getName());
71-
TEST_UTIL.getConfiguration().setInt("hbase.master.rs.remote.proc.fail.fast.limit", 5);
72+
TEST_UTIL.getConfiguration().setInt(RSProcDispatcher.FAIL_FAST_LIMIT_KEY, 5);
7273
TEST_UTIL.startMiniCluster(3);
7374
MiniHBaseCluster cluster = TEST_UTIL.getHBaseCluster();
7475
rs0 = cluster.getRegionServer(0).getServerName();
@@ -90,17 +91,23 @@ public void setUp(TestInfo testInfo) throws Exception {
9091
TEST_UTIL.getAdmin().createTable(tableDesc, Bytes.toBytes(startKey), Bytes.toBytes(endKey), 9);
9192
}
9293

94+
@AfterEach
95+
public void tearDown() {
96+
RSProcDispatcher.stopInjecting();
97+
}
98+
9399
@Test
94100
public void testRetryLimitOnConnClosedErrors(TestInfo testInfo) throws Exception {
95101
HbckChore hbckChore = new HbckChore(TEST_UTIL.getHBaseCluster().getMaster());
96102
final TableName tableName = TableName.valueOf(testInfo.getTestMethod().get().getName());
97103
MiniHBaseCluster cluster = TEST_UTIL.getHBaseCluster();
98104
Admin admin = TEST_UTIL.getAdmin();
99-
Table table = TEST_UTIL.getConnection().getTable(tableName);
100105
List<Put> puts = IntStream.range(10, 50000).mapToObj(i -> new Put(Bytes.toBytes(i))
101106
.addColumn(Bytes.toBytes("fam1"), Bytes.toBytes("q1"), Bytes.toBytes("val_" + i)))
102107
.collect(Collectors.toList());
103-
table.put(puts);
108+
try (Table table = TEST_UTIL.getConnection().getTable(tableName)) {
109+
table.put(puts);
110+
}
104111
admin.flush(tableName);
105112
admin.compact(tableName);
106113
Thread.sleep(3000);
@@ -120,18 +127,20 @@ public void testRetryLimitOnConnClosedErrors(TestInfo testInfo) throws Exception
120127
HRegion region0 = !hRegionServer0.getRegions().isEmpty()
121128
? hRegionServer0.getRegions().get(0)
122129
: hRegionServer1.getRegions().get(0);
130+
// Fail the next two open/close-region requests for this table so the moves trigger SCP(s).
131+
RSProcDispatcher.injectErrorsForNextRequests(tableName, 2);
123132
// move all regions from server1 to server0
124133
for (HRegion region : hRegionServer1.getRegions()) {
125134
TEST_UTIL.getAdmin().move(region.getRegionInfo().getEncodedNameAsBytes(), rs0);
126135
}
127136
TEST_UTIL.getAdmin().move(region0.getRegionInfo().getEncodedNameAsBytes());
128137
HMaster master = TEST_UTIL.getHBaseCluster().getMaster();
129138

130-
// Ensure:
131-
// 1. num of regions before and after scheduling SCP remain same
132-
// 2. all procedures including SCPs are successfully completed
133-
// 3. two servers have SCPs scheduled
134-
TEST_UTIL.waitFor(5000, 1000, () -> {
139+
// Ensure, after the injected connection errors:
140+
// 1. the total number of regions is unchanged before and after the SCP(s)
141+
// 2. all procedures (including the SCP(s)) complete successfully
142+
// 3. at least one ServerCrashProcedure was scheduled
143+
TEST_UTIL.waitFor(60000, 1000, () -> {
135144
LOG.info("numRegions0: {} , numRegions1: {} , numRegions2: {}", numRegions0, numRegions1,
136145
numRegions2);
137146
LOG.info("Online regions - server0 : {} , server1: {} , server2: {}",
@@ -144,7 +153,7 @@ public void testRetryLimitOnConnClosedErrors(TestInfo testInfo) throws Exception
144153
== ProcedureProtos.ProcedureState.SUCCESS)
145154
.count(),
146155
master.getMasterProcedureExecutor().getProcedures().size());
147-
LOG.info("Num of SCPs: " + master.getMasterProcedureExecutor().getProcedures().stream()
156+
LOG.info("Num of SCPs: {}", master.getMasterProcedureExecutor().getProcedures().stream()
148157
.filter(proc -> proc instanceof ServerCrashProcedure).count());
149158
return (numRegions0 + numRegions1 + numRegions2)
150159
== (cluster.getRegionServer(0).getNumberOfOnlineRegions()
@@ -159,13 +168,11 @@ public void testRetryLimitOnConnClosedErrors(TestInfo testInfo) throws Exception
159168
});
160169

161170
// Ensure we have no inconsistent regions
162-
TEST_UTIL.waitFor(5000, 1000, () -> {
171+
TEST_UTIL.waitFor(60000, 1000, () -> {
163172
hbckChore.choreForTesting();
164173
HbckReport report = hbckChore.getLastReport();
165174
return report.getInconsistentRegions().isEmpty() && report.getOrphanRegionsOnFS().isEmpty()
166175
&& report.getOrphanRegionsOnRS().isEmpty();
167176
});
168-
169177
}
170-
171178
}

0 commit comments

Comments
 (0)