Skip to content

Commit a966b7d

Browse files
committed
Don't serve stale JAR metadata from cache in classpath implementation
Use the last modified timestamp and the file inode to help detect when the file has been overwritten (as is common in SBT builds with `exportJars := true`, or when using snapshot dependencies). Fixes scala/bug#10295
1 parent 6fb0343 commit a966b7d

File tree

2 files changed

+96
-13
lines changed

2 files changed

+96
-13
lines changed

src/compiler/scala/tools/nsc/classpath/ZipAndJarFileLookupFactory.scala

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,33 @@ package scala.tools.nsc.classpath
55

66
import java.io.File
77
import java.net.URL
8+
import java.nio.file.Files
9+
import java.nio.file.attribute.{BasicFileAttributes, FileTime}
810

911
import scala.annotation.tailrec
10-
import scala.reflect.io.{ AbstractFile, FileZipArchive, ManifestResources }
12+
import scala.reflect.io.{AbstractFile, FileZipArchive, ManifestResources}
1113
import scala.tools.nsc.util.{ClassPath, ClassRepresentation}
1214
import scala.tools.nsc.Settings
1315
import FileUtils._
1416

1517
/**
1618
* A trait providing an optional cache for classpath entries obtained from zip and jar files.
17-
* It's possible to create such a cache assuming that entries in such files won't change (at
18-
* least will be the same each time we'll load classpath during the lifetime of JVM process)
19-
* - unlike class and source files in directories, which can be modified and recompiled.
2019
* It allows us to e.g. reduce significantly memory used by PresentationCompilers in Scala IDE
2120
* when there are a lot of projects having a lot of common dependencies.
2221
*/
2322
sealed trait ZipAndJarFileLookupFactory {
24-
private val cache = collection.mutable.Map.empty[AbstractFile, ClassPath]
23+
private case class Stamp(lastModified: FileTime, fileKey: Object)
24+
private val cache = new FileBasedCache[ClassPath]
2525

2626
def create(zipFile: AbstractFile, settings: Settings): ClassPath = {
27-
if (settings.YdisableFlatCpCaching) createForZipFile(zipFile)
27+
if (settings.YdisableFlatCpCaching || zipFile.file == null) createForZipFile(zipFile)
2828
else createUsingCache(zipFile, settings)
2929
}
3030

3131
protected def createForZipFile(zipFile: AbstractFile): ClassPath
3232

33-
private def createUsingCache(zipFile: AbstractFile, settings: Settings): ClassPath = cache.synchronized {
34-
def newClassPathInstance = {
35-
if (settings.verbose || settings.Ylogcp)
36-
println(s"$zipFile is not yet in the classpath cache")
37-
createForZipFile(zipFile)
38-
}
39-
cache.getOrElseUpdate(zipFile, newClassPathInstance)
33+
private def createUsingCache(zipFile: AbstractFile, settings: Settings): ClassPath = {
34+
cache.getOrCreate(zipFile.file.toPath, () => createForZipFile(zipFile))
4035
}
4136
}
4237

@@ -181,3 +176,27 @@ object ZipAndJarSourcePathFactory extends ZipAndJarFileLookupFactory {
181176

182177
override protected def createForZipFile(zipFile: AbstractFile): ClassPath = ZipArchiveSourcePath(zipFile.file)
183178
}
179+
180+
final class FileBasedCache[T] {
181+
private case class Stamp(lastModified: FileTime, fileKey: Object)
182+
private val cache = collection.mutable.Map.empty[java.nio.file.Path, (Stamp, T)]
183+
184+
def getOrCreate(path: java.nio.file.Path, create: () => T): T = cache.synchronized {
185+
val lastModified = Files.getLastModifiedTime(path)
186+
val attrs = Files.readAttributes(path, classOf[BasicFileAttributes])
187+
val stamp = Stamp(lastModified, attrs.fileKey())
188+
cache.get(path) match {
189+
case Some((cachedStamp, cached)) if cachedStamp == stamp => cached
190+
case _ =>
191+
val value = create()
192+
cache.put(path, (stamp, value))
193+
value
194+
}
195+
}
196+
197+
def clear(): Unit = cache.synchronized {
198+
// TODO support closing
199+
// cache.valuesIterator.foreach(_.close())
200+
cache.clear()
201+
}
202+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package scala.tools.nsc
2+
package classpath
3+
4+
import org.junit.Test
5+
import java.nio.file._
6+
import java.nio.file.attribute.FileTime
7+
import scala.reflect.io.AbstractFile
8+
9+
class ZipAndJarFileLookupFactoryTest {
10+
@Test def cacheInvalidation(): Unit = {
11+
val f = Files.createTempFile("test-", ".jar")
12+
Files.delete(f)
13+
val g = new scala.tools.nsc.Global(new scala.tools.nsc.Settings())
14+
assert(!g.settings.YdisableFlatCpCaching.value) // we're testing with our JAR metadata caching enabled.
15+
def createCp = ZipAndJarClassPathFactory.create(AbstractFile.getFile(f.toFile), g.settings)
16+
try {
17+
createZip(f, Array(), "p1/C.class")
18+
createZip(f, Array(), "p2/X.class")
19+
createZip(f, Array(), "p3/Y.class")
20+
val cp1 = createCp
21+
assert(cp1.findClass("p1.C").isDefined)
22+
23+
// We expect get a cache hit as the underlying zip hasn't changed
24+
val cp2 = createCp
25+
assert(cp2 eq cp1)
26+
27+
// check things work after the cache hit
28+
cp1.findClassFile("p2.X").get.toByteArray
29+
30+
val lastMod1 = Files.getLastModifiedTime(f)
31+
// Create a new zip at the same path with different contents and last modified
32+
Files.delete(f)
33+
createZip(f, Array(), "p1/D.class")
34+
Files.setLastModifiedTime(f, FileTime.fromMillis(lastMod1.toMillis + 2000))
35+
36+
// Our classpath cache should create a new instance
37+
val cp3 = createCp
38+
assert(cp1 ne cp3, (System.identityHashCode(cp1), System.identityHashCode(cp3)))
39+
// And that instance should see D, not C, in package p1.
40+
assert(cp3.findClass("p1.C").isEmpty)
41+
assert(cp3.findClass("p1.D").isDefined)
42+
} finally Files.delete(f)
43+
}
44+
45+
def createZip(zipLocation: Path, content: Array[Byte], internalPath: String): Unit = {
46+
val env = new java.util.HashMap[String, String]()
47+
env.put("create", String.valueOf(Files.notExists(zipLocation)))
48+
val fileUri = zipLocation.toUri
49+
val zipUri = new java.net.URI("jar:" + fileUri.getScheme, fileUri.getPath, null)
50+
val zipfs = FileSystems.newFileSystem(zipUri, env)
51+
try {
52+
try {
53+
val internalTargetPath = zipfs.getPath(internalPath)
54+
Files.createDirectories(internalTargetPath.getParent)
55+
Files.write(internalTargetPath, content)
56+
} finally {
57+
if (zipfs != null) zipfs.close()
58+
}
59+
} finally {
60+
zipfs.close()
61+
}
62+
}
63+
}
64+

0 commit comments

Comments
 (0)