HBASE-6308. Coprocessors should be loaded in a custom ClassLoader (James Baldassari)

git-svn-id: https://svn.apache.org/repos/asf/hbase/trunk@1372558 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Andrew Kyle Purtell 2012-08-13 19:31:00 +00:00
parent 697acb3ea0
commit e64fe02581
3 changed files with 260 additions and 30 deletions

View File

@ -0,0 +1,199 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.hbase.coprocessor;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* ClassLoader used to load Coprocessor instances.
*
* This ClassLoader always tries to load classes from the Coprocessor jar first
* before delegating to the parent ClassLoader, thus avoiding dependency
* conflicts between HBase's classpath and classes in the coprocessor's jar.
* Certain classes are exempt from being loaded by this ClassLoader because it
* would prevent them from being cast to the equivalent classes in the region
* server. For example, the Coprocessor interface needs to be loaded by the
* region server's ClassLoader to prevent a ClassCastException when casting the
* coprocessor implementation.
*
* This ClassLoader also handles resource loading. In most cases this
* ClassLoader will attempt to load resources from the coprocessor jar first
* before delegating to the parent. However, like in class loading,
* some resources need to be handled differently. For all of the Hadoop
* default configurations (e.g. hbase-default.xml) we will check the parent
* ClassLoader first to prevent issues such as failing the HBase default
* configuration version check.
*/
public class CoprocessorClassLoader extends URLClassLoader {
private static final Log LOG =
LogFactory.getLog(CoprocessorClassLoader.class);
/**
* If the class being loaded starts with any of these strings, we will skip
* trying to load it from the coprocessor jar and instead delegate
* directly to the parent ClassLoader.
*/
private static final String[] CLASS_PREFIX_EXEMPTIONS = new String[] {
// Java standard library:
"com.sun.",
"launcher.",
"java.",
"javax.",
"org.ietf",
"org.omg",
"org.w3c",
"org.xml",
"sunw.",
// Hadoop/HBase:
"org.apache.hadoop",
};
/**
* If the resource being loaded matches any of these patterns, we will first
* attempt to load the resource with the parent ClassLoader. Only if the
* resource is not found by the parent do we attempt to load it from the
* coprocessor jar.
*/
private static final Pattern[] RESOURCE_LOAD_PARENT_FIRST_PATTERNS =
new Pattern[] {
Pattern.compile("^[^-]+-default\\.xml$")
};
/**
* Creates a CoprocessorClassLoader that loads classes from the given paths.
* @param paths paths from which to load classes.
* @param parent the parent ClassLoader to set.
*/
public CoprocessorClassLoader(List<URL> paths, ClassLoader parent) {
super(paths.toArray(new URL[]{}), parent);
}
@Override
synchronized public Class<?> loadClass(String name)
throws ClassNotFoundException {
// Delegate to the parent immediately if this class is exempt
if (isClassExempt(name)) {
if (LOG.isDebugEnabled()) {
LOG.debug("Skipping exempt class " + name +
" - delegating directly to parent");
}
return super.loadClass(name);
}
// Check whether the class has already been loaded:
Class<?> clasz = findLoadedClass(name);
if (clasz != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Class " + name + " already loaded");
}
}
else {
try {
// Try to find this class using the URLs passed to this ClassLoader,
// which includes the coprocessor jar
if (LOG.isDebugEnabled()) {
LOG.debug("Finding class: " + name);
}
clasz = findClass(name);
} catch (ClassNotFoundException e) {
// Class not found using this ClassLoader, so delegate to parent
if (LOG.isDebugEnabled()) {
LOG.debug("Class " + name + " not found - delegating to parent");
}
try {
clasz = super.loadClass(name);
} catch (ClassNotFoundException e2) {
// Class not found in this ClassLoader or in the parent ClassLoader
// Log some debug output before rethrowing ClassNotFoundException
if (LOG.isDebugEnabled()) {
LOG.debug("Class " + name + " not found in parent loader");
}
throw e2;
}
}
}
return clasz;
}
@Override
synchronized public URL getResource(String name) {
URL resource = null;
boolean parentLoaded = false;
// Delegate to the parent first if necessary
if (loadResourceUsingParentFirst(name)) {
if (LOG.isDebugEnabled()) {
LOG.debug("Checking parent first for resource " + name);
}
resource = super.getResource(name);
parentLoaded = true;
}
if (resource == null) {
// Try to find the resource in the coprocessor jar
resource = findResource(name);
if ((resource == null) && !parentLoaded) {
// Not found in the coprocessor jar and we haven't attempted to load
// the resource in the parent yet; fall back to the parent
resource = super.getResource(name);
}
}
return resource;
}
/**
* Determines whether the given class should be exempt from being loaded
* by this ClassLoader.
* @param name the name of the class to test.
* @return true if the class should *not* be loaded by this ClassLoader;
* false otherwise.
*/
protected boolean isClassExempt(String name) {
for (String exemptPrefix : CLASS_PREFIX_EXEMPTIONS) {
if (name.startsWith(exemptPrefix)) {
return true;
}
}
return false;
}
/**
* Determines whether we should attempt to load the given resource using the
* parent first before attempting to load the resource using this ClassLoader.
* @param name the name of the resource to test.
* @return true if we should attempt to load the resource using the parent
* first; false if we should attempt to load the resource using this
* ClassLoader first.
*/
protected boolean loadResourceUsingParentFirst(String name) {
for (Pattern resourcePattern : RESOURCE_LOAD_PARENT_FIRST_PATTERNS) {
if (resourcePattern.matcher(name).matches()) {
return true;
}
}
return false;
}
}

View File

@ -45,7 +45,6 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
@ -161,6 +160,8 @@ public abstract class CoprocessorHost<E extends CoprocessorEnvironment> {
public E load(Path path, String className, int priority,
Configuration conf) throws IOException {
Class<?> implClass = null;
LOG.debug("Loading coprocessor class " + className + " with path " +
path + " and priority " + priority);
// Have we already loaded the class, perhaps from an earlier region open
// for the same table?
@ -168,12 +169,15 @@ public abstract class CoprocessorHost<E extends CoprocessorEnvironment> {
implClass = getClass().getClassLoader().loadClass(className);
} catch (ClassNotFoundException e) {
LOG.info("Class " + className + " needs to be loaded from a file - " +
path.toString() + ".");
path + ".");
// go ahead to load from file system.
}
// If not, load
if (implClass == null) {
if (path == null) {
throw new IOException("No jar path specified for " + className);
}
// copy the jar to the local filesystem
if (!path.toString().endsWith(".jar")) {
throw new IOException(path.toString() + ": not a jar file?");
@ -193,7 +197,6 @@ public abstract class CoprocessorHost<E extends CoprocessorEnvironment> {
aborts runaway user code */
// load the jar and get the implementation main class
String cp = System.getProperty("java.class.path");
// NOTE: Path.toURL is deprecated (toURI instead) but the URLClassLoader
// unsurprisingly wants URLs, not URIs; so we will use the deprecated
// method which returns URLs for as long as it is available
@ -215,11 +218,7 @@ public abstract class CoprocessorHost<E extends CoprocessorEnvironment> {
}
jarFile.close();
StringTokenizer st = new StringTokenizer(cp, File.pathSeparator);
while (st.hasMoreTokens()) {
paths.add((new File(st.nextToken())).getCanonicalFile().toURL());
}
ClassLoader cl = new URLClassLoader(paths.toArray(new URL[]{}),
ClassLoader cl = new CoprocessorClassLoader(paths,
this.getClass().getClassLoader());
Thread.currentThread().setContextClassLoader(cl);
try {

View File

@ -61,11 +61,12 @@ public class TestClassLoading {
static final String cpName3 = "TestCP3";
static final String cpName4 = "TestCP4";
static final String cpName5 = "TestCP5";
static final String cpName6 = "TestCP6";
private static Class regionCoprocessor1 = ColumnAggregationEndpoint.class;
private static Class regionCoprocessor2 = GenericEndpoint.class;
private static Class regionServerCoprocessor = SampleRegionWALObserver.class;
private static Class masterCoprocessor = BaseMasterObserver.class;
private static Class<?> regionCoprocessor1 = ColumnAggregationEndpoint.class;
private static Class<?> regionCoprocessor2 = GenericEndpoint.class;
private static Class<?> regionServerCoprocessor = SampleRegionWALObserver.class;
private static Class<?> masterCoprocessor = BaseMasterObserver.class;
private static final String[] regionServerSystemCoprocessors =
new String[]{
@ -296,6 +297,37 @@ public class TestClassLoading {
assertTrue("Class " + cpName3 + " was missing on a region", found);
}
@Test
// HBASE-6308: Test CP classloader is the CoprocessorClassLoader
public void testPrivateClassLoader() throws Exception {
File jarFile = buildCoprocessorJar(cpName4);
// create a table that references the jar
HTableDescriptor htd = new HTableDescriptor(cpName4);
htd.addFamily(new HColumnDescriptor("test"));
htd.setValue("COPROCESSOR$1", jarFile.toString() + "|" + cpName4 + "|" +
Coprocessor.PRIORITY_USER);
HBaseAdmin admin = TEST_UTIL.getHBaseAdmin();
admin.createTable(htd);
TEST_UTIL.waitTableAvailable(htd.getName(), 5000);
// verify that the coprocessor was loaded correctly
boolean found = false;
MiniHBaseCluster hbase = TEST_UTIL.getHBaseCluster();
for (HRegion region:
hbase.getRegionServer(0).getOnlineRegionsLocalContext()) {
if (region.getRegionNameAsString().startsWith(cpName4)) {
Coprocessor cp = region.getCoprocessorHost().findCoprocessor(cpName4);
if (cp != null) {
found = true;
assertEquals("Class " + cpName4 + " was not loaded by CoprocessorClassLoader",
cp.getClass().getClassLoader().getClass(), CoprocessorClassLoader.class);
}
}
}
assertTrue("Class " + cpName4 + " was missing on a region", found);
}
@Test
// HBase-3810: Registering a Coprocessor at HTableDescriptor should be
// less strict
@ -304,8 +336,8 @@ public class TestClassLoading {
File jarFile1 = buildCoprocessorJar(cpName1);
File jarFile2 = buildCoprocessorJar(cpName2);
File jarFile4 = buildCoprocessorJar(cpName4);
File jarFile5 = buildCoprocessorJar(cpName5);
File jarFile6 = buildCoprocessorJar(cpName6);
String cpKey1 = "COPROCESSOR$1";
String cpKey2 = " Coprocessor$2 ";
@ -328,13 +360,13 @@ public class TestClassLoading {
htd.setValue(cpKey3, cpValue3);
// add 2 coprocessor by using new htd.addCoprocessor() api
htd.addCoprocessor(cpName4, new Path(jarFile4.getPath()),
htd.addCoprocessor(cpName5, new Path(jarFile5.getPath()),
Coprocessor.PRIORITY_USER, null);
Map<String, String> kvs = new HashMap<String, String>();
kvs.put("k1", "v1");
kvs.put("k2", "v2");
kvs.put("k3", "v3");
htd.addCoprocessor(cpName5, new Path(jarFile5.getPath()),
htd.addCoprocessor(cpName6, new Path(jarFile6.getPath()),
Coprocessor.PRIORITY_USER, kvs);
HBaseAdmin admin = TEST_UTIL.getHBaseAdmin();
@ -349,9 +381,9 @@ public class TestClassLoading {
// verify that the coprocessor was loaded
boolean found_2 = false, found_1 = false, found_3 = false,
found_4 = false, found_5 = false;
boolean found5_k1 = false, found5_k2 = false, found5_k3 = false,
found5_k4 = false;
found_5 = false, found_6 = false;
boolean found6_k1 = false, found6_k2 = false, found6_k3 = false,
found6_k4 = false;
MiniHBaseCluster hbase = TEST_UTIL.getHBaseCluster();
for (HRegion region:
@ -364,17 +396,17 @@ public class TestClassLoading {
found_3 = found_3 ||
(region.getCoprocessorHost().findCoprocessor("SimpleRegionObserver")
!= null);
found_4 = found_4 ||
(region.getCoprocessorHost().findCoprocessor(cpName4) != null);
found_5 = found_5 ||
(region.getCoprocessorHost().findCoprocessor(cpName5) != null);
CoprocessorEnvironment env =
region.getCoprocessorHost().findCoprocessorEnvironment(cpName5);
region.getCoprocessorHost().findCoprocessorEnvironment(cpName6);
if (env != null) {
found_5 = true;
found_6 = true;
Configuration conf = env.getConfiguration();
found5_k1 = conf.get("k1") != null;
found5_k2 = conf.get("k2") != null;
found5_k3 = conf.get("k3") != null;
found6_k1 = conf.get("k1") != null;
found6_k2 = conf.get("k2") != null;
found6_k3 = conf.get("k3") != null;
}
}
}
@ -382,13 +414,13 @@ public class TestClassLoading {
assertTrue("Class " + cpName1 + " was missing on a region", found_1);
assertTrue("Class " + cpName2 + " was missing on a region", found_2);
assertTrue("Class SimpleRegionObserver was missing on a region", found_3);
assertTrue("Class " + cpName4 + " was missing on a region", found_4);
assertTrue("Class " + cpName5 + " was missing on a region", found_5);
assertTrue("Class " + cpName6 + " was missing on a region", found_6);
assertTrue("Configuration key 'k1' was missing on a region", found5_k1);
assertTrue("Configuration key 'k2' was missing on a region", found5_k2);
assertTrue("Configuration key 'k3' was missing on a region", found5_k3);
assertFalse("Configuration key 'k4' wasn't configured", found5_k4);
assertTrue("Configuration key 'k1' was missing on a region", found6_k1);
assertTrue("Configuration key 'k2' was missing on a region", found6_k2);
assertTrue("Configuration key 'k3' was missing on a region", found6_k3);
assertFalse("Configuration key 'k4' wasn't configured", found6_k4);
}
@Test