Synchronize caches

This commit is contained in:
Mark Harrah 2009-10-10 22:01:03 -04:00
parent ac02123ace
commit ec61fa3df0
3 changed files with 65 additions and 22 deletions

View File

@ -1,6 +1,7 @@
package xsbt package xsbt
import java.io.File import java.io.{File,FileOutputStream}
import ComponentManager.lock
/** A component manager provides access to the pieces of xsbt that are distributed as components. /** A component manager provides access to the pieces of xsbt that are distributed as components.
* There are two types of components. The first type is compiled subproject jars with their dependencies. * There are two types of components. The first type is compiled subproject jars with their dependencies.
@ -14,33 +15,73 @@ import java.io.File
class ComponentManager(provider: xsbti.ComponentProvider, val log: IvyLogger) extends NotNull class ComponentManager(provider: xsbti.ComponentProvider, val log: IvyLogger) extends NotNull
{ {
/** Get all of the files for component 'id', throwing an exception if no files exist for the component. */ /** Get all of the files for component 'id', throwing an exception if no files exist for the component. */
def files(id: String): Iterable[File] = def files(id: String)(ifMissing: IfMissing): Iterable[File] =
{ {
val existing = provider.component(id) def fromGlobal =
val fs = if(existing.isEmpty) { update(id); provider.component(id) } else existing lockGlobalCache {
if(!fs.isEmpty) fs else invalid("Could not find required component '" + id + "'") try { update(id); getOrElse(createAndCache) }
catch { case e: NotInCache => createAndCache }
}
def getOrElse(orElse: => Iterable[File]) =
{
val existing = provider.component(id)
if(existing.isEmpty) orElse else existing
}
def notFound = invalid("Could not find required component '" + id + "'")
def createAndCache =
ifMissing match {
case IfMissing.Fail => notFound
case d: IfMissing.Define =>
d()
if(d.cache) cache(id)
getOrElse(notFound)
}
lockLocalCache { getOrElse(fromGlobal) }
} }
private def lockLocalCache[T](action: => T): T = lock("local cache", provider.lockFile, log) ( action )
private def lockGlobalCache[T](action: => T): T = lock("global cache", IvyCache.lockFile, log)( action )
/** Get the file for component 'id', throwing an exception if no files or multiple files exist for the component. */ /** Get the file for component 'id', throwing an exception if no files or multiple files exist for the component. */
def file(id: String): File = def file(id: String)(ifMissing: IfMissing): File =
files(id).toList match { files(id)(ifMissing).toList match {
case x :: Nil => x case x :: Nil => x
case xs => invalid("Expected single file for component '" + id + "', found: " + xs.mkString(", ")) case xs => invalid("Expected single file for component '" + id + "', found: " + xs.mkString(", "))
} }
private def invalid(msg: String) = throw new InvalidComponent(msg) private def invalid(msg: String) = throw new InvalidComponent(msg)
private def invalid(e: NotInCache) = throw new InvalidComponent(e.getMessage, e) private def invalid(e: NotInCache) = throw new InvalidComponent(e.getMessage, e)
def define(id: String, files: Iterable[File]) = provider.defineComponent(id, files.toSeq.toArray) def define(id: String, files: Iterable[File]) = lockLocalCache { provider.defineComponent(id, files.toSeq.toArray) }
/** Retrieve the file for component 'id' from the local repository. */ /** Retrieve the file for component 'id' from the local repository. */
def update(id: String): Unit = private def update(id: String): Unit = IvyCache.withCachedJar(sbtModuleID(id), log)(jar => define(id, Seq(jar)) )
try { IvyCache.withCachedJar(sbtModuleID(id), log)(jar => define(id, Seq(jar)) ) }
catch { case e: NotInCache => invalid(e) }
def sbtModuleID(id: String) = ModuleID("org.scala-tools.sbt", id, xsbti.Versions.Sbt) private def sbtModuleID(id: String) = ModuleID("org.scala-tools.sbt", id, xsbti.Versions.Sbt)
/** Install the files for component 'id' to the local repository. This is usually used after writing files to the directory returned by 'location'. */ /** Install the files for component 'id' to the local repository. This is usually used after writing files to the directory returned by 'location'. */
def cache(id: String): Unit = IvyCache.cacheJar(sbtModuleID(id), file(id), log) def cache(id: String): Unit = IvyCache.cacheJar(sbtModuleID(id), file(id)(IfMissing.Fail), log)
def clearCache(id: String): Unit = IvyCache.clearCachedJar(sbtModuleID(id), log) def clearCache(id: String): Unit = lockGlobalCache { IvyCache.clearCachedJar(sbtModuleID(id), log) }
} }
class InvalidComponent(msg: String, cause: Throwable) extends RuntimeException(msg, cause) class InvalidComponent(msg: String, cause: Throwable) extends RuntimeException(msg, cause)
{ {
def this(msg: String) = this(msg, null) def this(msg: String) = this(msg, null)
}
sealed trait IfMissing extends NotNull
object IfMissing
{
object Fail extends IfMissing
final class Define(val cache: Boolean, define: => Unit) extends IfMissing { def apply() = define }
}
object ComponentManager
{
def lock[T](label: String, file: File, log: IvyLogger)(action: => T): T =
{
synchronized {
val channel = new FileOutputStream(file).getChannel
try {
val freeLock = channel.tryLock
val lock = if(freeLock eq null) { log.info("Waiting for " + label + " to be available..."); channel.lock } else freeLock
try { action }
finally { lock.release() }
}
finally { channel.close() }
}
}
} }

View File

@ -27,6 +27,7 @@ private object NotInCache
/** Provides methods for working at the level of a single jar file with the default Ivy cache.*/ /** Provides methods for working at the level of a single jar file with the default Ivy cache.*/
object IvyCache object IvyCache
{ {
def lockFile = new File(System.getProperty("temp.dir"), "sbt.cache.lock")
/** Caches the given 'file' with the given ID. It may be retrieved or cleared using this ID.*/ /** Caches the given 'file' with the given ID. It may be retrieved or cleared using this ID.*/
def cacheJar(moduleID: ModuleID, file: File, log: IvyLogger) def cacheJar(moduleID: ModuleID, file: File, log: IvyLogger)
{ {

View File

@ -4,47 +4,48 @@ import java.io.File
import org.specs._ import org.specs._
import FileUtilities.{createDirectory, delete, touch, withTemporaryDirectory} import FileUtilities.{createDirectory, delete, touch, withTemporaryDirectory}
import org.apache.ivy.util.ChecksumHelper import org.apache.ivy.util.ChecksumHelper
import IfMissing.Fail
object ComponentManagerTest extends Specification object ComponentManagerTest extends Specification
{ {
val TestID = "manager-test" val TestID = "manager-test"
"Component manager" should { "Component manager" should {
"throw an exception if 'file' is called for a non-existing component" in { "throw an exception if 'file' is called for a non-existing component" in {
withManager { _.file(TestID) must throwA[InvalidComponent] } withManager { _.file(TestID)(Fail) must throwA[InvalidComponent] }
} }
"throw an exception if 'file' is called for an empty component" in { "throw an exception if 'file' is called for an empty component" in {
withManager { manager => withManager { manager =>
manager.define(TestID, Nil) manager.define(TestID, Nil)
( manager.file(TestID) ) must throwA[InvalidComponent] ( manager.file(TestID)(Fail) ) must throwA[InvalidComponent]
} }
} }
"return the file for a single-file component" in { "return the file for a single-file component" in {
withManager { manager => withManager { manager =>
val hash = defineFile(manager, TestID, "a") val hash = defineFile(manager, TestID, "a")
checksum(manager.file(TestID)) must beEqualTo(hash) checksum(manager.file(TestID)(Fail)) must beEqualTo(hash)
} }
} }
"throw an exception if 'file' is called for multi-file component" in { "throw an exception if 'file' is called for multi-file component" in {
withManager { manager => withManager { manager =>
defineFiles(manager, TestID, "a", "b") defineFiles(manager, TestID, "a", "b")
( manager.file(TestID) ) must throwA[InvalidComponent] ( manager.file(TestID)(Fail) ) must throwA[InvalidComponent]
} }
} }
"return the files for a multi-file component" in { "return the files for a multi-file component" in {
withManager { manager => withManager { manager =>
val hashes = defineFiles(manager, TestID, "a", "b") val hashes = defineFiles(manager, TestID, "a", "b")
checksum(manager.files(TestID)) must haveTheSameElementsAs(hashes) checksum(manager.files(TestID)(Fail)) must haveTheSameElementsAs(hashes)
} }
} }
"return the files for a single-file component" in { "return the files for a single-file component" in {
withManager { manager => withManager { manager =>
val hashes = defineFiles(manager, TestID, "a") val hashes = defineFiles(manager, TestID, "a")
checksum(manager.files(TestID)) must haveTheSameElementsAs(hashes) checksum(manager.files(TestID)(Fail)) must haveTheSameElementsAs(hashes)
} }
} }
"throw an exception if 'files' is called for a non-existing component" in { "throw an exception if 'files' is called for a non-existing component" in {
withManager { _.files(TestID) must throwA[InvalidComponent] } withManager { _.files(TestID)(Fail) must throwA[InvalidComponent] }
} }
"properly cache a file and then retrieve it to an unresolved component" in { "properly cache a file and then retrieve it to an unresolved component" in {
@ -54,7 +55,7 @@ object ComponentManagerTest extends Specification
{ {
definingManager.cache(TestID) definingManager.cache(TestID)
withManager { usingManager => withManager { usingManager =>
checksum(usingManager.file(TestID)) must beEqualTo(hash) checksum(usingManager.file(TestID)(Fail)) must beEqualTo(hash)
} }
} }
finally { definingManager.clearCache(TestID) } finally { definingManager.clearCache(TestID) }