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.http; 019 020import static org.junit.jupiter.api.Assertions.assertEquals; 021import static org.junit.jupiter.api.Assertions.assertFalse; 022import static org.junit.jupiter.api.Assertions.assertNotNull; 023import static org.junit.jupiter.api.Assertions.assertNull; 024import static org.junit.jupiter.api.Assertions.assertTrue; 025 026import java.io.File; 027import java.io.PrintWriter; 028import java.io.StringWriter; 029import java.lang.reflect.Field; 030import java.nio.charset.StandardCharsets; 031import java.nio.file.Files; 032import java.time.Instant; 033import java.util.Collections; 034import java.util.HashMap; 035import java.util.Map; 036import java.util.concurrent.CountDownLatch; 037import javax.servlet.ServletConfig; 038import javax.servlet.ServletContext; 039import javax.servlet.http.HttpServletRequest; 040import javax.servlet.http.HttpServletResponse; 041import org.apache.hadoop.conf.Configuration; 042import org.apache.hadoop.hbase.testclassification.MiscTests; 043import org.apache.hadoop.hbase.testclassification.SmallTests; 044import org.junit.jupiter.api.AfterEach; 045import org.junit.jupiter.api.BeforeEach; 046import org.junit.jupiter.api.Tag; 047import org.junit.jupiter.api.Test; 048import org.mockito.ArgumentCaptor; 049import org.mockito.Mockito; 050 051@Tag(MiscTests.TAG) 052@Tag(SmallTests.TAG) 053public class TestProfileServlet { 054 055 @BeforeEach 056 public void resetStaticStateBeforeEach() throws Exception { 057 clearLastResult(); 058 resetProfiling(); 059 } 060 061 @AfterEach 062 public void joinStopperThreadAfterEach() throws Exception { 063 // T1: stopper threads left running by a test can write lastResult after the test body 064 // returns, overwriting the @BeforeEach clear of the next test. Join any live stopper so 065 // the next test starts with fully settled static state. 066 for (Thread t : Thread.getAllStackTraces().keySet()) { 067 if ("ProfileServlet-stopper".equals(t.getName()) && t.isAlive()) { 068 t.join(5000); 069 } 070 } 071 } 072 073 // ---- parseProfileRequest ---- 074 075 @Test 076 public void testParseProfileRequestDefaults() { 077 ProfileServlet servlet = new ProfileServlet(null); 078 HttpServletRequest req = mockRequest(Collections.emptyMap(), "pid", null, "duration", null, 079 "output", null, "event", null, "interval", null, "jstackdepth", null, "bufsize", null, 080 "width", null, "height", null, "minwidth", null, "refreshDelay", null); 081 082 ProfileServlet.ProfileRequest parsed = servlet.parseProfileRequest(req); 083 assertNull(parsed.getPid()); 084 assertEquals(10, parsed.getDuration()); 085 assertEquals(ProfileServlet.Event.CPU, parsed.getEvent()); 086 assertEquals(ProfileServlet.Output.HTML, parsed.getOutput()); 087 assertFalse(parsed.isThread()); 088 assertFalse(parsed.isSimple()); 089 assertFalse(parsed.isReverse()); 090 } 091 092 @Test 093 public void testParseProfileRequestAllOptions() { 094 Map<String, String[]> flags = new HashMap<>(); 095 flags.put("thread", new String[] { "" }); 096 flags.put("simple", new String[] { "" }); 097 flags.put("reverse", new String[] { "" }); 098 099 ProfileServlet servlet = new ProfileServlet(null); 100 HttpServletRequest req = mockRequest(flags, "pid", "42", "duration", "60", "output", "tree", 101 "event", "alloc", "interval", "1000", "jstackdepth", "256", "bufsize", "100000", "width", 102 "1200", "height", "16", "minwidth", "0.5", "refreshDelay", "3"); 103 104 ProfileServlet.ProfileRequest parsed = servlet.parseProfileRequest(req); 105 assertEquals(42, parsed.getPid()); 106 assertEquals(60, parsed.getDuration()); 107 assertEquals(ProfileServlet.Output.TREE, parsed.getOutput()); 108 assertEquals(ProfileServlet.Event.ALLOC, parsed.getEvent()); 109 assertEquals(1000L, parsed.getInterval()); 110 assertEquals(256, parsed.getJstackDepth()); 111 assertEquals(100000L, parsed.getBufsize()); 112 assertEquals(1200, parsed.getWidth()); 113 assertEquals(16, parsed.getHeight()); 114 assertEquals(0.5, parsed.getMinwidth(), 1e-9); 115 assertEquals(3, parsed.getRefreshDelay()); 116 assertTrue(parsed.isThread()); 117 assertTrue(parsed.isSimple()); 118 assertTrue(parsed.isReverse()); 119 } 120 121 // ---- doGet ---- 122 123 @Test 124 public void testDoGetSetsRefreshHeaderAndCallsBackend() throws Exception { 125 ProfilerBackend mockBackend = Mockito.mock(ProfilerBackend.class); 126 Mockito.when(mockBackend.executeStart(Mockito.any(), Mockito.any())).thenReturn("OK"); 127 128 ProfileServlet servlet = new ProfileServlet(mockBackend); 129 servlet.init(mockServletConfig()); 130 131 HttpServletRequest req = mockRequest(Collections.emptyMap(), "pid", null, "duration", "1", 132 "refreshDelay", "2", "output", null, "event", null, "interval", null, "jstackdepth", null, 133 "bufsize", null, "width", null, "height", null, "minwidth", null); 134 135 HttpServletResponse resp = Mockito.mock(HttpServletResponse.class); 136 StringWriter body = new StringWriter(); 137 Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(body)); 138 139 servlet.doGet(req, resp); 140 141 Mockito.verify(mockBackend).executeStart(Mockito.any(), Mockito.any()); 142 Mockito.verify(resp).setStatus(HttpServletResponse.SC_ACCEPTED); 143 144 ArgumentCaptor<String> refreshCaptor = ArgumentCaptor.forClass(String.class); 145 Mockito.verify(resp).setHeader(Mockito.eq("Refresh"), refreshCaptor.capture()); 146 assertTrue(refreshCaptor.getValue().startsWith("3;")); 147 assertTrue(refreshCaptor.getValue().contains("/prof-output-hbase/")); 148 } 149 150 // ---- doGet error paths ---- 151 152 @Test 153 public void testDoGetBackendThrowsRuntimeException() throws Exception { 154 ProfilerBackend mockBackend = Mockito.mock(ProfilerBackend.class); 155 Mockito.when(mockBackend.executeStart(Mockito.any(), Mockito.any())) 156 .thenThrow(new IllegalStateException("profiler already started")); 157 158 ProfileServlet servlet = new ProfileServlet(mockBackend); 159 servlet.init(mockServletConfig()); 160 161 HttpServletRequest req = mockRequest(Collections.emptyMap(), "pid", null, "duration", "1", 162 "refreshDelay", null, "output", null, "event", null, "interval", null, "jstackdepth", null, 163 "bufsize", null, "width", null, "height", null, "minwidth", null); 164 HttpServletResponse resp = Mockito.mock(HttpServletResponse.class); 165 StringWriter body = new StringWriter(); 166 Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(body)); 167 168 servlet.doGet(req, resp); 169 170 Mockito.verify(resp).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 171 assertTrue(body.toString().contains("Profiler error")); 172 } 173 174 @Test 175 public void testDoGetBackendThrowsError() throws Exception { 176 ProfilerBackend mockBackend = Mockito.mock(ProfilerBackend.class); 177 Mockito.when(mockBackend.executeStart(Mockito.any(), Mockito.any())) 178 .thenThrow(new UnsatisfiedLinkError("no libasyncProfiler in java.library.path")); 179 180 ProfileServlet servlet = new ProfileServlet(mockBackend); 181 servlet.init(mockServletConfig()); 182 183 HttpServletRequest req = mockRequest(Collections.emptyMap(), "pid", null, "duration", "1", 184 "refreshDelay", null, "output", null, "event", null, "interval", null, "jstackdepth", null, 185 "bufsize", null, "width", null, "height", null, "minwidth", null); 186 HttpServletResponse resp = Mockito.mock(HttpServletResponse.class); 187 StringWriter body = new StringWriter(); 188 Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(body)); 189 190 servlet.doGet(req, resp); 191 192 Mockito.verify(resp).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 193 assertTrue(body.toString().contains("Profiler error")); 194 } 195 196 @Test 197 public void testDoGetRejectsNonPositivePidWith400() throws Exception { 198 ProfilerBackend mockBackend = Mockito.mock(ProfilerBackend.class); 199 ProfileServlet servlet = new ProfileServlet(mockBackend); 200 servlet.init(mockServletConfig()); 201 202 for (String badPid : new String[] { "-1", "0", "-999" }) { 203 HttpServletRequest req = mockRequest(Collections.emptyMap(), "pid", badPid, "duration", "1", 204 "refreshDelay", null, "output", null, "event", null, "interval", null, "jstackdepth", null, 205 "bufsize", null, "width", null, "height", null, "minwidth", null); 206 HttpServletResponse resp = Mockito.mock(HttpServletResponse.class); 207 StringWriter body = new StringWriter(); 208 Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(body)); 209 210 servlet.doGet(req, resp); 211 212 Mockito.verify(resp).setStatus(HttpServletResponse.SC_BAD_REQUEST); 213 assertTrue(body.toString().contains("Invalid pid"), 214 "Expected 'Invalid pid' in response for pid=" + badPid + ", got: " + body); 215 // backend must never be called for an invalid pid 216 Mockito.verify(mockBackend, Mockito.never()).executeStart(Mockito.any(), Mockito.any()); 217 218 // reset mocks for next iteration 219 Mockito.reset(resp, mockBackend); 220 } 221 } 222 223 @Test 224 public void testDoGetSecondRequestRejectedWithConflictWhenProfilingActive() throws Exception { 225 // T2: without synchronization the stopper's 1s sleep could finish before the second doGet 226 // runs on a slow CI runner, producing 202 instead of 409. Block executeStop on a latch so 227 // profiling=true is guaranteed to be set when the second request arrives. 228 CountDownLatch secondRequestIssued = new CountDownLatch(1); 229 ProfilerBackend mockBackend = Mockito.mock(ProfilerBackend.class); 230 Mockito.when(mockBackend.executeStart(Mockito.any(), Mockito.any())).thenReturn("OK"); 231 Mockito.when(mockBackend.executeStop(Mockito.any(), Mockito.any())).thenAnswer(inv -> { 232 secondRequestIssued.await(); 233 return ""; 234 }); 235 236 ProfileServlet servlet = new ProfileServlet(mockBackend); 237 servlet.init(mockServletConfig()); 238 239 HttpServletRequest req = mockRequest(Collections.emptyMap(), "pid", null, "duration", "1", 240 "refreshDelay", null, "output", null, "event", null, "interval", null, "jstackdepth", null, 241 "bufsize", null, "width", null, "height", null, "minwidth", null); 242 243 // First request succeeds and sets profiling=true. 244 HttpServletResponse resp1 = Mockito.mock(HttpServletResponse.class); 245 Mockito.when(resp1.getWriter()).thenReturn(new PrintWriter(new StringWriter())); 246 servlet.doGet(req, resp1); 247 Mockito.verify(resp1).setStatus(HttpServletResponse.SC_ACCEPTED); 248 249 // Second request must see profiling=true under the lock and return 409 CONFLICT. 250 HttpServletResponse resp2 = Mockito.mock(HttpServletResponse.class); 251 StringWriter body2 = new StringWriter(); 252 Mockito.when(resp2.getWriter()).thenReturn(new PrintWriter(body2)); 253 servlet.doGet(req, resp2); 254 secondRequestIssued.countDown(); 255 256 Mockito.verify(resp2).setStatus(HttpServletResponse.SC_CONFLICT); 257 assertTrue(body2.toString().contains("already running")); 258 } 259 260 // ---- doGet error paths — orphan file cleanup ---- 261 262 @Test 263 public void testDoGetDeletesOrphanFileWhenExecuteStartThrows() throws Exception { 264 ProfilerBackend mockBackend = Mockito.mock(ProfilerBackend.class); 265 ArgumentCaptor<File> fileCaptor = ArgumentCaptor.forClass(File.class); 266 Mockito.when(mockBackend.executeStart(Mockito.any(), fileCaptor.capture())) 267 .thenThrow(new IllegalStateException("profiler already started")); 268 269 ProfileServlet servlet = new ProfileServlet(mockBackend); 270 servlet.init(mockServletConfig()); 271 272 HttpServletRequest req = mockRequest(Collections.emptyMap(), "pid", null, "duration", "1", 273 "refreshDelay", null, "output", null, "event", null, "interval", null, "jstackdepth", null, 274 "bufsize", null, "width", null, "height", null, "minwidth", null); 275 HttpServletResponse resp = Mockito.mock(HttpServletResponse.class); 276 Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(new StringWriter())); 277 278 servlet.doGet(req, resp); 279 280 Mockito.verify(resp).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 281 // The placeholder created for this specific request must have been deleted. 282 File created = fileCaptor.getValue(); 283 assertFalse(created.exists(), 284 "Orphan placeholder must be deleted when executeStart fails: " + created.getName()); 285 } 286 287 @Test 288 public void testStopperThreadWritesPaddedErrorOnExecuteStopFailure() throws Exception { 289 ProfilerBackend mockBackend = Mockito.mock(ProfilerBackend.class); 290 Mockito.when(mockBackend.executeStart(Mockito.any(), Mockito.any())).thenReturn("OK"); 291 Mockito.when(mockBackend.executeStop(Mockito.any(), Mockito.any())) 292 .thenThrow(new IllegalStateException("stop failed")); 293 294 ProfileServlet servlet = new ProfileServlet(mockBackend); 295 servlet.init(mockServletConfig()); 296 297 HttpServletRequest req = mockRequest(Collections.emptyMap(), "pid", null, "duration", "1", 298 "refreshDelay", null, "output", null, "event", null, "interval", null, "jstackdepth", null, 299 "bufsize", null, "width", null, "height", null, "minwidth", null); 300 HttpServletResponse resp = Mockito.mock(HttpServletResponse.class); 301 ArgumentCaptor<String> refreshCaptor = ArgumentCaptor.forClass(String.class); 302 Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(new StringWriter())); 303 304 servlet.doGet(req, resp); 305 Mockito.verify(resp).setHeader(Mockito.eq("Refresh"), refreshCaptor.capture()); 306 307 // Extract the output file path from the Refresh header URL 308 String refreshValue = refreshCaptor.getValue(); 309 // Refresh header: "1;/prof-output-hbase/<filename>" 310 String relUrl = refreshValue.substring(refreshValue.indexOf(';') + 1); 311 String fileName = relUrl.substring(relUrl.lastIndexOf('/') + 1); 312 File outputFile = new File(ProfileServlet.OUTPUT_DIR, fileName); 313 314 // Wait for the stopper thread to finish (duration=1s + some buffer) 315 long deadline = System.currentTimeMillis() + 5000; 316 while ( 317 outputFile.length() < ProfileServlet.PROF_OUTPUT_MIN_BYTES 318 && System.currentTimeMillis() < deadline 319 ) { 320 Thread.sleep(50); 321 } 322 323 byte[] content = Files.readAllBytes(outputFile.toPath()); 324 assertTrue(content.length > ProfileServlet.PROF_OUTPUT_MIN_BYTES, 325 "Stopper must pad error file to > PROF_OUTPUT_MIN_BYTES so ProfileOutputServlet stops polling"); 326 assertTrue(new String(content, StandardCharsets.UTF_8).contains("stop failed"), 327 "Padded error file must contain the failure message"); 328 } 329 330 @Test 331 public void testStopperThreadWritesPaddedErrorOnInterrupt() throws Exception { 332 ProfilerBackend mockBackend = Mockito.mock(ProfilerBackend.class); 333 Mockito.when(mockBackend.executeStart(Mockito.any(), Mockito.any())).thenReturn("OK"); 334 335 // Use a long duration so the stopper is sleeping when we interrupt it 336 ProfileServlet servlet = new ProfileServlet(mockBackend); 337 servlet.init(mockServletConfig()); 338 339 HttpServletRequest req = mockRequest(Collections.emptyMap(), "pid", null, "duration", "60", 340 "refreshDelay", null, "output", null, "event", null, "interval", null, "jstackdepth", null, 341 "bufsize", null, "width", null, "height", null, "minwidth", null); 342 HttpServletResponse resp = Mockito.mock(HttpServletResponse.class); 343 ArgumentCaptor<String> refreshCaptor = ArgumentCaptor.forClass(String.class); 344 Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(new StringWriter())); 345 346 servlet.doGet(req, resp); 347 Mockito.verify(resp).setHeader(Mockito.eq("Refresh"), refreshCaptor.capture()); 348 349 String relUrl = refreshCaptor.getValue().substring(refreshCaptor.getValue().indexOf(';') + 1); 350 String fileName = relUrl.substring(relUrl.lastIndexOf('/') + 1); 351 File outputFile = new File(ProfileServlet.OUTPUT_DIR, fileName); 352 353 // Find the stopper thread by name and interrupt it while it is sleeping 354 Thread stopper = null; 355 long findDeadline = System.currentTimeMillis() + 2000; 356 while (stopper == null && System.currentTimeMillis() < findDeadline) { 357 for (Thread t : Thread.getAllStackTraces().keySet()) { 358 if ("ProfileServlet-stopper".equals(t.getName()) && t.isAlive()) { 359 stopper = t; 360 break; 361 } 362 } 363 if (stopper == null) { 364 Thread.sleep(10); 365 } 366 } 367 assertNotNull(stopper, "ProfileServlet-stopper thread must be alive during the sleep"); 368 stopper.interrupt(); 369 370 // Wait for the stopper to write the interrupted-session message 371 long deadline = System.currentTimeMillis() + 3000; 372 while ( 373 outputFile.length() < ProfileServlet.PROF_OUTPUT_MIN_BYTES 374 && System.currentTimeMillis() < deadline 375 ) { 376 Thread.sleep(50); 377 } 378 379 byte[] content = Files.readAllBytes(outputFile.toPath()); 380 assertTrue(content.length > ProfileServlet.PROF_OUTPUT_MIN_BYTES, 381 "Stopper must pad interrupted-session file to > PROF_OUTPUT_MIN_BYTES"); 382 assertTrue(new String(content, StandardCharsets.UTF_8).contains("interrupted"), 383 "Padded error file must contain 'interrupted'"); 384 } 385 386 // ---- ?last ---- 387 388 @Test 389 public void testLastReturns404WhenNoResultCached() throws Exception { 390 clearLastResult(); 391 392 ProfileServlet servlet = new ProfileServlet(null); 393 servlet.init(mockServletConfig()); 394 Map<String, String[]> params = new HashMap<>(); 395 params.put("last", new String[] { "" }); 396 HttpServletRequest req = mockRequest(params, "pid", null, "duration", null, "output", null, 397 "event", null, "interval", null, "jstackdepth", null, "bufsize", null, "width", null, 398 "height", null, "minwidth", null, "refreshDelay", null); 399 HttpServletResponse resp = Mockito.mock(HttpServletResponse.class); 400 StringWriter body = new StringWriter(); 401 Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(body)); 402 403 servlet.doGet(req, resp); 404 405 Mockito.verify(resp).setStatus(HttpServletResponse.SC_NOT_FOUND); 406 assertTrue(body.toString().contains("No profiling results available yet")); 407 } 408 409 @Test 410 public void testLastRedirectsToMostRecentResult() throws Exception { 411 // C8 fix: ?last checks Files.exists before redirecting, so the output file must exist on disk. 412 String fileName = "profile-cpu-20260612-120000.html"; 413 File outputDir = new File(ProfileServlet.OUTPUT_DIR); 414 outputDir.mkdirs(); 415 File outputFile = new File(outputDir, fileName); 416 outputFile.createNewFile(); 417 String expectedUrl = "/prof-output-hbase/" + fileName; 418 setLastResult(new ProfileServlet.ProfileResult(expectedUrl, "cpu", 10, Instant.now())); 419 420 ProfileServlet servlet = new ProfileServlet(null); 421 servlet.init(mockServletConfig()); 422 Map<String, String[]> params = new HashMap<>(); 423 params.put("last", new String[] { "" }); 424 HttpServletRequest req = mockRequest(params, "pid", null, "duration", null, "output", null, 425 "event", null, "interval", null, "jstackdepth", null, "bufsize", null, "width", null, 426 "height", null, "minwidth", null, "refreshDelay", null); 427 HttpServletResponse resp = Mockito.mock(HttpServletResponse.class); 428 Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(new StringWriter())); 429 430 servlet.doGet(req, resp); 431 432 Mockito.verify(resp).sendRedirect(expectedUrl); 433 } 434 435 @Test 436 public void testLastResultOverwrittenByNewSession() throws Exception { 437 // C8 fix: ?last checks Files.exists before redirecting, so output files must exist on disk. 438 File outputDir = new File(ProfileServlet.OUTPUT_DIR); 439 outputDir.mkdirs(); 440 File firstFile = new File(outputDir, "profile-cpu-first.html"); 441 firstFile.createNewFile(); 442 File secondFile = new File(outputDir, "profile-cpu-second.html"); 443 secondFile.createNewFile(); 444 String firstUrl = "/prof-output-hbase/profile-cpu-first.html"; 445 String secondUrl = "/prof-output-hbase/profile-cpu-second.html"; 446 setLastResult(new ProfileServlet.ProfileResult(firstUrl, "cpu", 10, Instant.now())); 447 448 // Overwrite with a second result — only the latest is kept 449 setLastResult(new ProfileServlet.ProfileResult(secondUrl, "cpu", 30, Instant.now())); 450 451 ProfileServlet servlet = new ProfileServlet(null); 452 servlet.init(mockServletConfig()); 453 Map<String, String[]> params = new HashMap<>(); 454 params.put("last", new String[] { "" }); 455 HttpServletRequest req = mockRequest(params, "pid", null, "duration", null, "output", null, 456 "event", null, "interval", null, "jstackdepth", null, "bufsize", null, "width", null, 457 "height", null, "minwidth", null, "refreshDelay", null); 458 HttpServletResponse resp = Mockito.mock(HttpServletResponse.class); 459 Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(new StringWriter())); 460 461 servlet.doGet(req, resp); 462 463 // Only the most recent result is cached — redirect must point to secondUrl, not firstUrl 464 Mockito.verify(resp).sendRedirect(secondUrl); 465 Mockito.verify(resp, Mockito.never()).sendRedirect(firstUrl); 466 } 467 468 // ---- parseProfileRequest — enum fallbacks ---- 469 470 @Test 471 public void testGetOutputFallsBackToHtmlOnUnknownValue() { 472 ProfileServlet servlet = new ProfileServlet(null); 473 HttpServletRequest req = mockRequest(Collections.emptyMap(), "output", "bogusformat", "pid", 474 null, "duration", null, "event", null, "interval", null, "jstackdepth", null, "bufsize", null, 475 "width", null, "height", null, "minwidth", null, "refreshDelay", null); 476 assertEquals(ProfileServlet.Output.HTML, servlet.parseProfileRequest(req).getOutput()); 477 } 478 479 @Test 480 public void testGetEventFallsBackToCpuOnUnknownValue() { 481 ProfileServlet servlet = new ProfileServlet(null); 482 HttpServletRequest req = mockRequest(Collections.emptyMap(), "event", "bogusevent", "pid", null, 483 "duration", null, "output", null, "interval", null, "jstackdepth", null, "bufsize", null, 484 "width", null, "height", null, "minwidth", null, "refreshDelay", null); 485 assertEquals(ProfileServlet.Event.CPU, servlet.parseProfileRequest(req).getEvent()); 486 } 487 488 @Test 489 public void testDurationClampedToMinOne() { 490 ProfileServlet servlet = new ProfileServlet(null); 491 HttpServletRequest req = mockRequest(Collections.emptyMap(), "duration", "0", "pid", null, 492 "output", null, "event", null, "interval", null, "jstackdepth", null, "bufsize", null, 493 "width", null, "height", null, "minwidth", null, "refreshDelay", null); 494 assertEquals(1, servlet.parseProfileRequest(req).getDuration()); 495 496 HttpServletRequest negReq = mockRequest(Collections.emptyMap(), "duration", "-5", "pid", null, 497 "output", null, "event", null, "interval", null, "jstackdepth", null, "bufsize", null, 498 "width", null, "height", null, "minwidth", null, "refreshDelay", null); 499 assertEquals(1, servlet.parseProfileRequest(negReq).getDuration()); 500 } 501 502 @Test 503 public void testDurationCappedAtMax() { 504 ProfileServlet servlet = new ProfileServlet(null); 505 HttpServletRequest req = mockRequest(Collections.emptyMap(), "duration", "999999", "pid", null, 506 "output", null, "event", null, "interval", null, "jstackdepth", null, "bufsize", null, 507 "width", null, "height", null, "minwidth", null, "refreshDelay", null); 508 assertEquals(ProfileServlet.MAX_DURATION_SECONDS, 509 servlet.parseProfileRequest(req).getDuration()); 510 } 511 512 // ---- isAvailable / getAsyncProfilerHome ---- 513 514 @Test 515 public void testIsAvailableDetectReturnsBackendWhenLibraryPresent() { 516 // async-profiler is on the test classpath (compile-time optional dep present in tests), 517 // so detect() returns LibraryBackend even with null home. 518 assertNotNull(ProfilerBackend.detect(null)); 519 } 520 521 @Test 522 public void testGetAsyncProfilerHomeSystemProperty() { 523 String key = "async.profiler.home"; 524 String prev = System.getProperty(key); 525 try { 526 System.setProperty(key, "/tmp/fake-profiler"); 527 assertEquals("/tmp/fake-profiler", ProfileServlet.getAsyncProfilerHome()); 528 } finally { 529 if (prev == null) { 530 System.clearProperty(key); 531 } else { 532 System.setProperty(key, prev); 533 } 534 } 535 } 536 537 // ---- DisabledServlet ---- 538 539 @Test 540 public void testDisabledServletReturns500WithDefaultReason() throws Exception { 541 ProfileServlet.DisabledServlet disabled = new ProfileServlet.DisabledServlet(); 542 disabled.init(mockServletConfig()); 543 HttpServletRequest req = Mockito.mock(HttpServletRequest.class); 544 HttpServletResponse resp = Mockito.mock(HttpServletResponse.class); 545 StringWriter body = new StringWriter(); 546 Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(body)); 547 548 disabled.doGet(req, resp); 549 550 Mockito.verify(resp).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 551 // No reason param set — falls back to the default message 552 assertTrue(body.toString().contains("disabled at startup")); 553 } 554 555 @Test 556 public void testDisabledServletReturns500WithCustomReason() throws Exception { 557 ProfileServlet.DisabledServlet disabled = new ProfileServlet.DisabledServlet(); 558 ServletConfig config = Mockito.mock(ServletConfig.class); 559 ServletContext ctx = Mockito.mock(ServletContext.class); 560 Mockito.when(config.getServletContext()).thenReturn(ctx); 561 Mockito.when(config.getInitParameter(ProfileServlet.DisabledServlet.REASON_PARAM)) 562 .thenReturn("disabled via hbase.profiler.enabled=false"); 563 disabled.init(config); 564 565 HttpServletRequest req = Mockito.mock(HttpServletRequest.class); 566 HttpServletResponse resp = Mockito.mock(HttpServletResponse.class); 567 StringWriter body = new StringWriter(); 568 Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(body)); 569 570 disabled.doGet(req, resp); 571 572 Mockito.verify(resp).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 573 assertTrue(body.toString().contains("hbase.profiler.enabled=false")); 574 } 575 576 // ---- helpers ---- 577 578 private HttpServletRequest mockRequest(Map<String, String[]> paramMap, String... kvPairs) { 579 HttpServletRequest req = Mockito.mock(HttpServletRequest.class); 580 Mockito.when(req.getParameterMap()).thenReturn(paramMap); 581 for (int i = 0; i < kvPairs.length; i += 2) { 582 Mockito.when(req.getParameter(kvPairs[i])).thenReturn(kvPairs[i + 1]); 583 } 584 return req; 585 } 586 587 private ServletConfig mockServletConfig() throws Exception { 588 ServletContext ctx = Mockito.mock(ServletContext.class); 589 Mockito.when(ctx.getAttribute(HttpServer.CONF_CONTEXT_ATTRIBUTE)) 590 .thenReturn(new Configuration(false)); 591 ServletConfig config = Mockito.mock(ServletConfig.class); 592 Mockito.when(config.getServletContext()).thenReturn(ctx); 593 return config; 594 } 595 596 private static Field lastResultField() throws Exception { 597 Field f = ProfileServlet.class.getDeclaredField("lastResult"); 598 f.setAccessible(true); 599 return f; 600 } 601 602 private static void setLastResult(ProfileServlet.ProfileResult result) throws Exception { 603 lastResultField().set(null, result); 604 } 605 606 private static void clearLastResult() throws Exception { 607 lastResultField().set(null, null); 608 } 609 610 private static void resetProfiling() throws Exception { 611 Field f = ProfileServlet.class.getDeclaredField("profiling"); 612 f.setAccessible(true); 613 f.set(null, false); 614 } 615}