mirror of https://github.com/sbt/sbt.git
Make LayeredClassLoaders parallel capable
The docs for ClassLoader, https://docs.oracle.com/javase/8/docs/api/java/lang/ClassLoader.html say that all non-hierarchical custom classloaders should be registered as parallel capable. The docs also suggest that custom classloaders should try to only override findClass so I reworked LayerdClassLoader to only override findClass. I also added locking to the class loading to make it safe for concurrent loading. All of the custom classloaders besides LayeredClassLoader either subclass URLClassLoader or LayeredClassLoader but don't override loadClass. Because those two classloaders are parallel capable, the subclasses should be as well. It isn't possible to make classloaders that are implemented in scala parallel capable because scala 2 doesn't support jvm static blocks (dotty does support this with an annotation). To work around this, I re-worked some of the classloaders so that they are either directly implemented in java or I subclassed a scala implementation class in java.
This commit is contained in:
parent
cc8c66c66d
commit
625470cdd5
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt.internal.classpath;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
|
||||
public class WrappedLoader extends URLClassLoader {
|
||||
static {
|
||||
ClassLoader.registerAsParallelCapable();
|
||||
}
|
||||
|
||||
WrappedLoader(final ClassLoader parent) {
|
||||
super(new URL[] {}, parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL[] getURLs() {
|
||||
final ClassLoader parent = getParent();
|
||||
return (parent instanceof URLClassLoader)
|
||||
? ((URLClassLoader) parent).getURLs()
|
||||
: super.getURLs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "WrappedClassLoader(" + getParent() + ")";
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ package sbt.internal.classpath
|
|||
import java.io.File
|
||||
import java.lang.management.ManagementFactory
|
||||
import java.lang.ref.{ Reference, ReferenceQueue, SoftReference }
|
||||
import java.net.{ URL, URLClassLoader }
|
||||
import java.net.URLClassLoader
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
import sbt.internal.inc.classpath.{
|
||||
|
|
@ -142,14 +142,6 @@ private[sbt] class ClassLoaderCache(
|
|||
private[this] val cleanupThread = new CleanupThread(ClassLoaderCache.threadID.getAndIncrement())
|
||||
private[this] val lock = new Object
|
||||
|
||||
private class WrappedLoader(parent: ClassLoader) extends URLClassLoader(Array.empty, parent) {
|
||||
// This is to make dotty work which extracts the URLs from the loader
|
||||
override def getURLs: Array[URL] = parent match {
|
||||
case u: URLClassLoader => u.getURLs
|
||||
case _ => Array.empty
|
||||
}
|
||||
override def toString: String = s"WrappedLoader($parent)"
|
||||
}
|
||||
private def close(classLoader: ClassLoader): Unit = classLoader match {
|
||||
case a: AutoCloseable => a.close()
|
||||
case _ =>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt.internal;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
|
||||
class FlatLoader extends URLClassLoader {
|
||||
static {
|
||||
ClassLoader.registerAsParallelCapable();
|
||||
}
|
||||
|
||||
FlatLoader(final URL[] urls, final ClassLoader parent) {
|
||||
super(urls, parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder jars = new StringBuilder();
|
||||
for (final URL u : getURLs()) {
|
||||
jars.append(" ");
|
||||
jars.append(u);
|
||||
jars.append("\n");
|
||||
}
|
||||
return "FlatLoader(\n parent = " + getParent() + "\n jars = " + jars.toString() + ")";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt.internal;
|
||||
|
||||
import java.io.File;
|
||||
import scala.collection.immutable.Map;
|
||||
import scala.collection.Seq;
|
||||
|
||||
class LayeredClassLoader extends LayeredClassLoaderImpl {
|
||||
LayeredClassLoader(
|
||||
final Seq<File> classpath,
|
||||
final ClassLoader parent,
|
||||
final Map<String, String> resources,
|
||||
final File tempDir) {
|
||||
super(classpath, parent, resources, tempDir);
|
||||
}
|
||||
|
||||
static {
|
||||
ClassLoader.registerAsParallelCapable();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt.internal;
|
||||
|
||||
import java.io.File;
|
||||
import scala.collection.immutable.Map;
|
||||
import scala.collection.Seq;
|
||||
|
||||
class ResourceLoader extends ResourceLoaderImpl {
|
||||
ResourceLoader(
|
||||
final Seq<File> classpath, final ClassLoader parent, final Map<String, String> resources) {
|
||||
super(classpath, parent, resources);
|
||||
}
|
||||
|
||||
static {
|
||||
ClassLoader.registerAsParallelCapable();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt.internal;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
|
||||
final class ScalaReflectClassLoader extends URLClassLoader {
|
||||
static {
|
||||
ClassLoader.registerAsParallelCapable();
|
||||
}
|
||||
|
||||
private final URL jar;
|
||||
|
||||
ScalaReflectClassLoader(final URL jar, final ClassLoader parent) {
|
||||
super(new URL[] {jar}, parent);
|
||||
this.jar = jar;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ScalaReflectClassLoader(" + jar + " parent = " + getParent() + ")";
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ package sbt
|
|||
package internal
|
||||
|
||||
import java.io.File
|
||||
import java.net.URLClassLoader
|
||||
import java.nio.file.Path
|
||||
|
||||
import sbt.ClassLoaderLayeringStrategy._
|
||||
|
|
@ -139,17 +138,12 @@ private[sbt] object ClassLoaders {
|
|||
case f if f.getName == "scala-reflect.jar" =>
|
||||
si.allJars.find(_.getName == "scala-reflect.jar")
|
||||
}.flatten
|
||||
class ScalaReflectClassLoader(jar: File)
|
||||
extends URLClassLoader(Array(jar.toURI.toURL), scalaLibraryLayer) {
|
||||
override def toString: String =
|
||||
s"ScalaReflectClassLoader($jar, parent = $scalaLibraryLayer)"
|
||||
}
|
||||
val scalaReflectLayer = scalaReflectJar
|
||||
.map { file =>
|
||||
cache.apply(
|
||||
file -> IO.getModifiedTimeOrZero(file) :: Nil,
|
||||
scalaLibraryLayer,
|
||||
() => new ScalaReflectClassLoader(file)
|
||||
() => new ScalaReflectClassLoader(file.toURI.toURL, scalaLibraryLayer)
|
||||
)
|
||||
}
|
||||
.getOrElse(scalaLibraryLayer)
|
||||
|
|
@ -201,19 +195,6 @@ private[sbt] object ClassLoaders {
|
|||
} else parent
|
||||
}
|
||||
|
||||
private class ResourceLoader(
|
||||
classpath: Seq[File],
|
||||
parent: ClassLoader,
|
||||
resources: Map[String, String]
|
||||
) extends LayeredClassLoader(classpath, parent, resources, new File("/dev/null")) {
|
||||
override def findClass(name: String): Class[_] = throw new ClassNotFoundException(name)
|
||||
override def loadClass(name: String, resolve: Boolean): Class[_] = {
|
||||
val clazz = parent.loadClass(name)
|
||||
if (resolve) resolveClass(clazz)
|
||||
clazz
|
||||
}
|
||||
override def toString: String = "ResourceLoader"
|
||||
}
|
||||
// Creates a one or two layered classloader for the provided classpaths depending on whether
|
||||
// or not the classpath contains any snapshots. If it does, the snapshots are placed in a layer
|
||||
// above the regular jar layer. This allows the snapshot layer to be invalidated without
|
||||
|
|
@ -235,14 +216,9 @@ private[sbt] object ClassLoaders {
|
|||
} else parent
|
||||
}
|
||||
|
||||
private[this] class FlatLoader(classpath: Seq[File], parent: ClassLoader)
|
||||
extends URLClassLoader(classpath.map(_.toURI.toURL).toArray, parent) {
|
||||
override def toString: String =
|
||||
s"FlatClassLoader(parent = $interfaceLoader, jars =\n${classpath.mkString("\n")}\n)"
|
||||
}
|
||||
// helper methods
|
||||
private def flatLoader(classpath: Seq[File], parent: ClassLoader): ClassLoader =
|
||||
new FlatLoader(classpath, parent)
|
||||
new FlatLoader(classpath.map(_.toURI.toURL).toArray, parent)
|
||||
private[this] def modifiedTimes(stamps: Seq[(Path, FileStamp)]): Seq[(File, Long)] = stamps.map {
|
||||
case (p, LastModified(lm)) => p.toFile -> lm
|
||||
case (p, _) =>
|
||||
|
|
|
|||
|
|
@ -1,112 +0,0 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt.internal
|
||||
|
||||
import java.io.File
|
||||
import java.net.URLClassLoader
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import sbt.internal.inc.classpath._
|
||||
import sbt.io.IO
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
||||
private[sbt] class LayeredClassLoader(
|
||||
classpath: Seq[File],
|
||||
parent: ClassLoader,
|
||||
override protected val resources: Map[String, String],
|
||||
tempDir: File,
|
||||
) extends URLClassLoader(classpath.toArray.map(_.toURI.toURL), parent)
|
||||
with RawResources
|
||||
with NativeCopyLoader
|
||||
with AutoCloseable {
|
||||
private[this] val nativeLibs = new java.util.HashSet[File]().asScala
|
||||
override protected val config = new NativeCopyConfig(
|
||||
tempDir,
|
||||
classpath,
|
||||
IO.parseClasspath(System.getProperty("java.library.path", ""))
|
||||
)
|
||||
override def findLibrary(name: String): String = {
|
||||
super.findLibrary(name) match {
|
||||
case null => null
|
||||
case l =>
|
||||
nativeLibs += new File(l)
|
||||
l
|
||||
}
|
||||
}
|
||||
|
||||
private[this] val loaded = new ConcurrentHashMap[String, Class[_]]
|
||||
/*
|
||||
* Override findClass to memoize its result. We need to do this because in loadClass we will
|
||||
* delegate to findClass if the current LayeredClassLoader cannot load a class but it is a
|
||||
* descendant of the thread's context class loader and a class loader below it in the layering
|
||||
* hierarchy is able to load the required class. Unlike loadClass, findClass does not cache
|
||||
* the result which would make it possible to return multiple versions of the same class.
|
||||
*/
|
||||
override def findClass(name: String): Class[_] = loaded.get(name) match {
|
||||
case null =>
|
||||
val res = super.findClass(name)
|
||||
loaded.putIfAbsent(name, res) match {
|
||||
case null => res
|
||||
case clazz => clazz
|
||||
}
|
||||
case c => c
|
||||
}
|
||||
override def loadClass(name: String, resolve: Boolean): Class[_] = {
|
||||
try super.loadClass(name, resolve)
|
||||
catch {
|
||||
case e: ClassNotFoundException =>
|
||||
val loaders = new ListBuffer[LayeredClassLoader]
|
||||
var currentLoader: ClassLoader = Thread.currentThread.getContextClassLoader
|
||||
do {
|
||||
currentLoader match {
|
||||
case cl: LayeredClassLoader if cl != this => loaders.prepend(cl)
|
||||
case _ =>
|
||||
}
|
||||
currentLoader = currentLoader.getParent
|
||||
} while (currentLoader != null && currentLoader != this)
|
||||
if (currentLoader == this) {
|
||||
val resourceName = name.replace('.', '/').concat(".class")
|
||||
loaders
|
||||
.collectFirst {
|
||||
case l if l.findResource(resourceName) != null =>
|
||||
val res = l.findClass(name)
|
||||
if (resolve) l.resolveClass(res)
|
||||
res
|
||||
}
|
||||
.getOrElse(throw e)
|
||||
} else throw e
|
||||
}
|
||||
}
|
||||
override def close(): Unit = nativeLibs.foreach(NativeLibs.delete)
|
||||
override def toString: String = s"""LayeredClassLoader(
|
||||
| classpath =
|
||||
| ${classpath mkString "\n "}
|
||||
| parent =
|
||||
| ${parent.toString.linesIterator.mkString("\n ")}
|
||||
|)""".stripMargin
|
||||
}
|
||||
|
||||
private[internal] object NativeLibs {
|
||||
private[this] val nativeLibs = new java.util.HashSet[File].asScala
|
||||
ShutdownHooks.add(() => {
|
||||
nativeLibs.foreach(IO.delete)
|
||||
IO.deleteIfEmpty(nativeLibs.map(_.getParentFile).toSet)
|
||||
nativeLibs.clear()
|
||||
})
|
||||
def addNativeLib(lib: String): Unit = {
|
||||
nativeLibs.add(new File(lib))
|
||||
()
|
||||
}
|
||||
def delete(file: File): Unit = {
|
||||
nativeLibs.remove(file)
|
||||
file.delete()
|
||||
()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt.internal
|
||||
|
||||
import java.io.File
|
||||
import java.net.URLClassLoader
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import sbt.internal.inc.classpath._
|
||||
import sbt.io.IO
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
||||
private[sbt] class LayeredClassLoaderImpl(
|
||||
classpath: Seq[File],
|
||||
parent: ClassLoader,
|
||||
override protected val resources: Map[String, String],
|
||||
tempDir: File,
|
||||
) extends URLClassLoader(classpath.toArray.map(_.toURI.toURL), parent)
|
||||
with RawResources
|
||||
with NativeCopyLoader
|
||||
with AutoCloseable {
|
||||
private[this] val nativeLibs = new java.util.HashSet[File]().asScala
|
||||
override protected val config = new NativeCopyConfig(
|
||||
tempDir,
|
||||
classpath,
|
||||
IO.parseClasspath(System.getProperty("java.library.path", ""))
|
||||
)
|
||||
override def findLibrary(name: String): String = {
|
||||
super.findLibrary(name) match {
|
||||
case null => null
|
||||
case l =>
|
||||
nativeLibs += new File(l)
|
||||
l
|
||||
}
|
||||
}
|
||||
|
||||
private[this] val loaded = new ConcurrentHashMap[String, Class[_]]
|
||||
private[this] val classLocks = new ConcurrentHashMap[String, AnyRef]()
|
||||
/*
|
||||
* Override findClass to both memoize its result and look down the class hierarchy to attempt to
|
||||
* load a missing class from a descendant loader. If we didn't cache the loaded classes,
|
||||
* then it would be possible for this class loader to load a different version of the class than
|
||||
* the descendant, which would likely cause a crash. The search for the class in the descendants
|
||||
* allows java reflection to work in cases where the class to load via reflection is not directly
|
||||
* visible to the class that is attempting to load it.
|
||||
*/
|
||||
override def findClass(name: String): Class[_] = loaded.get(name) match {
|
||||
case null =>
|
||||
val newLock = new AnyRef
|
||||
val lock = classLocks.putIfAbsent(name, newLock) match {
|
||||
case null => newLock
|
||||
case l => l
|
||||
}
|
||||
lock.synchronized {
|
||||
try {
|
||||
val clazz = super.findClass(name)
|
||||
loaded.putIfAbsent(name, clazz) match {
|
||||
case null => clazz
|
||||
case c => c
|
||||
}
|
||||
} catch {
|
||||
case e: ClassNotFoundException =>
|
||||
/*
|
||||
* If new threads are spawned, they inherit the context class loader of the parent
|
||||
* This means that if a test or run task spawns background threads to do work, then the
|
||||
* same context class loader is available on all of the background threads. In the test
|
||||
* and run tasks, we temporarily set the context class loader of the main sbt thread to
|
||||
* be the classloader generated by ClassLoaders.getLayers. This creates an environment
|
||||
* that looks somewhat like a forked jvm with the app classloader set to be the
|
||||
* generated class loader. If the test or run main changes the thread context class
|
||||
* loader, this search might fail even if it would have passed on the initial entry
|
||||
* into the method. Applications generally only modify the context classloader if they
|
||||
* are manually loading classes. It's likely that if an application generated
|
||||
* ClassLoader needs access to the classes in the sbt classpath, then it would be using
|
||||
* the original context class loader as the parent of the new context class loader
|
||||
* anyway.
|
||||
*
|
||||
* If we wanted to make this change permanent so that the user could not
|
||||
* override the global context classloader, we would possibly need to intercept the
|
||||
* classloading of java.lang.Thread itself to return a custom Thread class that mirrors
|
||||
* the java.lang.Thread api, but stores the context class loader in a custom field.
|
||||
*
|
||||
*/
|
||||
var currentLoader: ClassLoader = Thread.currentThread.getContextClassLoader
|
||||
val loaders = new ListBuffer[LayeredClassLoader]
|
||||
do {
|
||||
currentLoader match {
|
||||
case cl: LayeredClassLoader if cl != this => loaders.prepend(cl)
|
||||
case _ =>
|
||||
}
|
||||
currentLoader = currentLoader.getParent
|
||||
} while (currentLoader != null && currentLoader != this)
|
||||
if (currentLoader == this && loaders.nonEmpty) {
|
||||
val resourceName = name.replace('.', '/').concat(".class")
|
||||
loaders
|
||||
.collectFirst {
|
||||
case l if l.findResource(resourceName) != null => l.findClass(name)
|
||||
}
|
||||
.getOrElse(throw e)
|
||||
} else throw e
|
||||
}
|
||||
}
|
||||
case c => c
|
||||
}
|
||||
override def close(): Unit = nativeLibs.foreach(NativeLibs.delete)
|
||||
override def toString: String = s"""LayeredClassLoader(
|
||||
| classpath =
|
||||
| ${classpath mkString "\n "}
|
||||
| parent =
|
||||
| ${parent.toString.linesIterator.mkString("\n ")}
|
||||
|)""".stripMargin
|
||||
}
|
||||
|
||||
private[internal] class ResourceLoaderImpl(
|
||||
classpath: Seq[File],
|
||||
parent: ClassLoader,
|
||||
resources: Map[String, String]
|
||||
) extends LayeredClassLoader(classpath, parent, resources, new File("/dev/null")) {
|
||||
override def findClass(name: String): Class[_] = throw new ClassNotFoundException(name)
|
||||
override def loadClass(name: String, resolve: Boolean): Class[_] = {
|
||||
val clazz = parent.loadClass(name)
|
||||
if (resolve) resolveClass(clazz)
|
||||
clazz
|
||||
}
|
||||
override def toString: String = "ResourceLoader"
|
||||
}
|
||||
|
||||
private[internal] object NativeLibs {
|
||||
private[this] val nativeLibs = new java.util.HashSet[File].asScala
|
||||
ShutdownHooks.add(() => {
|
||||
nativeLibs.foreach(IO.delete)
|
||||
IO.deleteIfEmpty(nativeLibs.map(_.getParentFile).toSet)
|
||||
nativeLibs.clear()
|
||||
})
|
||||
def addNativeLib(lib: String): Unit = {
|
||||
nativeLibs.add(new File(lib))
|
||||
()
|
||||
}
|
||||
def delete(file: File): Unit = {
|
||||
nativeLibs.remove(file)
|
||||
file.delete()
|
||||
()
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue