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.tool;
019
020import static org.apache.hadoop.hbase.regionserver.TestRegionServerNoMaster.closeRegion;
021import static org.apache.hadoop.hbase.tool.CanaryTool.HBASE_CANARY_INFO_PORT;
022import static org.junit.Assert.assertEquals;
023import static org.junit.Assert.assertFalse;
024import static org.junit.Assert.assertNotEquals;
025import static org.junit.Assert.assertNotNull;
026import static org.junit.Assert.assertTrue;
027import static org.mockito.ArgumentMatchers.anyLong;
028import static org.mockito.ArgumentMatchers.argThat;
029import static org.mockito.ArgumentMatchers.eq;
030import static org.mockito.ArgumentMatchers.isA;
031import static org.mockito.Mockito.atLeastOnce;
032import static org.mockito.Mockito.mock;
033import static org.mockito.Mockito.never;
034import static org.mockito.Mockito.spy;
035import static org.mockito.Mockito.times;
036import static org.mockito.Mockito.verify;
037import static org.mockito.Mockito.when;
038
039import java.io.IOException;
040import java.io.InputStream;
041import java.net.HttpURLConnection;
042import java.net.URL;
043import java.nio.charset.StandardCharsets;
044import java.util.List;
045import java.util.Map;
046import java.util.concurrent.ConcurrentHashMap;
047import java.util.concurrent.ConcurrentMap;
048import java.util.concurrent.ExecutorService;
049import java.util.concurrent.ScheduledThreadPoolExecutor;
050import java.util.concurrent.TimeUnit;
051import java.util.concurrent.atomic.LongAdder;
052import org.apache.commons.io.IOUtils;
053import org.apache.hadoop.conf.Configuration;
054import org.apache.hadoop.hbase.HBaseClassTestRule;
055import org.apache.hadoop.hbase.HBaseConfiguration;
056import org.apache.hadoop.hbase.HBaseTestingUtil;
057import org.apache.hadoop.hbase.HConstants;
058import org.apache.hadoop.hbase.ServerName;
059import org.apache.hadoop.hbase.TableName;
060import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
061import org.apache.hadoop.hbase.client.Put;
062import org.apache.hadoop.hbase.client.RegionInfo;
063import org.apache.hadoop.hbase.client.Table;
064import org.apache.hadoop.hbase.regionserver.HRegionServer;
065import org.apache.hadoop.hbase.testclassification.LargeTests;
066import org.apache.hadoop.hbase.util.Bytes;
067import org.apache.hadoop.hbase.util.JvmVersion;
068import org.apache.hadoop.hbase.util.VersionInfo;
069import org.apache.hadoop.util.ToolRunner;
070import org.junit.After;
071import org.junit.Before;
072import org.junit.ClassRule;
073import org.junit.Rule;
074import org.junit.Test;
075import org.junit.experimental.categories.Category;
076import org.junit.rules.TestName;
077import org.mockito.ArgumentMatcher;
078
079@Category({ LargeTests.class })
080public class TestCanaryTool {
081
082  @ClassRule
083  public static final HBaseClassTestRule CLASS_RULE =
084    HBaseClassTestRule.forClass(TestCanaryTool.class);
085
086  private HBaseTestingUtil testingUtility;
087  private static final byte[] FAMILY = Bytes.toBytes("f");
088  private static final byte[] COLUMN = Bytes.toBytes("col");
089
090  @Rule
091  public TestName name = new TestName();
092
093  private org.apache.logging.log4j.core.Appender mockAppender;
094
095  @Before
096  public void setUp() throws Exception {
097    testingUtility = new HBaseTestingUtil();
098    testingUtility.startMiniCluster();
099    mockAppender = mock(org.apache.logging.log4j.core.Appender.class);
100    when(mockAppender.getName()).thenReturn("mockAppender");
101    when(mockAppender.isStarted()).thenReturn(true);
102    ((org.apache.logging.log4j.core.Logger) org.apache.logging.log4j.LogManager
103      .getLogger("org.apache.hadoop.hbase")).addAppender(mockAppender);
104  }
105
106  @After
107  public void tearDown() throws Exception {
108    testingUtility.shutdownMiniCluster();
109    ((org.apache.logging.log4j.core.Logger) org.apache.logging.log4j.LogManager
110      .getLogger("org.apache.hadoop.hbase")).removeAppender(mockAppender);
111  }
112
113  @Test
114  public void testBasicZookeeperCanaryWorks() throws Exception {
115    final String[] args = { "-t", "10000", "-zookeeper" };
116    testZookeeperCanaryWithArgs(args);
117  }
118
119  @Test
120  public void testZookeeperCanaryPermittedFailuresArgumentWorks() throws Exception {
121    final String[] args =
122      { "-t", "10000", "-zookeeper", "-treatFailureAsError", "-permittedZookeeperFailures", "1" };
123    testZookeeperCanaryWithArgs(args);
124  }
125
126  @Test
127  public void testBasicCanaryWorks() throws Exception {
128    final TableName tableName = TableName.valueOf(name.getMethodName());
129    Table table = testingUtility.createTable(tableName, new byte[][] { FAMILY });
130    // insert some test rows
131    for (int i = 0; i < 1000; i++) {
132      byte[] iBytes = Bytes.toBytes(i);
133      Put p = new Put(iBytes);
134      p.addColumn(FAMILY, COLUMN, iBytes);
135      table.put(p);
136    }
137    ExecutorService executor = new ScheduledThreadPoolExecutor(1);
138    CanaryTool.RegionStdOutSink sink = spy(new CanaryTool.RegionStdOutSink());
139    CanaryTool canary = new CanaryTool(executor, sink);
140    String[] args = { "-writeSniffing", "-t", "10000", tableName.getNameAsString() };
141    assertEquals(0, ToolRunner.run(testingUtility.getConfiguration(), canary, args));
142    assertEquals("verify no read error count", 0, canary.getReadFailures().size());
143    assertEquals("verify no write error count", 0, canary.getWriteFailures().size());
144    verify(sink, atLeastOnce()).publishReadTiming(isA(ServerName.class), isA(RegionInfo.class),
145      isA(ColumnFamilyDescriptor.class), anyLong());
146  }
147
148  /**
149   * When CanaryTool times out, it should stop scanning and shutdown quickly and gracefully. This
150   * test helps to confirm that threadpools do not continue executing work after the canary
151   * finishes. It also verifies sink behavior and measures correct failure counts in the sink.
152   * @throws Exception if it can't create a table, communicate with minicluster, or run the canary.
153   */
154  @Test
155  public void testCanaryStopsScanningAfterTimeout() throws Exception {
156    // Prepare a table with multiple regions, and close those regions on the regionserver.
157    // Do not notify HMaster or META. CanaryTool will scan and receive NotServingRegionExceptions.
158    final TableName tableName = TableName.valueOf(name.getMethodName());
159    // Close the unused Table reference returned by createMultiRegionTable.
160    testingUtility.createMultiRegionTable(tableName, new byte[][] { FAMILY }).close();
161    List<RegionInfo> regions = testingUtility.getAdmin().getRegions(tableName);
162    assertTrue("verify table has multiple regions", regions.size() > 1);
163    HRegionServer regionserver = testingUtility.getMiniHBaseCluster().getRegionServer(0);
164    for (RegionInfo region : regions) {
165      closeRegion(testingUtility, regionserver, region);
166    }
167
168    // Run CanaryTool with 1 thread. This thread will attempt to scan the first region.
169    // It will use default rpc retries and receive NotServingRegionExceptions for many seconds
170    // according to HConstants.RETRY_BACKOFF. The CanaryTool timeout is set to 4 seconds, so it
171    // will time out before the first region scan is complete.
172    ExecutorService executor = new ScheduledThreadPoolExecutor(1);
173    CanaryTool canary = new CanaryTool(executor);
174    String[] args = { "-t", "4000", tableName.getNameAsString() };
175    int retCode = ToolRunner.run(testingUtility.getConfiguration(), canary, args);
176    executor.shutdown();
177    try {
178      if (!executor.awaitTermination(3, TimeUnit.SECONDS)) {
179        executor.shutdownNow();
180      }
181    } catch (InterruptedException e) {
182      executor.shutdownNow();
183    }
184
185    CanaryTool.Sink sink = canary.getActiveSink();
186    assertEquals("verify canary timed out with TIMEOUT_ERROR_EXIT_CODE", 3, retCode);
187    assertEquals("verify only the first region failed", 1, sink.getReadFailureCount());
188    assertEquals("verify no successful reads", 0, sink.getReadSuccessCount());
189    assertEquals("verify we were attempting to scan all regions", regions.size(),
190      ((CanaryTool.RegionStdOutSink) sink).getTotalExpectedRegions());
191  }
192
193  @Test
194  public void testCanaryRegionTaskReadAllCF() throws Exception {
195    final TableName tableName = TableName.valueOf(name.getMethodName());
196    Table table = testingUtility.createTable(tableName,
197      new byte[][] { Bytes.toBytes("f1"), Bytes.toBytes("f2") });
198    // insert some test rows
199    for (int i = 0; i < 1000; i++) {
200      byte[] iBytes = Bytes.toBytes(i);
201      Put p = new Put(iBytes);
202      p.addColumn(Bytes.toBytes("f1"), COLUMN, iBytes);
203      p.addColumn(Bytes.toBytes("f2"), COLUMN, iBytes);
204      table.put(p);
205    }
206    Configuration configuration = HBaseConfiguration.create(testingUtility.getConfiguration());
207    String[] args = { "-t", "10000", "testCanaryRegionTaskReadAllCF" };
208    ExecutorService executor = new ScheduledThreadPoolExecutor(1);
209    for (boolean readAllCF : new boolean[] { true, false }) {
210      CanaryTool.RegionStdOutSink sink = spy(new CanaryTool.RegionStdOutSink());
211      CanaryTool canary = new CanaryTool(executor, sink);
212      configuration.setBoolean(HConstants.HBASE_CANARY_READ_ALL_CF, readAllCF);
213      assertEquals(0, ToolRunner.run(configuration, canary, args));
214      // the test table has two column family. If readAllCF set true,
215      // we expect read count is double of region count
216      int expectedReadCount =
217        readAllCF ? 2 * sink.getTotalExpectedRegions() : sink.getTotalExpectedRegions();
218      assertEquals("canary region success count should equal total expected read count",
219        expectedReadCount, sink.getReadSuccessCount());
220      Map<String, List<CanaryTool.RegionTaskResult>> regionMap = sink.getRegionMap();
221      assertFalse("verify region map has size > 0", regionMap.isEmpty());
222
223      for (String regionName : regionMap.keySet()) {
224        for (CanaryTool.RegionTaskResult res : regionMap.get(regionName)) {
225          assertNotNull("verify getRegionNameAsString()", regionName);
226          assertNotNull("verify getRegionInfo()", res.getRegionInfo());
227          assertNotNull("verify getTableName()", res.getTableName());
228          assertNotNull("verify getTableNameAsString()", res.getTableNameAsString());
229          assertNotNull("verify getServerName()", res.getServerName());
230          assertNotNull("verify getServerNameAsString()", res.getServerNameAsString());
231          assertNotNull("verify getColumnFamily()", res.getColumnFamily());
232          assertNotNull("verify getColumnFamilyNameAsString()", res.getColumnFamilyNameAsString());
233          assertTrue("read from region " + regionName + " succeeded", res.isReadSuccess());
234          assertTrue("read took some time", res.getReadLatency() > -1);
235        }
236      }
237    }
238  }
239
240  @Test
241  public void testCanaryRegionTaskResult() throws Exception {
242    TableName tableName = TableName.valueOf("testCanaryRegionTaskResult");
243    Table table = testingUtility.createTable(tableName, new byte[][] { FAMILY });
244    // insert some test rows
245    for (int i = 0; i < 1000; i++) {
246      byte[] iBytes = Bytes.toBytes(i);
247      Put p = new Put(iBytes);
248      p.addColumn(FAMILY, COLUMN, iBytes);
249      table.put(p);
250    }
251    ExecutorService executor = new ScheduledThreadPoolExecutor(1);
252    CanaryTool.RegionStdOutSink sink = spy(new CanaryTool.RegionStdOutSink());
253    CanaryTool canary = new CanaryTool(executor, sink);
254    String[] args = { "-writeSniffing", "-t", "10000", "testCanaryRegionTaskResult" };
255    assertEquals(0, ToolRunner.run(testingUtility.getConfiguration(), canary, args));
256
257    assertTrue("canary should expect to scan at least 1 region",
258      sink.getTotalExpectedRegions() > 0);
259    assertTrue("there should be no read failures", sink.getReadFailureCount() == 0);
260    assertTrue("there should be no write failures", sink.getWriteFailureCount() == 0);
261    assertTrue("verify read success count > 0", sink.getReadSuccessCount() > 0);
262    assertTrue("verify write success count > 0", sink.getWriteSuccessCount() > 0);
263    verify(sink, atLeastOnce()).publishReadTiming(isA(ServerName.class), isA(RegionInfo.class),
264      isA(ColumnFamilyDescriptor.class), anyLong());
265    verify(sink, atLeastOnce()).publishWriteTiming(isA(ServerName.class), isA(RegionInfo.class),
266      isA(ColumnFamilyDescriptor.class), anyLong());
267
268    assertEquals("canary region success count should equal total expected regions",
269      sink.getReadSuccessCount() + sink.getWriteSuccessCount(), sink.getTotalExpectedRegions());
270    Map<String, List<CanaryTool.RegionTaskResult>> regionMap = sink.getRegionMap();
271    assertFalse("verify region map has size > 0", regionMap.isEmpty());
272
273    for (String regionName : regionMap.keySet()) {
274      for (CanaryTool.RegionTaskResult res : regionMap.get(regionName)) {
275        assertNotNull("verify getRegionNameAsString()", regionName);
276        assertNotNull("verify getRegionInfo()", res.getRegionInfo());
277        assertNotNull("verify getTableName()", res.getTableName());
278        assertNotNull("verify getTableNameAsString()", res.getTableNameAsString());
279        assertNotNull("verify getServerName()", res.getServerName());
280        assertNotNull("verify getServerNameAsString()", res.getServerNameAsString());
281        assertNotNull("verify getColumnFamily()", res.getColumnFamily());
282        assertNotNull("verify getColumnFamilyNameAsString()", res.getColumnFamilyNameAsString());
283
284        if (regionName.contains(CanaryTool.DEFAULT_WRITE_TABLE_NAME.getNameAsString())) {
285          assertTrue("write to region " + regionName + " succeeded", res.isWriteSuccess());
286          assertTrue("write took some time", res.getWriteLatency() > -1);
287        } else {
288          assertTrue("read from region " + regionName + " succeeded", res.isReadSuccess());
289          assertTrue("read took some time", res.getReadLatency() > -1);
290        }
291      }
292    }
293  }
294
295  // Ignore this test. It fails w/ the below on some mac os x.
296  // [ERROR] Failures:
297  // [ERROR] TestCanaryTool.testReadTableTimeouts:216
298  // Argument(s) are different! Wanted:
299  // mockAppender.doAppend(
300  // <custom argument matcher>
301  // );
302  // -> at org.apache.hadoop.hbase.tool.TestCanaryTool
303  // .testReadTableTimeouts(TestCanaryTool.java:216)
304  // Actual invocations have different arguments:
305  // mockAppender.doAppend(
306  // org.apache.log4j.spi.LoggingEvent@2055cfc1
307  // );
308  // )
309  // )
310  //
311  @org.junit.Ignore
312  @Test
313  public void testReadTableTimeouts() throws Exception {
314    final TableName[] tableNames = new TableName[] { TableName.valueOf(name.getMethodName() + "1"),
315      TableName.valueOf(name.getMethodName() + "2") };
316    // Create 2 test tables.
317    for (int j = 0; j < 2; j++) {
318      Table table = testingUtility.createTable(tableNames[j], new byte[][] { FAMILY });
319      // insert some test rows
320      for (int i = 0; i < 10; i++) {
321        byte[] iBytes = Bytes.toBytes(i + j);
322        Put p = new Put(iBytes);
323        p.addColumn(FAMILY, COLUMN, iBytes);
324        table.put(p);
325      }
326    }
327    ExecutorService executor = new ScheduledThreadPoolExecutor(1);
328    CanaryTool.RegionStdOutSink sink = spy(new CanaryTool.RegionStdOutSink());
329    CanaryTool canary = new CanaryTool(executor, sink);
330    String configuredTimeoutStr = tableNames[0].getNameAsString() + "=" + Long.MAX_VALUE + ","
331      + tableNames[1].getNameAsString() + "=0";
332    String[] args = { "-readTableTimeouts", configuredTimeoutStr, name.getMethodName() + "1",
333      name.getMethodName() + "2" };
334    assertEquals(0, ToolRunner.run(testingUtility.getConfiguration(), canary, args));
335    verify(sink, times(tableNames.length)).initializeAndGetReadLatencyForTable(isA(String.class));
336    for (int i = 0; i < 2; i++) {
337      assertNotEquals("verify non-null read latency", null,
338        sink.getReadLatencyMap().get(tableNames[i].getNameAsString()));
339      assertNotEquals("verify non-zero read latency", 0L,
340        sink.getReadLatencyMap().get(tableNames[i].getNameAsString()));
341    }
342    // One table's timeout is set for 0 ms and thus, should lead to an error.
343    verify(mockAppender, times(1))
344      .append(argThat(new ArgumentMatcher<org.apache.logging.log4j.core.LogEvent>() {
345        @Override
346        public boolean matches(org.apache.logging.log4j.core.LogEvent argument) {
347          return argument.getMessage().getFormattedMessage()
348            .contains("exceeded the configured read timeout.");
349        }
350      }));
351    verify(mockAppender, times(2))
352      .append(argThat(new ArgumentMatcher<org.apache.logging.log4j.core.LogEvent>() {
353        @Override
354        public boolean matches(org.apache.logging.log4j.core.LogEvent argument) {
355          return argument.getMessage().getFormattedMessage().contains("Configured read timeout");
356        }
357      }));
358  }
359
360  @Test
361  public void testWriteTableTimeout() throws Exception {
362    ExecutorService executor = new ScheduledThreadPoolExecutor(1);
363    CanaryTool.RegionStdOutSink sink = spy(new CanaryTool.RegionStdOutSink());
364    CanaryTool canary = new CanaryTool(executor, sink);
365    String[] args = { "-writeSniffing", "-writeTableTimeout", String.valueOf(Long.MAX_VALUE) };
366    assertEquals(0, ToolRunner.run(testingUtility.getConfiguration(), canary, args));
367    assertNotEquals("verify non-null write latency", null, sink.getWriteLatency());
368    assertNotEquals("verify non-zero write latency", 0L, sink.getWriteLatency());
369    verify(mockAppender, times(1))
370      .append(argThat(new ArgumentMatcher<org.apache.logging.log4j.core.LogEvent>() {
371        @Override
372        public boolean matches(org.apache.logging.log4j.core.LogEvent argument) {
373          return argument.getMessage().getFormattedMessage().contains("Configured write timeout");
374        }
375      }));
376  }
377
378  // no table created, so there should be no regions
379  @Test
380  public void testRegionserverNoRegions() throws Exception {
381    runRegionserverCanary();
382    verify(mockAppender)
383      .append(argThat(new ArgumentMatcher<org.apache.logging.log4j.core.LogEvent>() {
384        @Override
385        public boolean matches(org.apache.logging.log4j.core.LogEvent argument) {
386          return argument.getMessage().getFormattedMessage()
387            .contains("Regionserver not serving any regions");
388        }
389      }));
390  }
391
392  // by creating a table, there shouldn't be any region servers not serving any regions
393  @Test
394  public void testRegionserverWithRegions() throws Exception {
395    final TableName tableName = TableName.valueOf(name.getMethodName());
396    testingUtility.createTable(tableName, new byte[][] { FAMILY });
397    runRegionserverCanary();
398    verify(mockAppender, never())
399      .append(argThat(new ArgumentMatcher<org.apache.logging.log4j.core.LogEvent>() {
400        @Override
401        public boolean matches(org.apache.logging.log4j.core.LogEvent argument) {
402          return argument.getMessage().getFormattedMessage()
403            .contains("Regionserver not serving any regions");
404        }
405      }));
406  }
407
408  @Test
409  public void testRawScanConfig() throws Exception {
410    final TableName tableName = TableName.valueOf(name.getMethodName());
411    Table table = testingUtility.createTable(tableName, new byte[][] { FAMILY });
412    // insert some test rows
413    for (int i = 0; i < 1000; i++) {
414      byte[] iBytes = Bytes.toBytes(i);
415      Put p = new Put(iBytes);
416      p.addColumn(FAMILY, COLUMN, iBytes);
417      table.put(p);
418    }
419    ExecutorService executor = new ScheduledThreadPoolExecutor(1);
420    CanaryTool.RegionStdOutSink sink = spy(new CanaryTool.RegionStdOutSink());
421    CanaryTool canary = new CanaryTool(executor, sink);
422    String[] args = { "-t", "10000", name.getMethodName() };
423    org.apache.hadoop.conf.Configuration conf =
424      new org.apache.hadoop.conf.Configuration(testingUtility.getConfiguration());
425    conf.setBoolean(HConstants.HBASE_CANARY_READ_RAW_SCAN_KEY, true);
426    assertEquals(0, ToolRunner.run(conf, canary, args));
427    verify(sink, atLeastOnce()).publishReadTiming(isA(ServerName.class), isA(RegionInfo.class),
428      isA(ColumnFamilyDescriptor.class), anyLong());
429    assertEquals("verify no read error count", 0, canary.getReadFailures().size());
430  }
431
432  private void runRegionserverCanary() throws Exception {
433    ExecutorService executor = new ScheduledThreadPoolExecutor(1);
434    CanaryTool canary = new CanaryTool(executor, new CanaryTool.RegionServerStdOutSink());
435    String[] args = { "-t", "10000", "-regionserver" };
436    assertEquals(0, ToolRunner.run(testingUtility.getConfiguration(), canary, args));
437    assertEquals("verify no read error count", 0, canary.getReadFailures().size());
438  }
439
440  private void testZookeeperCanaryWithArgs(String[] args) throws Exception {
441    String hostPort = testingUtility.getZkCluster().getAddress().toString();
442    testingUtility.getConfiguration().set(HConstants.ZOOKEEPER_QUORUM, hostPort);
443    ExecutorService executor = new ScheduledThreadPoolExecutor(2);
444    CanaryTool.ZookeeperStdOutSink sink = spy(new CanaryTool.ZookeeperStdOutSink());
445    CanaryTool canary = new CanaryTool(executor, sink);
446    assertEquals(0, ToolRunner.run(testingUtility.getConfiguration(), canary, args));
447
448    String baseZnode = testingUtility.getConfiguration().get(HConstants.ZOOKEEPER_ZNODE_PARENT,
449      HConstants.DEFAULT_ZOOKEEPER_ZNODE_PARENT);
450    verify(sink, atLeastOnce()).publishReadTiming(eq(baseZnode), eq(hostPort), anyLong());
451  }
452
453  @Test
454  public void testWebUI() throws Exception {
455    CanaryTool.RegionStdOutSink sink = mock(CanaryTool.RegionStdOutSink.class);
456
457    Configuration configuration = HBaseConfiguration.create(testingUtility.getConfiguration());
458    int infoPort = 16666;
459    configuration.setInt(HBASE_CANARY_INFO_PORT, infoPort);
460
461    ExecutorService executorService = startCanaryToolInBackground(sink, configuration);
462
463    // Test that old canary status page URL redirects to JSP
464    URL oldPageUrl = new URL("http://localhost:" + infoPort + "/canary-status");
465    String oldPageContent = getPageContent(oldPageUrl);
466    assertTrue("expected=canary.jsp, content=" + oldPageContent,
467      oldPageContent.contains("canary.jsp"));
468
469    // Test web UI page content
470    URL url = new URL("http://localhost:" + infoPort + "/canary.jsp");
471    String page = getPageContent(url);
472
473    assertTrue("Page should contain page title.", page.contains("<title>Canary</title>"));
474
475    assertTrue("Page should contain Failed Servers header.",
476      page.contains("<h2>Failed Servers</h2>"));
477    assertTrue("Page should have zero Failed Servers.",
478      page.contains("<td>Total Failed Servers: 0</td>"));
479
480    assertTrue("Page should contain Failed Tables header.",
481      page.contains("<h2>Failed Tables</h2>"));
482    assertTrue("Page should have zero Failed Tables.",
483      page.contains("<td>Total Failed Tables: 0</td>"));
484
485    assertTrue("Page should contain Software Attributes header.",
486      page.contains("<h2>Software Attributes</h2>"));
487    assertTrue("Page should contain JVM version.",
488      page.contains("<td>" + JvmVersion.getVersion() + "</td>"));
489    assertTrue("Page should contain HBase version.", page
490      .contains("<td>" + VersionInfo.getVersion() + ", r" + VersionInfo.getRevision() + "</td>"));
491
492    // Stop Canary tool daemon
493    executorService.shutdown();
494  }
495
496  @Test
497  public void testWebUIWithFailures() throws Exception {
498    CanaryTool.RegionStdOutSink sink = mock(CanaryTool.RegionStdOutSink.class);
499
500    // Simulate a failed server
501    ServerName sn1 = ServerName.parseServerName("asf903.gq1.ygridcore.net,52690,1517835491385");
502    ConcurrentMap<ServerName, LongAdder> servers = new ConcurrentHashMap<>();
503    servers.put(sn1, new LongAdder());
504    when(sink.getPerServerFailuresCount()).thenReturn(servers);
505
506    // Simulate failed tables
507    ConcurrentMap<String, LongAdder> tables = new ConcurrentHashMap<>();
508    tables.put("awesome-table", new LongAdder());
509    tables.put("awesome-table-two", new LongAdder());
510    when(sink.getPerTableFailuresCount()).thenReturn(tables);
511
512    Configuration configuration = HBaseConfiguration.create(testingUtility.getConfiguration());
513    int infoPort = 16667;
514    configuration.setInt(HBASE_CANARY_INFO_PORT, infoPort);
515
516    ExecutorService executorService = startCanaryToolInBackground(sink, configuration);
517
518    URL url = new URL("http://localhost:" + infoPort + "/canary.jsp");
519    String page = getPageContent(url);
520
521    assertTrue("Page should contain page title.", page.contains("<title>Canary</title>"));
522
523    assertTrue("Page should contain Failed Servers header.",
524      page.contains("<h2>Failed Servers</h2>"));
525    assertTrue("Page should contain the failed server link.", page.contains(
526      "<a href=\"//asf903.gq1.ygridcore.net:52691/\">asf903.gq1.ygridcore.net,52690,1517835491385</a>"));
527    assertTrue("Page should summarize 1 failed server.",
528      page.contains("<td>Total Failed Servers: 1</td>"));
529
530    assertTrue("Page should contain Failed Tables header.",
531      page.contains("<h2>Failed Tables</h2>"));
532    assertTrue("Page should contain awesome-table as failed table link.",
533      page.contains("<td>awesome-table</td>"));
534    assertTrue("Page should contain awesome-table-two as failed table link.",
535      page.contains("<td>awesome-table-two</td>"));
536    assertTrue("Page should summarize 2 failed tables.",
537      page.contains("<td>Total Failed Tables: 2</td>"));
538
539    // Stop Canary tool daemon
540    executorService.shutdown();
541  }
542
543  private static ExecutorService startCanaryToolInBackground(CanaryTool.RegionStdOutSink sink,
544    Configuration configuration) {
545    ExecutorService canaryExecutor = new ScheduledThreadPoolExecutor(1);
546    CanaryTool canary = new CanaryTool(canaryExecutor, sink);
547    String[] args = { "-daemon", "-interval", "5", "-f", "false" };
548
549    // Run the Canary CLI tool in another thread otherwise it would block the unit test thread
550    // and we could not examine the web UI page.
551    ExecutorService executorService = new ScheduledThreadPoolExecutor(1);
552    executorService.submit(() -> ToolRunner.run(configuration, canary, args));
553    return executorService;
554  }
555
556  private String getPageContent(URL url) throws IOException, InterruptedException {
557    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
558
559    boolean success = false;
560    for (int i = 1; i <= 5; i++) {
561      try {
562        conn.connect();
563        if (
564          conn.getResponseCode() == 200 && "text/html;charset=utf-8".equals(conn.getContentType())
565        ) {
566          success = true;
567          break;
568        }
569      } catch (IOException e) {
570        // ignore connection error as we retry.
571      }
572
573      // Wait a bit for the Canary web UI to come up
574      TimeUnit.MILLISECONDS.sleep(100);
575    }
576
577    if (success) {
578      try (InputStream in = conn.getInputStream()) {
579        return IOUtils.toString(in, StandardCharsets.UTF_8);
580      }
581    } else {
582      throw new IllegalStateException("Could not get Canary status page.");
583    }
584  }
585}