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.assertNotNull; 022import static org.junit.jupiter.api.Assertions.assertNull; 023 024import java.lang.reflect.Method; 025import java.nio.file.Files; 026import java.nio.file.Path; 027import org.apache.hadoop.hbase.testclassification.MiscTests; 028import org.apache.hadoop.hbase.testclassification.SmallTests; 029import org.junit.jupiter.api.Tag; 030import org.junit.jupiter.api.Test; 031import org.junit.jupiter.api.io.TempDir; 032 033/** 034 * Verifies {@link ProfilerBackend#detect} fallback behaviour when 035 * {@code one.profiler.AsyncProfiler} is absent from the classpath. 036 * <p> 037 * Each test loads {@code ProfilerBackend} through a custom {@link ClassLoader} that blocks 038 * {@code one.profiler.*}, simulating a deployment where the async-profiler JAR was never packaged. 039 * This is the exact scenario for users who have async-profiler installed as a native binary 040 * ({@code ASYNC_PROFILER_HOME}) but are not allowed to bundle the JAR. 041 * <p> 042 * The split of {@link LibraryBackend} into its own file is what makes this possible: 043 * {@code ProfilerBackend.class} carries no static reference to {@code AsyncProfiler}, so the 044 * isolated loader can load it without a {@code NoClassDefFoundError}. 045 */ 046@Tag(MiscTests.TAG) 047@Tag(SmallTests.TAG) 048public class TestProfilerBackendIsolated { 049 050 @TempDir 051 Path tempDir; 052 053 /** 054 * When the library is absent AND no home is set, detect() must return null so that HttpServer 055 * registers DisabledServlet instead of crashing. 056 */ 057 @Test 058 public void testDetectReturnsNullWhenLibraryAbsentAndNoHome() throws Exception { 059 ClassLoader isolated = isolatedLoader(); 060 Method detect = detectMethod(isolated); 061 062 assertNull(detect.invoke(null, (String) null)); 063 assertNull(detect.invoke(null, "")); 064 assertNull(detect.invoke(null, " ")); 065 } 066 067 /** 068 * User has async-profiler installed as a native binary (ASYNC_PROFILER_HOME set, bin/asprof 069 * present) but no JAR on the classpath. detect() must return BinaryBackend. 070 */ 071 @Test 072 public void testDetectReturnsBinaryBackendWhenLibraryAbsentButHomeSet() throws Exception { 073 // Create a minimal fake profiler home with bin/asprof 074 Files.createDirectories(tempDir.resolve("bin")); 075 Files.createFile(tempDir.resolve("bin").resolve("asprof")); 076 077 ClassLoader isolated = isolatedLoader(); 078 Method detect = detectMethod(isolated); 079 080 Object backend = detect.invoke(null, tempDir.toString()); 081 assertNotNull(backend); 082 assertEquals("BinaryBackend", backend.getClass().getSimpleName()); 083 } 084 085 /** 086 * When the library IS on the classpath (normal test classpath), detect() must return 087 * LibraryBackend regardless of whether a home is set — library takes priority. 088 */ 089 @Test 090 public void testDetectReturnsLibraryBackendWhenLibraryPresent() { 091 // Use real classpath — async-profiler JAR is present as optional compile dep in tests 092 ProfilerBackend backend = ProfilerBackend.detect(null); 093 assertNotNull(backend); 094 assertEquals("LibraryBackend", backend.getClass().getSimpleName()); 095 } 096 097 /** 098 * Library present AND home set — LibraryBackend must still win (priority check). 099 */ 100 @Test 101 public void testDetectPrefersLibraryWhenBothPresent() throws Exception { 102 Files.createDirectories(tempDir.resolve("bin")); 103 Files.createFile(tempDir.resolve("bin").resolve("asprof")); 104 105 ProfilerBackend backend = ProfilerBackend.detect(tempDir.toString()); 106 assertNotNull(backend); 107 assertEquals("LibraryBackend", backend.getClass().getSimpleName()); 108 } 109 110 // ---- helpers ---- 111 112 /** 113 * Returns a ClassLoader that: - blocks {@code one.profiler.*} entirely (simulates absent 114 * async-profiler JAR) - reloads {@code org.apache.hadoop.hbase.http.*} classes fresh (so 115 * LibraryBackend resolves its own imports through this loader and also sees one.profiler.* as 116 * absent) - delegates everything else to the parent 117 */ 118 private ClassLoader isolatedLoader() { 119 ClassLoader parent = getClass().getClassLoader(); 120 return new ClassLoader(parent) { 121 @Override 122 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { 123 if (name.startsWith("one.profiler.")) { 124 throw new ClassNotFoundException("Simulated absent library: " + name); 125 } 126 // Force fresh load of our http package so LibraryBackend uses this loader 127 // (and therefore also sees one.profiler.* as absent when it tries to resolve it) 128 if (name.startsWith("org.apache.hadoop.hbase.http.")) { 129 Class<?> c = findLoadedClass(name); 130 if (c != null) { 131 return c; 132 } 133 // Load bytes from parent, define in this loader 134 String path = name.replace('.', '/') + ".class"; 135 try (java.io.InputStream in = parent.getResourceAsStream(path)) { 136 if (in != null) { 137 byte[] bytes = in.readAllBytes(); 138 c = defineClass(name, bytes, 0, bytes.length); 139 if (resolve) { 140 resolveClass(c); 141 } 142 return c; 143 } 144 } catch (java.io.IOException e) { 145 throw new ClassNotFoundException(name, e); 146 } 147 } 148 return super.loadClass(name, resolve); 149 } 150 }; 151 } 152 153 /** 154 * Loads {@code ProfilerBackend} through the given loader and returns its {@code detect(String)} 155 * method, made accessible across loader boundaries. 156 */ 157 private Method detectMethod(ClassLoader loader) throws Exception { 158 Class<?> backendClass = loader.loadClass("org.apache.hadoop.hbase.http.ProfilerBackend"); 159 Method m = backendClass.getMethod("detect", String.class); 160 m.setAccessible(true); 161 return m; 162 } 163}