001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.master.normalizer;
019
020import static java.util.Collections.singletonList;
021import static org.hamcrest.MatcherAssert.assertThat;
022import static org.hamcrest.Matchers.comparesEqualTo;
023import static org.hamcrest.Matchers.greaterThan;
024import static org.hamcrest.Matchers.greaterThanOrEqualTo;
025import static org.hamcrest.Matchers.nullValue;
026import static org.junit.Assert.assertTrue;
027import static org.mockito.ArgumentMatchers.any;
028import static org.mockito.ArgumentMatchers.anyBoolean;
029import static org.mockito.ArgumentMatchers.anyLong;
030import static org.mockito.Mockito.when;
031
032import java.time.Duration;
033import java.util.Arrays;
034import java.util.concurrent.ExecutorService;
035import java.util.concurrent.Executors;
036import java.util.concurrent.ThreadFactory;
037import java.util.concurrent.TimeUnit;
038import java.util.concurrent.atomic.AtomicReference;
039import java.util.function.Supplier;
040import org.apache.hadoop.conf.Configuration;
041import org.apache.hadoop.hbase.HBaseClassTestRule;
042import org.apache.hadoop.hbase.HBaseCommonTestingUtility;
043import org.apache.hadoop.hbase.TableName;
044import org.apache.hadoop.hbase.TableNameTestRule;
045import org.apache.hadoop.hbase.Waiter;
046import org.apache.hadoop.hbase.client.RegionInfo;
047import org.apache.hadoop.hbase.client.RegionInfoBuilder;
048import org.apache.hadoop.hbase.client.TableDescriptor;
049import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
050import org.apache.hadoop.hbase.master.MasterServices;
051import org.apache.hadoop.hbase.testclassification.MasterTests;
052import org.apache.hadoop.hbase.testclassification.SmallTests;
053import org.hamcrest.Description;
054import org.hamcrest.Matcher;
055import org.hamcrest.StringDescription;
056import org.junit.After;
057import org.junit.Before;
058import org.junit.ClassRule;
059import org.junit.Rule;
060import org.junit.Test;
061import org.junit.experimental.categories.Category;
062import org.junit.rules.TestName;
063import org.mockito.Answers;
064import org.mockito.Mock;
065import org.mockito.MockitoAnnotations;
066import org.mockito.junit.MockitoJUnit;
067import org.mockito.junit.MockitoRule;
068
069import org.apache.hbase.thirdparty.com.google.common.util.concurrent.ThreadFactoryBuilder;
070
071/**
072 * A test over {@link RegionNormalizerWorker}. Being a background thread, the only points of
073 * interaction we have to this class are its input source ({@link RegionNormalizerWorkQueue} and its
074 * callbacks invoked against {@link RegionNormalizer} and {@link MasterServices}. The work queue is
075 * simple enough to use directly; for {@link MasterServices}, use a mock because, as of now, the
076 * worker only invokes 4 methods.
077 */
078@Category({ MasterTests.class, SmallTests.class })
079public class TestRegionNormalizerWorker {
080
081  @ClassRule
082  public static final HBaseClassTestRule CLASS_RULE =
083    HBaseClassTestRule.forClass(TestRegionNormalizerWorker.class);
084
085  @Rule
086  public TestName testName = new TestName();
087  @Rule
088  public TableNameTestRule tableName = new TableNameTestRule();
089
090  @Rule
091  public MockitoRule mockitoRule = MockitoJUnit.rule();
092
093  @Mock(answer = Answers.RETURNS_DEEP_STUBS)
094  private MasterServices masterServices;
095  @Mock
096  private RegionNormalizer regionNormalizer;
097
098  private HBaseCommonTestingUtility testingUtility;
099  private RegionNormalizerWorkQueue<TableName> queue;
100  private ExecutorService workerPool;
101
102  private final AtomicReference<Throwable> workerThreadThrowable = new AtomicReference<>();
103
104  @Before
105  public void before() throws Exception {
106    MockitoAnnotations.initMocks(this);
107    when(masterServices.skipRegionManagementAction(any())).thenReturn(false);
108    testingUtility = new HBaseCommonTestingUtility();
109    queue = new RegionNormalizerWorkQueue<>();
110    workerThreadThrowable.set(null);
111
112    final String threadNameFmt =
113      TestRegionNormalizerWorker.class.getSimpleName() + "-" + testName.getMethodName() + "-%d";
114    final ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat(threadNameFmt)
115      .setDaemon(true).setUncaughtExceptionHandler((t, e) -> workerThreadThrowable.set(e)).build();
116    workerPool = Executors.newSingleThreadExecutor(threadFactory);
117  }
118
119  @After
120  public void after() throws Exception {
121    workerPool.shutdownNow(); // shutdownNow to interrupt the worker thread sitting on `take()`
122    assertTrue("timeout waiting for worker thread to terminate",
123      workerPool.awaitTermination(30, TimeUnit.SECONDS));
124    final Throwable workerThrowable = workerThreadThrowable.get();
125    assertThat("worker thread threw unexpected exception", workerThrowable, nullValue());
126  }
127
128  @Test
129  public void testMergeCounter() throws Exception {
130    final TableName tn = tableName.getTableName();
131    final TableDescriptor tnDescriptor =
132      TableDescriptorBuilder.newBuilder(tn).setNormalizationEnabled(true).build();
133    when(masterServices.getTableDescriptors().get(tn)).thenReturn(tnDescriptor);
134    when(masterServices.mergeRegions(any(), anyBoolean(), anyLong(), anyLong())).thenReturn(1L);
135    when(regionNormalizer.computePlansForTable(tnDescriptor)).thenReturn(singletonList(
136      new MergeNormalizationPlan.Builder().addTarget(RegionInfoBuilder.newBuilder(tn).build(), 10)
137        .addTarget(RegionInfoBuilder.newBuilder(tn).build(), 20).build()));
138
139    final RegionNormalizerWorker worker = new RegionNormalizerWorker(
140      testingUtility.getConfiguration(), masterServices, regionNormalizer, queue);
141    final long beforeMergePlanCount = worker.getMergePlanCount();
142    workerPool.submit(worker);
143    queue.put(tn);
144
145    assertThatEventually("executing work should see plan count increase", worker::getMergePlanCount,
146      greaterThan(beforeMergePlanCount));
147  }
148
149  @Test
150  public void testSplitCounter() throws Exception {
151    final TableName tn = tableName.getTableName();
152    final TableDescriptor tnDescriptor =
153      TableDescriptorBuilder.newBuilder(tn).setNormalizationEnabled(true).build();
154    when(masterServices.getTableDescriptors().get(tn)).thenReturn(tnDescriptor);
155    when(masterServices.splitRegion(any(), any(), anyLong(), anyLong())).thenReturn(1L);
156    when(regionNormalizer.computePlansForTable(tnDescriptor)).thenReturn(
157      singletonList(new SplitNormalizationPlan(RegionInfoBuilder.newBuilder(tn).build(), 10)));
158
159    final RegionNormalizerWorker worker = new RegionNormalizerWorker(
160      testingUtility.getConfiguration(), masterServices, regionNormalizer, queue);
161    final long beforeSplitPlanCount = worker.getSplitPlanCount();
162    workerPool.submit(worker);
163    queue.put(tn);
164
165    assertThatEventually("executing work should see plan count increase", worker::getSplitPlanCount,
166      greaterThan(beforeSplitPlanCount));
167  }
168
169  /**
170   * Assert that a rate limit is honored, at least in a rough way. Maintainers should manually
171   * inspect the log messages emitted by the worker thread to confirm that expected behavior.
172   */
173  @Test
174  public void testRateLimit() throws Exception {
175    final TableName tn = tableName.getTableName();
176    final TableDescriptor tnDescriptor =
177      TableDescriptorBuilder.newBuilder(tn).setNormalizationEnabled(true).build();
178    final RegionInfo splitRegionInfo = RegionInfoBuilder.newBuilder(tn).build();
179    final RegionInfo mergeRegionInfo1 = RegionInfoBuilder.newBuilder(tn).build();
180    final RegionInfo mergeRegionInfo2 = RegionInfoBuilder.newBuilder(tn).build();
181    when(masterServices.getTableDescriptors().get(tn)).thenReturn(tnDescriptor);
182    when(masterServices.splitRegion(any(), any(), anyLong(), anyLong())).thenReturn(1L);
183    when(masterServices.mergeRegions(any(), anyBoolean(), anyLong(), anyLong())).thenReturn(1L);
184    when(regionNormalizer.computePlansForTable(tnDescriptor)).thenReturn(Arrays.asList(
185      new SplitNormalizationPlan(splitRegionInfo, 2), new MergeNormalizationPlan.Builder()
186        .addTarget(mergeRegionInfo1, 1).addTarget(mergeRegionInfo2, 2).build(),
187      new SplitNormalizationPlan(splitRegionInfo, 1)));
188
189    final Configuration conf = testingUtility.getConfiguration();
190    conf.set("hbase.normalizer.throughput.max_bytes_per_sec", "1m");
191    final RegionNormalizerWorker worker = new RegionNormalizerWorker(
192      testingUtility.getConfiguration(), masterServices, regionNormalizer, queue);
193    workerPool.submit(worker);
194    final long startTime = System.nanoTime();
195    queue.put(tn);
196
197    assertThatEventually("executing work should see split plan count increase",
198      worker::getSplitPlanCount, comparesEqualTo(2L));
199    assertThatEventually("executing work should see merge plan count increase",
200      worker::getMergePlanCount, comparesEqualTo(1L));
201
202    final long endTime = System.nanoTime();
203    assertThat("rate limited normalizer should have taken at least 5 seconds",
204      Duration.ofNanos(endTime - startTime), greaterThanOrEqualTo(Duration.ofSeconds(5)));
205  }
206
207  @Test
208  public void testPlansSizeLimit() throws Exception {
209    final TableName tn = tableName.getTableName();
210    final TableDescriptor tnDescriptor =
211      TableDescriptorBuilder.newBuilder(tn).setNormalizationEnabled(true).build();
212    final RegionInfo splitRegionInfo = RegionInfoBuilder.newBuilder(tn).build();
213    final RegionInfo mergeRegionInfo1 = RegionInfoBuilder.newBuilder(tn).build();
214    final RegionInfo mergeRegionInfo2 = RegionInfoBuilder.newBuilder(tn).build();
215    when(masterServices.getTableDescriptors().get(tn)).thenReturn(tnDescriptor);
216    when(masterServices.splitRegion(any(), any(), anyLong(), anyLong())).thenReturn(1L);
217    when(masterServices.mergeRegions(any(), anyBoolean(), anyLong(), anyLong())).thenReturn(1L);
218    when(regionNormalizer.computePlansForTable(tnDescriptor)).thenReturn(Arrays.asList(
219      new SplitNormalizationPlan(splitRegionInfo, 2), new MergeNormalizationPlan.Builder()
220        .addTarget(mergeRegionInfo1, 1).addTarget(mergeRegionInfo2, 2).build(),
221      new SplitNormalizationPlan(splitRegionInfo, 1)));
222
223    final Configuration conf = testingUtility.getConfiguration();
224    conf.setLong(RegionNormalizerWorker.CUMULATIVE_SIZE_LIMIT_MB_KEY, 5);
225
226    final RegionNormalizerWorker worker = new RegionNormalizerWorker(
227      testingUtility.getConfiguration(), masterServices, regionNormalizer, queue);
228    workerPool.submit(worker);
229    queue.put(tn);
230
231    assertThatEventually("worker should process first split plan, but not second",
232      worker::getSplitPlanCount, comparesEqualTo(1L));
233    assertThatEventually("worker should process merge plan", worker::getMergePlanCount,
234      comparesEqualTo(1L));
235  }
236
237  /**
238   * Repeatedly evaluates {@code matcher} against the result of calling {@code actualSupplier} until
239   * the matcher succeeds or the timeout period of 30 seconds is exhausted.
240   */
241  private <T> void assertThatEventually(final String reason,
242    final Supplier<? extends T> actualSupplier, final Matcher<? super T> matcher) throws Exception {
243    testingUtility.waitFor(TimeUnit.SECONDS.toMillis(30),
244      new Waiter.ExplainingPredicate<Exception>() {
245        private T lastValue = null;
246
247        @Override
248        public String explainFailure() {
249          final Description description = new StringDescription().appendText(reason)
250            .appendText("\nExpected: ").appendDescriptionOf(matcher).appendText("\n     but: ");
251          matcher.describeMismatch(lastValue, description);
252          return description.toString();
253        }
254
255        @Override
256        public boolean evaluate() {
257          lastValue = actualSupplier.get();
258          return matcher.matches(lastValue);
259        }
260      });
261  }
262}