HBASE-26921 Rewrite the counting cells part in TestMultiVersions (#4316)
[hbase.git] / hbase-common / src / main / java / org / apache / hadoop / hbase / util / DynamicClassLoader.java
blob9c242ab1f76465f4312951c560dc11d405474f32
1 /**
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
18 package org.apache.hadoop.hbase.util;
20 import java.io.File;
21 import java.io.IOException;
22 import java.net.MalformedURLException;
23 import java.net.URL;
24 import java.util.HashMap;
26 import org.apache.hadoop.conf.Configuration;
27 import org.apache.hadoop.fs.FileStatus;
28 import org.apache.hadoop.fs.FileSystem;
29 import org.apache.hadoop.fs.Path;
30 import org.apache.yetus.audience.InterfaceAudience;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
34 /**
35 * This is a class loader that can load classes dynamically from new
36 * jar files under a configured folder. The paths to the jar files are
37 * converted to URLs, and URLClassLoader logic is actually used to load
38 * classes. This class loader always uses its parent class loader
39 * to load a class at first. Only if its parent class loader
40 * can not load a class, we will try to load it using the logic here.
41 * <p>
42 * The configured folder can be a HDFS path. In this case, the jar files
43 * under that folder will be copied to local at first under ${hbase.local.dir}/jars/.
44 * The local copy will be updated if the remote copy is updated, according to its
45 * last modified timestamp.
46 * <p>
47 * We can't unload a class already loaded. So we will use the existing
48 * jar files we already know to load any class which can't be loaded
49 * using the parent class loader. If we still can't load the class from
50 * the existing jar files, we will check if any new jar file is added,
51 * if so, we will load the new jar file and try to load the class again.
52 * If still failed, a class not found exception will be thrown.
53 * <p>
54 * Be careful in uploading new jar files and make sure all classes
55 * are consistent, otherwise, we may not be able to load your
56 * classes properly.
58 @InterfaceAudience.Private
59 public class DynamicClassLoader extends ClassLoaderBase {
60 private static final Logger LOG = LoggerFactory.getLogger(DynamicClassLoader.class);
62 // Dynamic jars are put under ${hbase.local.dir}/jars/
63 private static final String DYNAMIC_JARS_DIR = File.separator
64 + "jars" + File.separator;
66 private static final String DYNAMIC_JARS_DIR_KEY = "hbase.dynamic.jars.dir";
68 private static final String DYNAMIC_JARS_OPTIONAL_CONF_KEY = "hbase.use.dynamic.jars";
69 private static final boolean DYNAMIC_JARS_OPTIONAL_DEFAULT = true;
71 // The user-provided value for using the DynamicClassLoader
72 private final boolean userConfigUseDynamicJars;
73 // The current state of whether to use the DynamicClassLoader
74 private final boolean useDynamicJars;
76 private File localDir;
78 // FileSystem of the remote path, set only if remoteDir != null
79 private FileSystem remoteDirFs;
80 private Path remoteDir;
82 // Last modified time of local jars
83 private HashMap<String, Long> jarModifiedTime;
85 /**
86 * Creates a DynamicClassLoader that can load classes dynamically
87 * from jar files under a specific folder.
89 * @param conf the configuration for the cluster.
90 * @param parent the parent ClassLoader to set.
92 public DynamicClassLoader(final Configuration conf, final ClassLoader parent) {
93 super(parent);
95 // Save off the user's original configuration value for the DynamicClassLoader
96 userConfigUseDynamicJars = conf.getBoolean(
97 DYNAMIC_JARS_OPTIONAL_CONF_KEY, DYNAMIC_JARS_OPTIONAL_DEFAULT);
99 boolean dynamicJarsEnabled = userConfigUseDynamicJars;
100 if (dynamicJarsEnabled) {
101 try {
102 initTempDir(conf);
103 dynamicJarsEnabled = true;
104 } catch (Exception e) {
105 LOG.error("Disabling the DynamicClassLoader as it failed to initialize its temp directory."
106 + " Check your configuration and filesystem permissions. Custom coprocessor code may"
107 + " not be loaded as a result of this failure.", e);
108 dynamicJarsEnabled = false;
111 useDynamicJars = dynamicJarsEnabled;
114 // FindBugs: Making synchronized to avoid IS2_INCONSISTENT_SYNC complaints about
115 // remoteDirFs and jarModifiedTime being part synchronized protected.
116 private synchronized void initTempDir(final Configuration conf) {
117 jarModifiedTime = new HashMap<>();
118 String localDirPath = conf.get(
119 LOCAL_DIR_KEY, DEFAULT_LOCAL_DIR) + DYNAMIC_JARS_DIR;
120 localDir = new File(localDirPath);
121 if (!localDir.mkdirs() && !localDir.isDirectory()) {
122 throw new RuntimeException("Failed to create local dir " + localDir.getPath()
123 + ", DynamicClassLoader failed to init");
126 String remotePath = conf.get(DYNAMIC_JARS_DIR_KEY);
127 if (remotePath == null || remotePath.equals(localDirPath)) {
128 remoteDir = null; // ignore if it is the same as the local path
129 } else {
130 remoteDir = new Path(remotePath);
131 try {
132 remoteDirFs = remoteDir.getFileSystem(conf);
133 } catch (IOException ioe) {
134 LOG.warn("Failed to identify the fs of dir "
135 + remoteDir + ", ignored", ioe);
136 remoteDir = null;
141 @Override
142 public Class<?> loadClass(String name)
143 throws ClassNotFoundException {
144 try {
145 return parent.loadClass(name);
146 } catch (ClassNotFoundException e) {
147 if (useDynamicJars) {
148 LOG.debug("Class {} not found - using dynamical class loader", name);
149 return tryRefreshClass(name);
150 } else if (userConfigUseDynamicJars) {
151 // If the user tried to enable the DCL, then warn again.
152 LOG.debug("Not checking DynamicClassLoader for missing class because it is disabled."
153 + " See the log for previous errors.");
155 throw e;
159 private Class<?> tryRefreshClass(String name) throws ClassNotFoundException {
160 synchronized (getClassLoadingLock(name)) {
161 // Check whether the class has already been loaded:
162 Class<?> clasz = findLoadedClass(name);
164 if (clasz != null) {
165 if (LOG.isDebugEnabled()) {
166 LOG.debug("Class {} already loaded", name);
168 } else {
169 try {
170 if (LOG.isDebugEnabled()) {
171 LOG.debug("Finding class: {}", name);
174 clasz = findClass(name);
175 } catch (ClassNotFoundException cnfe) {
176 // Load new jar files if any
177 if (LOG.isDebugEnabled()) {
178 LOG.debug("Loading new jar files, if any");
181 loadNewJars();
183 if (LOG.isDebugEnabled()) {
184 LOG.debug("Finding class again: {}", name);
187 clasz = findClass(name);
191 return clasz;
195 private synchronized void loadNewJars() {
196 // Refresh local jar file lists
197 File[] files = localDir == null ? null : localDir.listFiles();
198 if (files != null) {
199 for (File file : files) {
200 String fileName = file.getName();
201 if (jarModifiedTime.containsKey(fileName)) {
202 continue;
204 if (file.isFile() && fileName.endsWith(".jar")) {
205 jarModifiedTime.put(fileName, file.lastModified());
206 try {
207 URL url = file.toURI().toURL();
208 addURL(url);
209 } catch (MalformedURLException mue) {
210 // This should not happen, just log it
211 LOG.warn("Failed to load new jar " + fileName, mue);
217 // Check remote files
218 FileStatus[] statuses = null;
219 if (remoteDir != null) {
220 try {
221 statuses = remoteDirFs.listStatus(remoteDir);
222 } catch (IOException ioe) {
223 LOG.warn("Failed to check remote dir status " + remoteDir, ioe);
226 if (statuses == null || statuses.length == 0) {
227 return; // no remote files at all
230 for (FileStatus status: statuses) {
231 if (status.isDirectory()) {
232 continue; // No recursive lookup
235 Path path = status.getPath();
236 String fileName = path.getName();
237 if (!fileName.endsWith(".jar")) {
238 if (LOG.isDebugEnabled()) {
239 LOG.debug("Ignored non-jar file {}", fileName);
241 continue; // Ignore non-jar files
243 Long cachedLastModificationTime = jarModifiedTime.get(fileName);
244 if (cachedLastModificationTime != null) {
245 long lastModified = status.getModificationTime();
246 if (lastModified < cachedLastModificationTime) {
247 // There could be some race, for example, someone uploads
248 // a new one right in the middle the old one is copied to
249 // local. We can check the size as well. But it is still
250 // not guaranteed. This should be rare. Most likely,
251 // we already have the latest one.
252 // If you are unlucky to hit this race issue, you have
253 // to touch the remote jar to update its last modified time
254 continue;
257 try {
258 // Copy it to local
259 File dst = new File(localDir, fileName);
260 remoteDirFs.copyToLocalFile(path, new Path(dst.getPath()));
261 jarModifiedTime.put(fileName, dst.lastModified());
262 URL url = dst.toURI().toURL();
263 addURL(url);
264 } catch (IOException ioe) {
265 LOG.warn("Failed to load new jar " + fileName, ioe);