2323import java .util .Arrays ;
2424import java .util .List ;
2525import java .util .Set ;
26+ import java .util .concurrent .atomic .AtomicBoolean ;
2627import java .util .concurrent .atomic .AtomicInteger ;
2728import org .apache .hadoop .hbase .ServerName ;
29+ import org .apache .hadoop .hbase .TableName ;
30+ import org .apache .hadoop .hbase .client .RegionInfo ;
2831import org .apache .hadoop .hbase .exceptions .ConnectionClosedException ;
2932import org .apache .hadoop .hbase .master .MasterServices ;
3033import org .apache .hadoop .hbase .master .procedure .RSProcedureDispatcher ;
3437
3538import org .apache .hbase .thirdparty .com .google .protobuf .ServiceException ;
3639
40+ import org .apache .hadoop .hbase .shaded .protobuf .ProtobufUtil ;
3741import 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}
0 commit comments