mirror of
https://github.com/sbt/sbt.git
synced 2026-08-22 06:07:24 +02:00
[2.x] feat: Execution log (#9203)
**Problem** We need some tooling to debug caching issues. **Solution** This adds an exeprimental execution log support, which shows input and output of cached tasks.
This commit is contained in:
@@ -11,7 +11,7 @@ package sbt
|
|||||||
import java.io.{ File, IOException }
|
import java.io.{ File, IOException }
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.nio.channels.ClosedChannelException
|
import java.nio.channels.ClosedChannelException
|
||||||
import java.nio.file.{ FileAlreadyExistsException, FileSystems, Files }
|
import java.nio.file.{ FileAlreadyExistsException, FileSystems, Files, Paths }
|
||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
import java.util.concurrent.ForkJoinPool
|
import java.util.concurrent.ForkJoinPool
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
@@ -32,7 +32,7 @@ import sbt.internal.util.complete.Parser
|
|||||||
import sbt.internal.util.{ RunningProcesses, Terminal as ITerminal, * }
|
import sbt.internal.util.{ RunningProcesses, Terminal as ITerminal, * }
|
||||||
import sbt.io.*
|
import sbt.io.*
|
||||||
import sbt.io.syntax.*
|
import sbt.io.syntax.*
|
||||||
import sbt.util.{ Level, Logger, Show }
|
import sbt.util.{ ActionCache, Level, Logger, Show }
|
||||||
import xsbti.AppProvider
|
import xsbti.AppProvider
|
||||||
|
|
||||||
import scala.annotation.{ nowarn, tailrec }
|
import scala.annotation.{ nowarn, tailrec }
|
||||||
@@ -261,12 +261,21 @@ object StandardMain {
|
|||||||
args.flatMap(levelFromArg).headOption.getOrElse(Level.Info)
|
args.flatMap(levelFromArg).headOption.getOrElse(Level.Info)
|
||||||
|
|
||||||
private def initialGlobalLogging(file: Option[File], initialLevel: Level.Value): GlobalLogging =
|
private def initialGlobalLogging(file: Option[File], initialLevel: Level.Value): GlobalLogging =
|
||||||
def createTemp(attempt: Int = 0): File = Retry:
|
def createTemp(prefix: String)(attempt: Int = 0): File = Retry:
|
||||||
file.foreach(f => if (!f.exists()) IO.createDirectory(f))
|
file.foreach(f => if (!f.exists()) IO.createDirectory(f))
|
||||||
File.createTempFile("sbt-global-log", ".log", file.orNull)
|
File.createTempFile(prefix, ".log", file.orNull)
|
||||||
|
|
||||||
|
// Call this experimental since we don't want to commit to the log format for now
|
||||||
|
val execLogProp = "sbt.experimental_execution_log"
|
||||||
|
val execLog =
|
||||||
|
if java.lang.Boolean.getBoolean(execLogProp) then
|
||||||
|
Option(ActionCache.setExecLog(createTemp("exec-log")().toPath()))
|
||||||
|
else sys.props.get(execLogProp).map(Paths.get(_)).map(ActionCache.setExecLog)
|
||||||
|
execLog.foreach: log =>
|
||||||
|
ShutdownHooks.add(() => log.close())
|
||||||
GlobalLogging.initial(
|
GlobalLogging.initial(
|
||||||
MainAppender.globalDefault(ConsoleOut.globalProxy),
|
MainAppender.globalDefault(ConsoleOut.globalProxy),
|
||||||
createTemp(),
|
createTemp("sbt-global-log")(),
|
||||||
ConsoleOut.globalProxy,
|
ConsoleOut.globalProxy,
|
||||||
initialLevel
|
initialLevel
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ object DatatypeConfig {
|
|||||||
case "Map" | "Tuple2" | "scala.Tuple2" => { tpe =>
|
case "Map" | "Tuple2" | "scala.Tuple2" => { tpe =>
|
||||||
twoArgs(tpe).flatMap(getFormats)
|
twoArgs(tpe).flatMap(getFormats)
|
||||||
}
|
}
|
||||||
case "Int" | "Long" => { _ =>
|
case "Int" | "Long" | "sbt.util.Digest" => { _ =>
|
||||||
Nil
|
Nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* This code is generated using [[https://www.scala-sbt.org/contraband]].
|
||||||
|
*/
|
||||||
|
|
||||||
|
// DO NOT EDIT MANUALLY
|
||||||
|
package sbt.internal.util
|
||||||
|
final class SpawnExec private (
|
||||||
|
val input: sbt.internal.util.SpawnInput,
|
||||||
|
val cacheHit: Boolean,
|
||||||
|
val exitCode: Option[Int],
|
||||||
|
val outputs: Vector[xsbti.HashedVirtualFileRef]) extends Serializable {
|
||||||
|
|
||||||
|
private def this(input: sbt.internal.util.SpawnInput, cacheHit: Boolean, exitCode: Option[Int]) = this(input, cacheHit, exitCode, Vector())
|
||||||
|
|
||||||
|
override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match {
|
||||||
|
case x: SpawnExec => (this.input == x.input) && (this.cacheHit == x.cacheHit) && (this.exitCode == x.exitCode) && (this.outputs == x.outputs)
|
||||||
|
case _ => false
|
||||||
|
})
|
||||||
|
override def hashCode: Int = {
|
||||||
|
37 * (37 * (37 * (37 * (37 * (17 + "sbt.internal.util.SpawnExec".##) + input.##) + cacheHit.##) + exitCode.##) + outputs.##)
|
||||||
|
}
|
||||||
|
override def toString: String = {
|
||||||
|
"SpawnExec(" + input + ", " + cacheHit + ", " + exitCode + ", " + outputs + ")"
|
||||||
|
}
|
||||||
|
private def copy(input: sbt.internal.util.SpawnInput = input, cacheHit: Boolean = cacheHit, exitCode: Option[Int] = exitCode, outputs: Vector[xsbti.HashedVirtualFileRef] = outputs): SpawnExec = {
|
||||||
|
new SpawnExec(input, cacheHit, exitCode, outputs)
|
||||||
|
}
|
||||||
|
def withInput(input: sbt.internal.util.SpawnInput): SpawnExec = {
|
||||||
|
copy(input = input)
|
||||||
|
}
|
||||||
|
def withCacheHit(cacheHit: Boolean): SpawnExec = {
|
||||||
|
copy(cacheHit = cacheHit)
|
||||||
|
}
|
||||||
|
def withExitCode(exitCode: Option[Int]): SpawnExec = {
|
||||||
|
copy(exitCode = exitCode)
|
||||||
|
}
|
||||||
|
def withExitCode(exitCode: Int): SpawnExec = {
|
||||||
|
copy(exitCode = Option(exitCode))
|
||||||
|
}
|
||||||
|
def withOutputs(outputs: Vector[xsbti.HashedVirtualFileRef]): SpawnExec = {
|
||||||
|
copy(outputs = outputs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
object SpawnExec {
|
||||||
|
|
||||||
|
def apply(input: sbt.internal.util.SpawnInput, cacheHit: Boolean, exitCode: Option[Int]): SpawnExec = new SpawnExec(input, cacheHit, exitCode)
|
||||||
|
def apply(input: sbt.internal.util.SpawnInput, cacheHit: Boolean, exitCode: Int): SpawnExec = new SpawnExec(input, cacheHit, Option(exitCode))
|
||||||
|
def apply(input: sbt.internal.util.SpawnInput, cacheHit: Boolean, exitCode: Option[Int], outputs: Vector[xsbti.HashedVirtualFileRef]): SpawnExec = new SpawnExec(input, cacheHit, exitCode, outputs)
|
||||||
|
def apply(input: sbt.internal.util.SpawnInput, cacheHit: Boolean, exitCode: Int, outputs: Vector[xsbti.HashedVirtualFileRef]): SpawnExec = new SpawnExec(input, cacheHit, Option(exitCode), outputs)
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* This code is generated using [[https://www.scala-sbt.org/contraband]].
|
||||||
|
*/
|
||||||
|
|
||||||
|
// DO NOT EDIT MANUALLY
|
||||||
|
package sbt.internal.util
|
||||||
|
final class SpawnInput private (
|
||||||
|
val digest: sbt.util.Digest,
|
||||||
|
val codeContentHash: sbt.util.Digest,
|
||||||
|
val extraHash: sbt.util.Digest,
|
||||||
|
val cacheVersion: Option[Long],
|
||||||
|
val str: Option[String]) extends Serializable {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match {
|
||||||
|
case x: SpawnInput => (this.digest == x.digest) && (this.codeContentHash == x.codeContentHash) && (this.extraHash == x.extraHash) && (this.cacheVersion == x.cacheVersion) && (this.str == x.str)
|
||||||
|
case _ => false
|
||||||
|
})
|
||||||
|
override def hashCode: Int = {
|
||||||
|
37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.internal.util.SpawnInput".##) + digest.##) + codeContentHash.##) + extraHash.##) + cacheVersion.##) + str.##)
|
||||||
|
}
|
||||||
|
override def toString: String = {
|
||||||
|
"SpawnInput(" + digest + ", " + codeContentHash + ", " + extraHash + ", " + cacheVersion + ", " + str + ")"
|
||||||
|
}
|
||||||
|
private def copy(digest: sbt.util.Digest = digest, codeContentHash: sbt.util.Digest = codeContentHash, extraHash: sbt.util.Digest = extraHash, cacheVersion: Option[Long] = cacheVersion, str: Option[String] = str): SpawnInput = {
|
||||||
|
new SpawnInput(digest, codeContentHash, extraHash, cacheVersion, str)
|
||||||
|
}
|
||||||
|
def withDigest(digest: sbt.util.Digest): SpawnInput = {
|
||||||
|
copy(digest = digest)
|
||||||
|
}
|
||||||
|
def withCodeContentHash(codeContentHash: sbt.util.Digest): SpawnInput = {
|
||||||
|
copy(codeContentHash = codeContentHash)
|
||||||
|
}
|
||||||
|
def withExtraHash(extraHash: sbt.util.Digest): SpawnInput = {
|
||||||
|
copy(extraHash = extraHash)
|
||||||
|
}
|
||||||
|
def withCacheVersion(cacheVersion: Option[Long]): SpawnInput = {
|
||||||
|
copy(cacheVersion = cacheVersion)
|
||||||
|
}
|
||||||
|
def withCacheVersion(cacheVersion: Long): SpawnInput = {
|
||||||
|
copy(cacheVersion = Option(cacheVersion))
|
||||||
|
}
|
||||||
|
def withStr(str: Option[String]): SpawnInput = {
|
||||||
|
copy(str = str)
|
||||||
|
}
|
||||||
|
def withStr(str: String): SpawnInput = {
|
||||||
|
copy(str = Option(str))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
object SpawnInput {
|
||||||
|
|
||||||
|
def apply(digest: sbt.util.Digest, codeContentHash: sbt.util.Digest, extraHash: sbt.util.Digest, cacheVersion: Option[Long], str: Option[String]): SpawnInput = new SpawnInput(digest, codeContentHash, extraHash, cacheVersion, str)
|
||||||
|
def apply(digest: sbt.util.Digest, codeContentHash: sbt.util.Digest, extraHash: sbt.util.Digest, cacheVersion: Long, str: String): SpawnInput = new SpawnInput(digest, codeContentHash, extraHash, Option(cacheVersion), Option(str))
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* This code is generated using [[https://www.scala-sbt.org/contraband]].
|
||||||
|
*/
|
||||||
|
|
||||||
|
// DO NOT EDIT MANUALLY
|
||||||
|
package sbt.internal.util.codec
|
||||||
|
trait SpawnCodec extends sjsonnew.BasicJsonProtocol
|
||||||
|
with sbt.internal.util.codec.SpawnInputFormats
|
||||||
|
with sbt.internal.util.codec.HashedVirtualFileRefFormats
|
||||||
|
with sbt.internal.util.codec.SpawnExecFormats
|
||||||
|
object SpawnCodec extends SpawnCodec
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* This code is generated using [[https://www.scala-sbt.org/contraband]].
|
||||||
|
*/
|
||||||
|
|
||||||
|
// DO NOT EDIT MANUALLY
|
||||||
|
package sbt.internal.util.codec
|
||||||
|
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
|
||||||
|
trait SpawnExecFormats { self: sbt.internal.util.codec.SpawnInputFormats & sjsonnew.BasicJsonProtocol & sbt.internal.util.codec.HashedVirtualFileRefFormats =>
|
||||||
|
given SpawnExecFormat: JsonFormat[sbt.internal.util.SpawnExec] = new JsonFormat[sbt.internal.util.SpawnExec] {
|
||||||
|
override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.util.SpawnExec = {
|
||||||
|
__jsOpt match {
|
||||||
|
case Some(__js) =>
|
||||||
|
unbuilder.beginObject(__js)
|
||||||
|
val input = unbuilder.readField[sbt.internal.util.SpawnInput]("input")
|
||||||
|
val cacheHit = unbuilder.readField[Boolean]("cacheHit")
|
||||||
|
val exitCode = unbuilder.readField[Option[Int]]("exitCode")
|
||||||
|
val outputs = unbuilder.readField[Vector[xsbti.HashedVirtualFileRef]]("outputs")
|
||||||
|
unbuilder.endObject()
|
||||||
|
sbt.internal.util.SpawnExec(input, cacheHit, exitCode, outputs)
|
||||||
|
case None =>
|
||||||
|
deserializationError("Expected JsObject but found None")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override def write[J](obj: sbt.internal.util.SpawnExec, builder: Builder[J]): Unit = {
|
||||||
|
builder.beginObject()
|
||||||
|
builder.addField("input", obj.input)
|
||||||
|
builder.addField("cacheHit", obj.cacheHit)
|
||||||
|
builder.addField("exitCode", obj.exitCode)
|
||||||
|
builder.addField("outputs", obj.outputs)
|
||||||
|
builder.endObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* This code is generated using [[https://www.scala-sbt.org/contraband]].
|
||||||
|
*/
|
||||||
|
|
||||||
|
// DO NOT EDIT MANUALLY
|
||||||
|
package sbt.internal.util.codec
|
||||||
|
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
|
||||||
|
trait SpawnInputFormats { self: sjsonnew.BasicJsonProtocol =>
|
||||||
|
given SpawnInputFormat: JsonFormat[sbt.internal.util.SpawnInput] = new JsonFormat[sbt.internal.util.SpawnInput] {
|
||||||
|
override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.util.SpawnInput = {
|
||||||
|
__jsOpt match {
|
||||||
|
case Some(__js) =>
|
||||||
|
unbuilder.beginObject(__js)
|
||||||
|
val digest = unbuilder.readField[sbt.util.Digest]("digest")
|
||||||
|
val codeContentHash = unbuilder.readField[sbt.util.Digest]("codeContentHash")
|
||||||
|
val extraHash = unbuilder.readField[sbt.util.Digest]("extraHash")
|
||||||
|
val cacheVersion = unbuilder.readField[Option[Long]]("cacheVersion")
|
||||||
|
val str = unbuilder.readField[Option[String]]("str")
|
||||||
|
unbuilder.endObject()
|
||||||
|
sbt.internal.util.SpawnInput(digest, codeContentHash, extraHash, cacheVersion, str)
|
||||||
|
case None =>
|
||||||
|
deserializationError("Expected JsObject but found None")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override def write[J](obj: sbt.internal.util.SpawnInput, builder: Builder[J]): Unit = {
|
||||||
|
builder.beginObject()
|
||||||
|
builder.addField("digest", obj.digest)
|
||||||
|
builder.addField("codeContentHash", obj.codeContentHash)
|
||||||
|
builder.addField("extraHash", obj.extraHash)
|
||||||
|
builder.addField("cacheVersion", obj.cacheVersion)
|
||||||
|
builder.addField("str", obj.str)
|
||||||
|
builder.endObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package sbt.internal.util
|
||||||
|
@target(Scala)
|
||||||
|
@codecPackage("sbt.internal.util.codec")
|
||||||
|
@fullCodec("SpawnCodec")
|
||||||
|
|
||||||
|
# https://github.com/bazelbuild/bazel/blob/master/src/main/protobuf/spawn.proto
|
||||||
|
type SpawnInput {
|
||||||
|
digest: sbt.util.Digest!
|
||||||
|
codeContentHash: sbt.util.Digest!
|
||||||
|
extraHash: sbt.util.Digest!
|
||||||
|
cacheVersion: Long
|
||||||
|
str: String
|
||||||
|
}
|
||||||
|
|
||||||
|
# https://github.com/bazelbuild/bazel/blob/master/src/main/protobuf/spawn.proto
|
||||||
|
type SpawnExec {
|
||||||
|
input: sbt.internal.util.SpawnInput!
|
||||||
|
cacheHit: Boolean!
|
||||||
|
exitCode: Int
|
||||||
|
outputs: [xsbti.HashedVirtualFileRef] @since("0.2.0")
|
||||||
|
}
|
||||||
@@ -8,10 +8,16 @@
|
|||||||
|
|
||||||
package sbt.util
|
package sbt.util
|
||||||
|
|
||||||
import java.io.{ File, IOException }
|
import java.io.{ File, IOException, PrintWriter }
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.nio.file.{ AtomicMoveNotSupportedException, Files, Path, Paths, StandardCopyOption }
|
import java.nio.file.{ AtomicMoveNotSupportedException, Files, Path, Paths, StandardCopyOption }
|
||||||
import sbt.internal.util.{ ActionCacheEvent, CacheEventLog, StringVirtualFile1 }
|
import sbt.internal.util.{
|
||||||
|
ActionCacheEvent,
|
||||||
|
CacheEventLog,
|
||||||
|
SpawnExec,
|
||||||
|
SpawnInput,
|
||||||
|
StringVirtualFile1
|
||||||
|
}
|
||||||
import sbt.io.syntax.*
|
import sbt.io.syntax.*
|
||||||
import sbt.io.IO
|
import sbt.io.IO
|
||||||
import sbt.nio.file.{ **, FileTreeView }
|
import sbt.nio.file.{ **, FileTreeView }
|
||||||
@@ -19,10 +25,10 @@ import sbt.nio.file.syntax.*
|
|||||||
import sbt.util.CacheImplicits
|
import sbt.util.CacheImplicits
|
||||||
import scala.reflect.ClassTag
|
import scala.reflect.ClassTag
|
||||||
import scala.annotation.{ meta, StaticAnnotation }
|
import scala.annotation.{ meta, StaticAnnotation }
|
||||||
import scala.util.control.{ Exception, NonFatal }
|
import scala.util.control.NonFatal
|
||||||
import sjsonnew.{ HashWriter, JsonFormat }
|
import sjsonnew.{ HashWriter, JsonFormat }
|
||||||
import sjsonnew.support.murmurhash.Hasher
|
import sjsonnew.support.murmurhash.Hasher
|
||||||
import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter, Parser }
|
import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter, Parser, PrettyPrinter }
|
||||||
import scala.quoted.{ Expr, FromExpr, ToExpr, Quotes }
|
import scala.quoted.{ Expr, FromExpr, ToExpr, Quotes }
|
||||||
import xsbti.{ CompileFailed, FileConverter, HashedVirtualFileRef, VirtualFile, VirtualFileRef }
|
import xsbti.{ CompileFailed, FileConverter, HashedVirtualFileRef, VirtualFile, VirtualFileRef }
|
||||||
|
|
||||||
@@ -31,6 +37,11 @@ object ActionCache:
|
|||||||
private[sbt] val manifestFileName = "sbtdir_manifest.json"
|
private[sbt] val manifestFileName = "sbtdir_manifest.json"
|
||||||
private[sbt] val failureFileName = "failure.json"
|
private[sbt] val failureFileName = "failure.json"
|
||||||
private[sbt] val failureExitCode = 1
|
private[sbt] val failureExitCode = 1
|
||||||
|
private[sbt] var execLog: Option[PrintWriter] = None
|
||||||
|
private[sbt] def setExecLog(log: Path): PrintWriter =
|
||||||
|
val writer = PrintWriter(log.toFile, "UTF-8")
|
||||||
|
execLog = Option(writer)
|
||||||
|
writer
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a key function that drives remote caching.
|
* This is a key function that drives remote caching.
|
||||||
@@ -58,17 +69,19 @@ object ActionCache:
|
|||||||
): O =
|
): O =
|
||||||
import config.*
|
import config.*
|
||||||
|
|
||||||
|
val inputDigest = mkInput(key, codeContentHash, extraHash, cacheVersion)
|
||||||
|
|
||||||
def cacheFailure(e: CompileFailed): Nothing =
|
def cacheFailure(e: CompileFailed): Nothing =
|
||||||
// Cache the failure so subsequent builds don't re-run failed compilation
|
// Cache the failure so subsequent builds don't re-run failed compilation
|
||||||
// This fixes https://github.com/sbt/sbt/issues/7662
|
// This fixes https://github.com/sbt/sbt/issues/7662
|
||||||
// Use the same input digest as success, distinguished by exitCode
|
// Use the same input digest as success, distinguished by exitCode
|
||||||
cacheEventLog.append(ActionCacheEvent.OnsiteTask)
|
cacheEventLog.append(ActionCacheEvent.OnsiteTask)
|
||||||
val (input, valuePath) = mkInput(key, codeContentHash, extraHash, config.cacheVersion)
|
|
||||||
val cachedFailure = CachedCompileFailure.fromException(e)
|
val cachedFailure = CachedCompileFailure.fromException(e)
|
||||||
val json = Converter.toJsonUnsafe(cachedFailure)
|
val json = Converter.toJsonUnsafe(cachedFailure)
|
||||||
|
val valuePath = mkValuePath(inputDigest)
|
||||||
val failureFile = StringVirtualFile1(valuePath, CompactPrinter(json))
|
val failureFile = StringVirtualFile1(valuePath, CompactPrinter(json))
|
||||||
store.put(
|
store.put(
|
||||||
UpdateActionResultRequest(input, Vector(failureFile), exitCode = failureExitCode)
|
UpdateActionResultRequest(inputDigest, Vector(failureFile), exitCode = failureExitCode)
|
||||||
)
|
)
|
||||||
throw e
|
throw e
|
||||||
|
|
||||||
@@ -81,6 +94,9 @@ object ActionCache:
|
|||||||
cacheFailure(e)
|
cacheFailure(e)
|
||||||
case e: Exception =>
|
case e: Exception =>
|
||||||
cacheEventLog.append(ActionCacheEvent.Error)
|
cacheEventLog.append(ActionCacheEvent.Error)
|
||||||
|
logExec(
|
||||||
|
SpawnExec(input = spawnInput, cacheHit = false, exitCode = 1)
|
||||||
|
)
|
||||||
throw e
|
throw e
|
||||||
try
|
try
|
||||||
val json = Converter.toJsonUnsafe(result)
|
val json = Converter.toJsonUnsafe(result)
|
||||||
@@ -103,12 +119,20 @@ object ActionCache:
|
|||||||
result
|
result
|
||||||
else
|
else
|
||||||
cacheEventLog.append(ActionCacheEvent.OnsiteTask)
|
cacheEventLog.append(ActionCacheEvent.OnsiteTask)
|
||||||
val (input, valuePath) = mkInput(key, codeContentHash, extraHash, config.cacheVersion)
|
val valuePath = mkValuePath(inputDigest)
|
||||||
val valueFile = StringVirtualFile1(valuePath, CompactPrinter(json))
|
val valueFile = StringVirtualFile1(valuePath, CompactPrinter(json))
|
||||||
val newOutputs = Vector(valueFile) ++ outputs.toVector
|
val newOutputs = Vector(valueFile) ++ outputs.toVector
|
||||||
store.put(UpdateActionResultRequest(input, newOutputs, exitCode = 0)) match
|
store.put(UpdateActionResultRequest(inputDigest, newOutputs, exitCode = 0)) match
|
||||||
case Right(cachedResult) =>
|
case Right(cachedResult) =>
|
||||||
store.syncBlobs(cachedResult.outputFiles, outputDirectory)
|
store.syncBlobs(cachedResult.outputFiles, outputDirectory)
|
||||||
|
logExec(
|
||||||
|
SpawnExec(
|
||||||
|
input = spawnInput,
|
||||||
|
cacheHit = false,
|
||||||
|
exitCode = 0,
|
||||||
|
outputs = cachedResult.outputFiles,
|
||||||
|
)
|
||||||
|
)
|
||||||
result
|
result
|
||||||
case Left(e) => throw e
|
case Left(e) => throw e
|
||||||
catch
|
catch
|
||||||
@@ -117,13 +141,35 @@ object ActionCache:
|
|||||||
cacheEventLog.append(ActionCacheEvent.Error)
|
cacheEventLog.append(ActionCacheEvent.Error)
|
||||||
result
|
result
|
||||||
|
|
||||||
|
def spawnInput = SpawnInput(
|
||||||
|
digest = inputDigest,
|
||||||
|
codeContentHash = codeContentHash,
|
||||||
|
extraHash = extraHash,
|
||||||
|
cacheVersion = if cacheVersion != 0 then Some(cacheVersion) else None,
|
||||||
|
str = Some(key.toString()),
|
||||||
|
)
|
||||||
|
inline def logExec(inline event: SpawnExec): Unit =
|
||||||
|
execLog.foreach: log =>
|
||||||
|
logEvent(event, log)
|
||||||
// Single cache lookup - use exitCode to distinguish success from failure
|
// Single cache lookup - use exitCode to distinguish success from failure
|
||||||
getWithFailure(key, codeContentHash, extraHash, tags, config) match
|
getWithFailure(inputDigest, tags, config) match
|
||||||
case Right(value) => value
|
case Right((value, result)) =>
|
||||||
|
logExec(
|
||||||
|
SpawnExec(
|
||||||
|
input = spawnInput,
|
||||||
|
cacheHit = true,
|
||||||
|
exitCode = result.exitCode,
|
||||||
|
outputs = result.outputFiles,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
value
|
||||||
case Left(Some(failure)) =>
|
case Left(Some(failure)) =>
|
||||||
config.cacheEventLog.append(ActionCacheEvent.Found("cached-failure"))
|
config.cacheEventLog.append(ActionCacheEvent.Found("cached-failure"))
|
||||||
// Replay problems to the logger so users see the cached errors/warnings
|
// Replay problems to the logger so users see the cached errors/warnings
|
||||||
failure.replay(config.logger)
|
failure.replay(config.logger)
|
||||||
|
logExec(
|
||||||
|
SpawnExec(input = spawnInput, cacheHit = true, exitCode = 1)
|
||||||
|
)
|
||||||
throw failure.toException
|
throw failure.toException
|
||||||
case Left(None) => organicTask
|
case Left(None) => organicTask
|
||||||
end cache
|
end cache
|
||||||
@@ -133,13 +179,11 @@ object ActionCache:
|
|||||||
* Returns Right(value) for cached success, Left(Some(failure)) for cached failure,
|
* Returns Right(value) for cached success, Left(Some(failure)) for cached failure,
|
||||||
* or Left(None) for cache miss.
|
* or Left(None) for cache miss.
|
||||||
*/
|
*/
|
||||||
private def getWithFailure[I: HashWriter, O: JsonFormat](
|
private def getWithFailure[O: JsonFormat](
|
||||||
key: I,
|
inputDigest: Digest,
|
||||||
codeContentHash: Digest,
|
|
||||||
extraHash: Digest,
|
|
||||||
tags: List[CacheLevelTag],
|
tags: List[CacheLevelTag],
|
||||||
config: BuildWideCacheConfiguration,
|
config: BuildWideCacheConfiguration,
|
||||||
): Either[Option[CachedCompileFailure], O] =
|
): Either[Option[CachedCompileFailure], (O, ActionResult)] =
|
||||||
import config.store
|
import config.store
|
||||||
def valueFromStr(str: String, origin: Option[String]): O =
|
def valueFromStr(str: String, origin: Option[String]): O =
|
||||||
config.cacheEventLog.append(ActionCacheEvent.Found(origin.getOrElse("unknown")))
|
config.cacheEventLog.append(ActionCacheEvent.Found(origin.getOrElse("unknown")))
|
||||||
@@ -152,59 +196,36 @@ object ActionCache:
|
|||||||
|
|
||||||
def parseCachedValue(
|
def parseCachedValue(
|
||||||
str: String,
|
str: String,
|
||||||
origin: Option[String],
|
result: ActionResult,
|
||||||
isFailure: Boolean,
|
isFailure: Boolean,
|
||||||
): Option[Either[Option[CachedCompileFailure], O]] =
|
): Option[Either[Option[CachedCompileFailure], (O, ActionResult)]] =
|
||||||
try
|
try
|
||||||
if isFailure then Some(Left(Some(failureFromStr(str))))
|
if isFailure then Some(Left(Some(failureFromStr(str))))
|
||||||
else Some(Right(valueFromStr(str, origin)))
|
else Some(Right((valueFromStr(str, result.origin), result)))
|
||||||
catch case _: Exception => None
|
catch case _: Exception => None
|
||||||
|
|
||||||
// Optimization: Check if we can read directly from symlinked value file
|
findActionResult(inputDigest, config) match
|
||||||
val (input, valuePath) = mkInput(key, codeContentHash, extraHash, config.cacheVersion)
|
case Right(result) =>
|
||||||
val resolvedValuePath = config.fileConverter.toPath(VirtualFileRef.of(valuePath))
|
try
|
||||||
|
val isFailure = result.exitCode.contains(failureExitCode)
|
||||||
def readFromSymlink(): Option[Either[Option[CachedCompileFailure], O]] =
|
result.contents.headOption match
|
||||||
if java.nio.file.Files.isSymbolicLink(resolvedValuePath) && java.nio.file.Files
|
case Some(head) =>
|
||||||
.exists(resolvedValuePath)
|
store.syncBlobs(result.outputFiles, config.outputDirectory)
|
||||||
then
|
val str = String(head.array(), StandardCharsets.UTF_8)
|
||||||
Exception.nonFatalCatch
|
parseCachedValue(str, result, isFailure).getOrElse(Left(None))
|
||||||
.opt(IO.read(resolvedValuePath.toFile(), StandardCharsets.UTF_8))
|
case _ =>
|
||||||
.flatMap: str =>
|
val paths = store.syncBlobs(result.outputFiles, config.outputDirectory)
|
||||||
findActionResult(key, codeContentHash, extraHash, config) match
|
if paths.isEmpty then Left(None)
|
||||||
case Right(result) =>
|
else
|
||||||
try
|
val str = IO.read(paths.head.toFile())
|
||||||
store.syncBlobs(result.outputFiles, config.outputDirectory)
|
parseCachedValue(str, result, isFailure).getOrElse(Left(None))
|
||||||
parseCachedValue(str, Some("disk"), result.exitCode.contains(failureExitCode))
|
catch
|
||||||
catch case NonFatal(_) => None
|
case NonFatal(e) =>
|
||||||
case Left(_) => None
|
config.logger.debug(
|
||||||
else None
|
s"Ignoring cache retrieval failure, will recompute: ${e.getMessage}"
|
||||||
|
)
|
||||||
readFromSymlink() match
|
Left(None)
|
||||||
case Some(result) => result
|
case Left(_) => Left(None)
|
||||||
case None =>
|
|
||||||
findActionResult(key, codeContentHash, extraHash, config) match
|
|
||||||
case Right(result) =>
|
|
||||||
try
|
|
||||||
val isFailure = result.exitCode.contains(failureExitCode)
|
|
||||||
result.contents.headOption match
|
|
||||||
case Some(head) =>
|
|
||||||
store.syncBlobs(result.outputFiles, config.outputDirectory)
|
|
||||||
val str = String(head.array(), StandardCharsets.UTF_8)
|
|
||||||
parseCachedValue(str, result.origin, isFailure).getOrElse(Left(None))
|
|
||||||
case _ =>
|
|
||||||
val paths = store.syncBlobs(result.outputFiles, config.outputDirectory)
|
|
||||||
if paths.isEmpty then Left(None)
|
|
||||||
else
|
|
||||||
val str = IO.read(paths.head.toFile())
|
|
||||||
parseCachedValue(str, result.origin, isFailure).getOrElse(Left(None))
|
|
||||||
catch
|
|
||||||
case NonFatal(e) =>
|
|
||||||
config.logger.debug(
|
|
||||||
s"Ignoring cache retrieval failure, will recompute: ${e.getMessage}"
|
|
||||||
)
|
|
||||||
Left(None)
|
|
||||||
case Left(_) => Left(None)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the cached value.
|
* Retrieves the cached value.
|
||||||
@@ -216,8 +237,9 @@ object ActionCache:
|
|||||||
tags: List[CacheLevelTag],
|
tags: List[CacheLevelTag],
|
||||||
config: BuildWideCacheConfiguration,
|
config: BuildWideCacheConfiguration,
|
||||||
): Option[O] =
|
): Option[O] =
|
||||||
getWithFailure(key, codeContentHash, extraHash, tags, config) match
|
val inputDigest = mkInput(key, codeContentHash, extraHash, config.cacheVersion)
|
||||||
case Right(value) => Some(value)
|
getWithFailure(inputDigest, tags, config) match
|
||||||
|
case Right(value) => Some(value._1)
|
||||||
case Left(_) => None
|
case Left(_) => None
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -229,10 +251,26 @@ object ActionCache:
|
|||||||
extraHash: Digest,
|
extraHash: Digest,
|
||||||
config: BuildWideCacheConfiguration,
|
config: BuildWideCacheConfiguration,
|
||||||
): Boolean =
|
): Boolean =
|
||||||
findActionResult(key, codeContentHash, extraHash, config) match
|
val inputDigest = mkInput(key, codeContentHash, extraHash, config.cacheVersion)
|
||||||
|
findActionResult(inputDigest, config) match
|
||||||
case Right(_) => true
|
case Right(_) => true
|
||||||
case Left(_) => false
|
case Left(_) => false
|
||||||
|
|
||||||
|
inline private[sbt] def findActionResult(
|
||||||
|
inputDigest: Digest,
|
||||||
|
config: BuildWideCacheConfiguration,
|
||||||
|
): Either[Throwable, ActionResult] =
|
||||||
|
// val logger = config.logger
|
||||||
|
CacheImplicits.setCacheSize(config.localDigestCacheByteSize)
|
||||||
|
val getRequest =
|
||||||
|
GetActionResultRequest(
|
||||||
|
inputDigest,
|
||||||
|
inlineStdout = false,
|
||||||
|
inlineStderr = false,
|
||||||
|
Vector(mkValuePath(inputDigest))
|
||||||
|
)
|
||||||
|
config.store.get(getRequest)
|
||||||
|
|
||||||
inline private[sbt] def findActionResult[I: HashWriter](
|
inline private[sbt] def findActionResult[I: HashWriter](
|
||||||
key: I,
|
key: I,
|
||||||
codeContentHash: Digest,
|
codeContentHash: Digest,
|
||||||
@@ -241,9 +279,14 @@ object ActionCache:
|
|||||||
): Either[Throwable, ActionResult] =
|
): Either[Throwable, ActionResult] =
|
||||||
// val logger = config.logger
|
// val logger = config.logger
|
||||||
CacheImplicits.setCacheSize(config.localDigestCacheByteSize)
|
CacheImplicits.setCacheSize(config.localDigestCacheByteSize)
|
||||||
val (input, valuePath) = mkInput(key, codeContentHash, extraHash, config.cacheVersion)
|
val inputDigest = mkInput(key, codeContentHash, extraHash, config.cacheVersion)
|
||||||
val getRequest =
|
val getRequest =
|
||||||
GetActionResultRequest(input, inlineStdout = false, inlineStderr = false, Vector(valuePath))
|
GetActionResultRequest(
|
||||||
|
inputDigest,
|
||||||
|
inlineStdout = false,
|
||||||
|
inlineStderr = false,
|
||||||
|
Vector(mkValuePath(inputDigest))
|
||||||
|
)
|
||||||
config.store.get(getRequest)
|
config.store.get(getRequest)
|
||||||
|
|
||||||
private inline def mkInput[I: HashWriter](
|
private inline def mkInput[I: HashWriter](
|
||||||
@@ -251,17 +294,20 @@ object ActionCache:
|
|||||||
codeContentHash: Digest,
|
codeContentHash: Digest,
|
||||||
extraHash: Digest,
|
extraHash: Digest,
|
||||||
cacheVersion: Long,
|
cacheVersion: Long,
|
||||||
): (Digest, String) =
|
): Digest =
|
||||||
val effectiveExtraHash =
|
Digest.sha256Hash(
|
||||||
if cacheVersion != 0L then Digest.sha256Hash(extraHash, Digest.dummy(cacheVersion))
|
(Vector(
|
||||||
else extraHash
|
|
||||||
val input =
|
|
||||||
Digest.sha256Hash(
|
|
||||||
codeContentHash,
|
codeContentHash,
|
||||||
effectiveExtraHash,
|
Digest.dummy(Hasher.hashUnsafe[I](key)),
|
||||||
Digest.dummy(Hasher.hashUnsafe[I](key))
|
extraHash
|
||||||
)
|
) ++ {
|
||||||
(input, s"$${OUT}/value/$input.json")
|
if cacheVersion == 0 then Vector.empty
|
||||||
|
else Vector(Digest.dummy(cacheVersion))
|
||||||
|
})*
|
||||||
|
)
|
||||||
|
|
||||||
|
private inline def mkValuePath(inputDigest: Digest): String =
|
||||||
|
s"$${OUT}/value/${inputDigest}.json"
|
||||||
|
|
||||||
def manifestFromFile(manifest: Path): Manifest =
|
def manifestFromFile(manifest: Path): Manifest =
|
||||||
import sbt.internal.util.codec.ManifestCodec.given
|
import sbt.internal.util.codec.ManifestCodec.given
|
||||||
@@ -362,6 +408,14 @@ object ActionCache:
|
|||||||
private[sbt] def unapply[A1](r: InternalActionResult[A1]): Option[(A1, Seq[VirtualFile])] =
|
private[sbt] def unapply[A1](r: InternalActionResult[A1]): Option[(A1, Seq[VirtualFile])] =
|
||||||
Some(r.value, r.outputs)
|
Some(r.value, r.outputs)
|
||||||
end InternalActionResult
|
end InternalActionResult
|
||||||
|
|
||||||
|
private[sbt] def logEvent(event: SpawnExec, log: PrintWriter): Unit =
|
||||||
|
import sbt.internal.util.codec.SpawnCodec.given
|
||||||
|
val json = Converter.toJsonUnsafe(event)
|
||||||
|
val s = PrettyPrinter(json)
|
||||||
|
log.println(s)
|
||||||
|
log.flush()
|
||||||
|
|
||||||
end ActionCache
|
end ActionCache
|
||||||
|
|
||||||
class BuildWideCacheConfiguration(
|
class BuildWideCacheConfiguration(
|
||||||
@@ -387,7 +441,7 @@ class BuildWideCacheConfiguration(
|
|||||||
logger,
|
logger,
|
||||||
cacheEventLog,
|
cacheEventLog,
|
||||||
CacheImplicits.defaultLocalDigestCacheByteSize,
|
CacheImplicits.defaultLocalDigestCacheByteSize,
|
||||||
0L
|
0L,
|
||||||
)
|
)
|
||||||
|
|
||||||
def this(
|
def this(
|
||||||
@@ -398,7 +452,15 @@ class BuildWideCacheConfiguration(
|
|||||||
cacheEventLog: CacheEventLog,
|
cacheEventLog: CacheEventLog,
|
||||||
localDigestCacheByteSize: Long,
|
localDigestCacheByteSize: Long,
|
||||||
) =
|
) =
|
||||||
this(store, outputDirectory, fileConverter, logger, cacheEventLog, localDigestCacheByteSize, 0L)
|
this(
|
||||||
|
store,
|
||||||
|
outputDirectory,
|
||||||
|
fileConverter,
|
||||||
|
logger,
|
||||||
|
cacheEventLog,
|
||||||
|
localDigestCacheByteSize,
|
||||||
|
0L,
|
||||||
|
)
|
||||||
|
|
||||||
override def toString(): String =
|
override def toString(): String =
|
||||||
s"BuildWideCacheConfiguration(store = $store, outputDirectory = $outputDirectory)"
|
s"BuildWideCacheConfiguration(store = $store, outputDirectory = $outputDirectory)"
|
||||||
|
|||||||
@@ -83,7 +83,8 @@ end AbstractActionCacheStore
|
|||||||
/**
|
/**
|
||||||
* An aggregate ActionCacheStore.
|
* An aggregate ActionCacheStore.
|
||||||
*/
|
*/
|
||||||
class AggregateActionCacheStore(stores: Seq[ActionCacheStore]) extends AbstractActionCacheStore:
|
case class AggregateActionCacheStore(stores: Seq[ActionCacheStore])
|
||||||
|
extends AbstractActionCacheStore:
|
||||||
extension [A1](xs: Seq[A1])
|
extension [A1](xs: Seq[A1])
|
||||||
// unlike collectFirst this accepts A1 => Seq[A2]
|
// unlike collectFirst this accepts A1 => Seq[A2]
|
||||||
inline def collectFirst2[A2](f: A1 => Seq[A2], size: Int): Seq[A2] =
|
inline def collectFirst2[A2](f: A1 => Seq[A2], size: Int): Seq[A2] =
|
||||||
@@ -176,7 +177,8 @@ class InMemoryActionCacheStore extends AbstractActionCacheStore:
|
|||||||
underlying.toString()
|
underlying.toString()
|
||||||
end InMemoryActionCacheStore
|
end InMemoryActionCacheStore
|
||||||
|
|
||||||
class DiskActionCacheStore(base: Path, converter: FileConverter) extends AbstractActionCacheStore:
|
case class DiskActionCacheStore(base: Path, converter: FileConverter)
|
||||||
|
extends AbstractActionCacheStore:
|
||||||
lazy val casBase: Path = {
|
lazy val casBase: Path = {
|
||||||
val dir = base.resolve("cas")
|
val dir = base.resolve("cas")
|
||||||
IO.createDirectory(dir.toFile)
|
IO.createDirectory(dir.toFile)
|
||||||
|
|||||||
Reference in New Issue
Block a user