[2.x] fix: Widen IOException catch in organicTask to cover result serialization (#9050)

**Problem**
When a file referenced during cache serialization is deleted between task
execution and cache storage, sbt crashes with an uncaught NoSuchFileException.
The IOException catch added in #8699 only wraps store.put(), but
Converter.toJsonUnsafe(result) and mkInput() also call
hashedVirtualFileRefToStr which calls Files.readAttributes — and these are
outside the try-catch.

**Solution**
Move the try/catch IOException to wrap the entire post-action cache storage
section, so NoSuchFileException from result serialization or cache key
computation is caught and handled gracefully (skip caching, return result).

Fixes #9044

Co-authored-by: bittoby <218712309+bittoby@users.noreply.github.co>
This commit is contained in:
BitToby 2026-04-09 23:44:08 -06:00 committed by GitHub
parent d765ba263a
commit e0bdb531f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 29 additions and 29 deletions

View File

@ -82,40 +82,40 @@ object ActionCache:
case e: Exception => case e: Exception =>
cacheEventLog.append(ActionCacheEvent.Error) cacheEventLog.append(ActionCacheEvent.Error)
throw e throw e
val json = Converter.toJsonUnsafe(result) try
val normalizedOutputDir = outputDirectory.toAbsolutePath.normalize() val json = Converter.toJsonUnsafe(result)
val uncacheableOutputs = val normalizedOutputDir = outputDirectory.toAbsolutePath.normalize()
outputs.filter(f => val uncacheableOutputs =
f match outputs.filter(f =>
case vf if vf.id.endsWith(ActionCache.dirZipExt) => f match
false case vf if vf.id.endsWith(ActionCache.dirZipExt) =>
case _ => false
val outputPath = fileConverter.toPath(f).toAbsolutePath.normalize() case _ =>
!outputPath.startsWith(normalizedOutputDir) val outputPath = fileConverter.toPath(f).toAbsolutePath.normalize()
) !outputPath.startsWith(normalizedOutputDir)
if uncacheableOutputs.nonEmpty then )
cacheEventLog.append(ActionCacheEvent.Error) if uncacheableOutputs.nonEmpty then
logger.error( cacheEventLog.append(ActionCacheEvent.Error)
s"Cannot cache task because its output files are outside the output directory: \n" + logger.error(
uncacheableOutputs.mkString(" - ", "\n - ", "") s"Cannot cache task because its output files are outside the output directory: \n" +
) uncacheableOutputs.mkString(" - ", "\n - ", "")
result )
else result
cacheEventLog.append(ActionCacheEvent.OnsiteTask) else
val (input, valuePath) = mkInput(key, codeContentHash, extraHash, config.cacheVersion) cacheEventLog.append(ActionCacheEvent.OnsiteTask)
val valueFile = StringVirtualFile1(valuePath, CompactPrinter(json)) val (input, valuePath) = mkInput(key, codeContentHash, extraHash, config.cacheVersion)
val newOutputs = Vector(valueFile) ++ outputs.toVector val valueFile = StringVirtualFile1(valuePath, CompactPrinter(json))
try val newOutputs = Vector(valueFile) ++ outputs.toVector
store.put(UpdateActionResultRequest(input, newOutputs, exitCode = 0)) match store.put(UpdateActionResultRequest(input, newOutputs, exitCode = 0)) match
case Right(cachedResult) => case Right(cachedResult) =>
store.syncBlobs(cachedResult.outputFiles, outputDirectory) store.syncBlobs(cachedResult.outputFiles, outputDirectory)
result result
case Left(e) => throw e case Left(e) => throw e
catch catch
case e: IOException => case e: IOException =>
logger.debug(s"Skipping cache storage due to error: ${e.getMessage}") logger.debug(s"Skipping cache storage due to error: ${e.getMessage}")
cacheEventLog.append(ActionCacheEvent.Error) cacheEventLog.append(ActionCacheEvent.Error)
result result
// 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(key, codeContentHash, extraHash, tags, config) match