Remove resource layer for AllDependencyJars strategy

Changed resources were causing the dependency layer to be invalidated on
resource changes in turbo mode because the resource layer was in between
the scala library layer. This commit reworks the layers for the
AllDependencyJars strategy so that the top layer is able to load _all_
of the resources during a test run.

The resource layer was added to address the problem that dependencies
may need to be able to load resources from the project classpath but
wouldn't be able to do so if the dependencies were in a separate layer
from the rest of the classpath. The resource layer was a classloader
that could load any resource on the full classpath but couldn't load any
classes. When I added the resource layer, I was thinking that when
resources changed, the resource class loader needed to be invalidated.
Resources, however, are different from classes in that the same
ClassLoader can find the same resources in a different place because
getResource and getResourceAsStream just return locations but do not
actually do any loading.

Taking advantage of this, I add a proxy classloader for finding
resource locations to ReverseLookupClassLoader. We can reset the
classpath of the resource loader in
ReverseLookupClassLoaderHolder.checkout. This allows us to see the new
versions of the resources without invalidating the dependency layer.
This commit is contained in:
Ethan Atkins 2019-07-29 11:52:42 -07:00
parent be489e05ca
commit 621789eeb2
3 changed files with 25 additions and 64 deletions

View File

@ -1,23 +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 scala.collection.immutable.Map;
import scala.collection.Seq;
final class ResourceLoader extends ResourceLoaderImpl {
ResourceLoader(
final Seq<File> classpath, final ClassLoader parent, final Map<String, String> resources) {
super(classpath, parent, resources);
}
static {
ClassLoader.registerAsParallelCapable();
}
}

View File

@ -35,12 +35,10 @@ private[sbt] object ClassLoaders {
if (si.isManagedVersion) rawCP if (si.isManagedVersion) rawCP
else si.libraryJars.map(j => j -> IO.getModifiedTimeOrZero(j)).toSeq ++ rawCP else si.libraryJars.map(j => j -> IO.getModifiedTimeOrZero(j)).toSeq ++ rawCP
val exclude = dependencyJars(exportedProducts).value.toSet ++ si.libraryJars val exclude = dependencyJars(exportedProducts).value.toSet ++ si.libraryJars
val resourceCP = modifiedTimes((outputFileStamps in resources).value)
buildLayers( buildLayers(
strategy = classLoaderLayeringStrategy.value, strategy = classLoaderLayeringStrategy.value,
si = si, si = si,
fullCP = fullCP, fullCP = fullCP,
resourceCP = resourceCP,
allDependencies = dependencyJars(dependencyClasspath).value.filterNot(exclude), allDependencies = dependencyJars(dependencyClasspath).value.filterNot(exclude),
cache = extendedClassLoaderCache.value, cache = extendedClassLoaderCache.value,
resources = ClasspathUtilities.createClasspathResources(fullCP.map(_._1), si), resources = ClasspathUtilities.createClasspathResources(fullCP.map(_._1), si),
@ -55,7 +53,6 @@ private[sbt] object ClassLoaders {
val s = streams.value val s = streams.value
val opts = forkOptions.value val opts = forkOptions.value
val options = javaOptions.value val options = javaOptions.value
val resourceCP = modifiedTimes((outputFileStamps in resources).value)
if (fork.value) { if (fork.value) {
s.log.debug(s"javaOptions: $options") s.log.debug(s"javaOptions: $options")
Def.task(new ForkRun(opts)) Def.task(new ForkRun(opts))
@ -85,7 +82,6 @@ private[sbt] object ClassLoaders {
strategy = classLoaderLayeringStrategy.value: @sbtUnchecked, strategy = classLoaderLayeringStrategy.value: @sbtUnchecked,
si = instance, si = instance,
fullCP = classpath.map(f => f -> IO.getModifiedTimeOrZero(f)), fullCP = classpath.map(f => f -> IO.getModifiedTimeOrZero(f)),
resourceCP = resourceCP,
allDependencies = transformedDependencies, allDependencies = transformedDependencies,
cache = extendedClassLoaderCache.value: @sbtUnchecked, cache = extendedClassLoaderCache.value: @sbtUnchecked,
resources = ClasspathUtilities.createClasspathResources(classpath, instance), resources = ClasspathUtilities.createClasspathResources(classpath, instance),
@ -118,7 +114,6 @@ private[sbt] object ClassLoaders {
strategy: ClassLoaderLayeringStrategy, strategy: ClassLoaderLayeringStrategy,
si: ScalaInstance, si: ScalaInstance,
fullCP: Seq[(File, Long)], fullCP: Seq[(File, Long)],
resourceCP: Seq[(File, Long)],
allDependencies: Seq[File], allDependencies: Seq[File],
cache: ClassLoaderCache, cache: ClassLoaderCache,
resources: Map[String, String], resources: Map[String, String],
@ -156,34 +151,28 @@ private[sbt] object ClassLoaders {
} }
.getOrElse(scalaLibraryLayer) .getOrElse(scalaLibraryLayer)
// layer 2 (resources) // layer 2 (optional if in the test config and the runtime layer is not shared)
val resourceLayer = val dependencyLayer: ClassLoader =
if (layerDependencies)
getResourceLayer(cpFiles, resourceCP, scalaReflectLayer, cache, resources)
else scalaReflectLayer
// layer 3 (optional if in the test config and the runtime layer is not shared)
val dependencyLayer =
if (layerDependencies && allDependencies.nonEmpty) { if (layerDependencies && allDependencies.nonEmpty) {
cache( cache(
allDependencies.toList.map(f => f -> IO.getModifiedTimeOrZero(f)), allDependencies.toList.map(f => f -> IO.getModifiedTimeOrZero(f)),
resourceLayer, scalaReflectLayer,
() => new ReverseLookupClassLoaderHolder(allDependencies, resourceLayer) () => new ReverseLookupClassLoaderHolder(allDependencies, scalaReflectLayer)
) )
} else resourceLayer } else scalaReflectLayer
val scalaJarNames = (si.libraryJars ++ scalaReflectJar).map(_.getName).toSet val scalaJarNames = (si.libraryJars ++ scalaReflectJar).map(_.getName).toSet
// layer 4 // layer 3
val filteredSet = val filteredSet =
if (layerDependencies) allDependencies.toSet ++ si.libraryJars ++ scalaReflectJar if (layerDependencies) allDependencies.toSet ++ si.libraryJars ++ scalaReflectJar
else Set(si.libraryJars ++ scalaReflectJar: _*) else Set(si.libraryJars ++ scalaReflectJar: _*)
val dynamicClasspath = cpFiles.filterNot(f => filteredSet(f) || scalaJarNames(f.getName)) val dynamicClasspath = cpFiles.filterNot(f => filteredSet(f) || scalaJarNames(f.getName))
dependencyLayer match { dependencyLayer match {
case dl: ReverseLookupClassLoaderHolder => case dl: ReverseLookupClassLoaderHolder =>
dl.checkout(dynamicClasspath, tmp) dl.checkout(cpFiles, tmp)
case cl => case cl =>
cl.getParent match { cl.getParent match {
case dl: ReverseLookupClassLoaderHolder => dl.checkout(dynamicClasspath, tmp) case dl: ReverseLookupClassLoaderHolder => dl.checkout(cpFiles, tmp)
case _ => new LayeredClassLoader(dynamicClasspath, cl, tmp) case _ => new LayeredClassLoader(dynamicClasspath, cl, tmp)
} }
} }
@ -194,24 +183,6 @@ private[sbt] object ClassLoaders {
key: sbt.TaskKey[Seq[Attributed[File]]] key: sbt.TaskKey[Seq[Attributed[File]]]
): Def.Initialize[Task[Seq[File]]] = Def.task(data(key.value).filter(_.getName.endsWith(".jar"))) ): Def.Initialize[Task[Seq[File]]] = Def.task(data(key.value).filter(_.getName.endsWith(".jar")))
// 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
// invalidating the regular jar layer. If the classpath is empty, it just returns the parent
// loader.
private def getResourceLayer(
classpath: Seq[File],
resources: Seq[(File, Long)],
parent: ClassLoader,
cache: ClassLoaderCache,
resourceMap: Map[String, String]
): ClassLoader = {
if (resources.nonEmpty) {
val mkLoader = () => new ResourceLoader(classpath, parent, resourceMap)
cache(resources.toList, parent, mkLoader)
} else parent
}
// helper methods // helper methods
private def flatLoader(classpath: Seq[File], parent: ClassLoader): ClassLoader = private def flatLoader(classpath: Seq[File], parent: ClassLoader): ClassLoader =
new FlatLoader(classpath.map(_.toURI.toURL).toArray, parent) new FlatLoader(classpath.map(_.toURI.toURL).toArray, parent)

View File

@ -8,7 +8,7 @@
package sbt.internal package sbt.internal
import java.io.File import java.io.File
import java.net.URLClassLoader import java.net.{ URL, URLClassLoader }
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference } import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
@ -69,7 +69,7 @@ private[internal] final class ReverseLookupClassLoaderHolder(
* *
* @return a ClassLoader * @return a ClassLoader
*/ */
def checkout(dependencyClasspath: Seq[File], tempDir: File): ClassLoader = { def checkout(fullClasspath: Seq[File], tempDir: File): ClassLoader = {
if (closed.get()) { if (closed.get()) {
val msg = "Tried to extract class loader from closed ReverseLookupClassLoaderHolder. " + val msg = "Tried to extract class loader from closed ReverseLookupClassLoaderHolder. " +
"Try running the `clearCaches` command and re-trying." "Try running the `clearCaches` command and re-trying."
@ -79,8 +79,8 @@ private[internal] final class ReverseLookupClassLoaderHolder(
case null => new ReverseLookupClassLoader case null => new ReverseLookupClassLoader
case c => c case c => c
} }
reverseLookupClassLoader.setTempDir(tempDir) reverseLookupClassLoader.setup(tempDir, fullClasspath)
new BottomClassLoader(dependencyClasspath, reverseLookupClassLoader, tempDir) new BottomClassLoader(fullClasspath, reverseLookupClassLoader, tempDir)
} }
private def checkin(reverseLookupClassLoader: ReverseLookupClassLoader): Unit = { private def checkin(reverseLookupClassLoader: ReverseLookupClassLoader): Unit = {
@ -149,6 +149,19 @@ private[internal] final class ReverseLookupClassLoaderHolder(
private[this] val classLoadingLock = new ClassLoadingLock private[this] val classLoadingLock = new ClassLoadingLock
def isDirty: Boolean = dirty.get() def isDirty: Boolean = dirty.get()
def setDescendant(classLoader: BottomClassLoader): Unit = directDescendant.set(classLoader) def setDescendant(classLoader: BottomClassLoader): Unit = directDescendant.set(classLoader)
private[this] val resourceLoader = new AtomicReference[ResourceLoader](null)
private class ResourceLoader(cp: Seq[File])
extends URLClassLoader(cp.map(_.toURI.toURL).toArray, parent) {
def lookup(name: String): URL = findResource(name)
}
private[ReverseLookupClassLoaderHolder] def setup(tmpDir: File, cp: Seq[File]): Unit = {
setTempDir(tmpDir)
resourceLoader.set(new ResourceLoader(cp))
}
override def findResource(name: String): URL = resourceLoader.get() match {
case null => null
case l => l.lookup(name)
}
def loadClass(name: String, resolve: Boolean, reverseLookup: Boolean): Class[_] = { def loadClass(name: String, resolve: Boolean, reverseLookup: Boolean): Class[_] = {
classLoadingLock.withLock(name) { classLoadingLock.withLock(name) {
try super.loadClass(name, resolve) try super.loadClass(name, resolve)