Skip to content

Commit bcf47b1

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 bcf47b1

File tree

2 files changed

+97
-13
lines changed

2 files changed

+97
-13
lines changed

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

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,32 @@ 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 val cache = new FileBasedCache[ClassPath]
2524

2625
def create(zipFile: AbstractFile, settings: Settings): ClassPath = {
27-
if (settings.YdisableFlatCpCaching) createForZipFile(zipFile)
26+
if (settings.YdisableFlatCpCaching || zipFile.file == null) createForZipFile(zipFile)
2827
else createUsingCache(zipFile, settings)
2928
}
3029

3130
protected def createForZipFile(zipFile: AbstractFile): ClassPath
3231

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)
32+
private def createUsingCache(zipFile: AbstractFile, settings: Settings): ClassPath = {
33+
cache.getOrCreate(zipFile.file.toPath, () => createForZipFile(zipFile))
4034
}
4135
}
4236

@@ -181,3 +175,29 @@ object ZipAndJarSourcePathFactory extends ZipAndJarFileLookupFactory {
181175

182176
override protected def createForZipFile(zipFile: AbstractFile): ClassPath = ZipArchiveSourcePath(zipFile.file)
183177
}
178+
179+
final class FileBasedCache[T] {
180+
private case class Stamp(lastModified: FileTime, fileKey: Object)
181+
private val cache = collection.mutable.Map.empty[java.nio.file.Path, (Stamp, T)]
182+
183+
def getOrCreate(path: java.nio.file.Path, create: () => T): T = cache.synchronized {
184+
val attrs = Files.readAttributes(path, classOf[BasicFileAttributes])
185+
val lastModified = attrs.lastModifiedTime()
186+
// only null on some platforms, but that's okay, we just use the last modified timestamp as our stamp
187+
val fileKey = attrs.fileKey()
188+
val stamp = Stamp(lastModified, fileKey)
189+
cache.get(path) match {
190+
case Some((cachedStamp, cached)) if cachedStamp == stamp => cached
191+
case _ =>
192+
val value = create()
193+
cache.put(path, (stamp, value))
194+
value
195+
}
196+
}
197+
198+
def clear(): Unit = cache.synchronized {
199+
// TODO support closing
200+
// cache.valuesIterator.foreach(_.close())
201+
cache.clear()
202+
}
203+
}
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)