View Javadoc

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
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
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.
17   */
18  package org.apache.hadoop.hbase.util;
19  
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;
25  
26  import org.apache.commons.logging.Log;
27  import org.apache.commons.logging.LogFactory;
28  import org.apache.hadoop.conf.Configuration;
29  import org.apache.hadoop.fs.FileStatus;
30  import org.apache.hadoop.fs.FileSystem;
31  import org.apache.hadoop.fs.Path;
32  import org.apache.hadoop.hbase.classification.InterfaceAudience;
33  
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.
57   */
58  @InterfaceAudience.Private
59  public class DynamicClassLoader extends ClassLoaderBase {
60    private static final Log LOG =
61        LogFactory.getLog(DynamicClassLoader.class);
62  
63    // Dynamic jars are put under ${hbase.local.dir}/jars/
64    private static final String DYNAMIC_JARS_DIR = File.separator
65      + "jars" + File.separator;
66  
67    private static final String DYNAMIC_JARS_DIR_KEY = "hbase.dynamic.jars.dir";
68  
69    private static final String DYNAMIC_JARS_OPTIONAL_CONF_KEY = "hbase.use.dynamic.jars";
70    private static final boolean DYNAMIC_JARS_OPTIONAL_DEFAULT = true;
71  
72    // The user-provided value for using the DynamicClassLoader
73    private final boolean userConfigUseDynamicJars;
74    // The current state of whether to use the DynamicClassLoader
75    private final boolean useDynamicJars;
76  
77    private File localDir;
78  
79    // FileSystem of the remote path, set only if remoteDir != null
80    private FileSystem remoteDirFs;
81    private Path remoteDir;
82  
83    // Last modified time of local jars
84    private HashMap<String, Long> jarModifiedTime;
85  
86    /**
87     * Creates a DynamicClassLoader that can load classes dynamically
88     * from jar files under a specific folder.
89     *
90     * @param conf the configuration for the cluster.
91     * @param parent the parent ClassLoader to set.
92     */
93    public DynamicClassLoader(final Configuration conf, final ClassLoader parent) {
94      super(parent);
95  
96      // Save off the user's original configuration value for the DynamicClassLoader
97      userConfigUseDynamicJars = conf.getBoolean(
98          DYNAMIC_JARS_OPTIONAL_CONF_KEY, DYNAMIC_JARS_OPTIONAL_DEFAULT);
99  
100     boolean dynamicJarsEnabled = userConfigUseDynamicJars;
101     if (dynamicJarsEnabled) {
102       try {
103         initTempDir(conf);
104         dynamicJarsEnabled = true;
105       } catch (Exception e) {
106         LOG.error("Disabling the DynamicClassLoader as it failed to initialize its temp directory."
107             + " Check your configuration and filesystem permissions. Custom coprocessor code may"
108             + " not be loaded as a result of this failure.", e);
109         dynamicJarsEnabled = false;
110       }
111     }
112     useDynamicJars = dynamicJarsEnabled;
113   }
114 
115   // FindBugs: Making synchronized to avoid IS2_INCONSISTENT_SYNC complaints about
116   // remoteDirFs and jarModifiedTime being part synchronized protected.
117   private synchronized void initTempDir(final Configuration conf) {
118     jarModifiedTime = new HashMap<>();
119     String localDirPath = conf.get(
120       LOCAL_DIR_KEY, DEFAULT_LOCAL_DIR) + DYNAMIC_JARS_DIR;
121     localDir = new File(localDirPath);
122     if (!localDir.mkdirs() && !localDir.isDirectory()) {
123       throw new RuntimeException("Failed to create local dir " + localDir.getPath()
124         + ", DynamicClassLoader failed to init");
125     }
126 
127     String remotePath = conf.get(DYNAMIC_JARS_DIR_KEY);
128     if (remotePath == null || remotePath.equals(localDirPath)) {
129       remoteDir = null;  // ignore if it is the same as the local path
130     } else {
131       remoteDir = new Path(remotePath);
132       try {
133         remoteDirFs = remoteDir.getFileSystem(conf);
134       } catch (IOException ioe) {
135         LOG.warn("Failed to identify the fs of dir "
136           + remoteDir + ", ignored", ioe);
137         remoteDir = null;
138       }
139     }
140   }
141 
142   @Override
143   public Class<?> loadClass(String name)
144       throws ClassNotFoundException {
145     try {
146       return parent.loadClass(name);
147     } catch (ClassNotFoundException e) {
148       if (useDynamicJars) {
149         LOG.debug("Class " + name + " not found - using dynamical class loader");
150         return tryRefreshClass(name);
151       } else if (userConfigUseDynamicJars) {
152         // If the user tried to enable the DCL, then warn again.
153         LOG.debug("Not checking DynamicClassLoader for missing class because it is disabled."
154             + " See the log for previous errors.");
155       }
156       throw e;
157     }
158   }
159 
160   private Class<?> tryRefreshClass(String name) throws ClassNotFoundException {
161     synchronized (getClassLoadingLock(name)) {
162       // Check whether the class has already been loaded:
163       Class<?> clasz = findLoadedClass(name);
164 
165       if (clasz != null) {
166         if (LOG.isDebugEnabled()) {
167           LOG.debug("Class " + name + " already loaded");
168         }
169       } else {
170         try {
171           if (LOG.isDebugEnabled()) {
172             LOG.debug("Finding class: " + name);
173           }
174 
175           clasz = findClass(name);
176         } catch (ClassNotFoundException cnfe) {
177           // Load new jar files if any
178           if (LOG.isDebugEnabled()) {
179             LOG.debug("Loading new jar files, if any");
180           }
181 
182           loadNewJars();
183 
184           if (LOG.isDebugEnabled()) {
185             LOG.debug("Finding class again: " + name);
186           }
187 
188           clasz = findClass(name);
189         }
190       }
191 
192       return clasz;
193     }
194   }
195 
196   private synchronized void loadNewJars() {
197     // Refresh local jar file lists
198     File[] files = localDir == null ? null : localDir.listFiles();
199     if (files != null) {
200       for (File file : files) {
201         String fileName = file.getName();
202         if (jarModifiedTime.containsKey(fileName)) {
203           continue;
204         }
205         if (file.isFile() && fileName.endsWith(".jar")) {
206           jarModifiedTime.put(fileName, file.lastModified());
207           try {
208             URL url = file.toURI().toURL();
209             addURL(url);
210           } catch (MalformedURLException mue) {
211             // This should not happen, just log it
212             LOG.warn("Failed to load new jar " + fileName, mue);
213           }
214         }
215       }
216     }
217 
218     // Check remote files
219     FileStatus[] statuses = null;
220     if (remoteDir != null) {
221       try {
222         statuses = remoteDirFs.listStatus(remoteDir);
223       } catch (IOException ioe) {
224         LOG.warn("Failed to check remote dir status " + remoteDir, ioe);
225       }
226     }
227     if (statuses == null || statuses.length == 0) {
228       return; // no remote files at all
229     }
230 
231     for (FileStatus status: statuses) {
232       if (status.isDirectory()) {
233         continue; // No recursive lookup
234       }
235 
236       Path path = status.getPath();
237       String fileName = path.getName();
238       if (!fileName.endsWith(".jar")) {
239         if (LOG.isDebugEnabled()) {
240           LOG.debug("Ignored non-jar file " + fileName);
241         }
242         continue; // Ignore non-jar files
243       }
244       Long cachedLastModificationTime = jarModifiedTime.get(fileName);
245       if (cachedLastModificationTime != null) {
246         long lastModified = status.getModificationTime();
247         if (lastModified < cachedLastModificationTime) {
248           // There could be some race, for example, someone uploads
249           // a new one right in the middle the old one is copied to
250           // local. We can check the size as well. But it is still
251           // not guaranteed. This should be rare. Most likely,
252           // we already have the latest one.
253           // If you are unlucky to hit this race issue, you have
254           // to touch the remote jar to update its last modified time
255           continue;
256         }
257       }
258       try {
259         // Copy it to local
260         File dst = new File(localDir, fileName);
261         remoteDirFs.copyToLocalFile(path, new Path(dst.getPath()));
262         jarModifiedTime.put(fileName, dst.lastModified());
263         URL url = dst.toURI().toURL();
264         addURL(url);
265       } catch (IOException ioe) {
266         LOG.warn("Failed to load new jar " + fileName, ioe);
267       }
268     }
269   }
270 }