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}