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