Skip to content

Rewind repo fetches for files lost outside action execution - #30465

Draft
fmeum wants to merge 23 commits into
bazelbuild:masterfrom
fmeum:rewind-lost-repo-files
Draft

Rewind repo fetches for files lost outside action execution#30465
fmeum wants to merge 23 commits into
bazelbuild:masterfrom
fmeum:rewind-lost-repo-files

Conversation

@fmeum

@fmeum fmeum commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Description

Recovers files that the remote repo contents cache has lost through Skyframe rewinding instead of restarting the whole build.

--rewind_lost_inputs already recovers a lost source file that an action consumes (#29818). A file of a cached repo is also read at points that are not actions at all: while a repo rule runs, while a module extension is evaluated, and while a package is loaded. Those still failed the build with REMOTE_CACHE_EVICTED and were left to the bounded retry of the whole invocation.

While rewinding is enabled, the overlay file system now reports a lost file as a LostRepoFileException naming the repo that has to be fetched again. The failing Skyframe node returns a Reset that rewinds that repo's fetch and is re-evaluated once the repo rule has run again, all within the same build.

Rewinding the repo fetch alone is not enough. Reading a file by label resolves its package first, and a package lookup that stays valid hands out the very repo root whose contents are stale, so the rewound fetch is never consulted. The lost file therefore records the label it was read through, and the package lookup is rewound in between.

Two smaller pieces come along: materializing a repo moves onto the RewindableRepoFileSystem interface so that it no longer depends on the concrete remote file system, and a file read on behalf of a different package (a BUILD file loading a .bzl file from another repo) deliberately falls back to the bounded retry rather than building an incomplete rewind graph.

.bzl files need no handling of their own: they are prefetched when a repo is injected, so a lost one is already reported as a cache miss at that point, and the prefetched contents are written to the local file system, so a later read never reaches the remote cache.

Stacked on #30226 and #29818, whose commits this branch currently carries. Only the last two commits are new.

Motivation

With the remote repo contents cache enabled, a cache that serves action results without checking that the blobs they reference still exist can hand out a repo whose Tree outlived its file contents. Recovering by restarting the build costs one of the bounded --experimental_remote_cache_eviction_retries per affected repo, because each attempt only ever discovers the first lost file. A module extension that materializes one cached repo after another aborts at the first failure and only reaches the next repo once the previous one has recovered.

Rewinding makes that cost disappear: each affected repo is simply rewound in place, so a workspace with many stale repos recovers in a single invocation.

Build API Changes

No

Checklist

  • I have added tests for the new use cases (if any).
  • I have updated the documentation (if applicable).

Release Notes

RELNOTES: None

fmeum and others added 17 commits July 24, 2026 11:42
The remote repo contents cache injects a cached repo's metadata without
verifying that every leaf blob referenced by its Tree is still available
in the CAS. If a blob had been evicted by the time another repo rule
triggered full materialization through rctx.path() or rctx.read(), the
build failed with "Failed to materialize remote repo ...: Missing
digest ..." and never reran the repo rule, not even with the default
whole-command remote cache eviction retries.

Full materialization now handles an all-CacheNotFoundException prefetch
failure like the existing on-demand read path: the repo is marked as
having lost files and a transient REMOTE_FILE_EVICTED error is raised.
The failure detail is preserved through repo rule evaluation (as the
EvalException cause) and PackageLookupFunction so that the command
exits with REMOTE_CACHE_EVICTED and is automatically retried. Since the
stale ActionResult and Tree survive in the remote cache, retried
commands skip the cache lookup for such repos until their contents,
including the lost blobs, have been fetched and uploaded again, which
heals the cache entry.

Fixes bazelbuild#30218.

RELNOTES: The remote repo contents cache now recovers from cached
repository contents that are no longer fully available in the remote
cache by refetching the repository.
…nally

Repos whose cached contents referenced lost remote files must be
refetched and thus have no use for their stale in-memory contents, even
if no materialization was attempted for them. This also simplifies the
eviction filter for materialized repos.
A module extension that triggers the full materialization of a cached
repo via module_ctx.path() previously converted the transient
REMOTE_FILE_EVICTED failure into a persistent EXTENSION_EVAL_ERROR, so
the build failed without a retry. ExternalDepsException now derives its
exit code and transience from a DetailedIOException cause so that the
failure detail is preserved through module extension evaluation and the
build is retried, refetching the repo.
testLostRemoteFile_sourceDirectoryMaterialization covers a cached repo
whose only lost CAS blob lies below a source directory input consumed by
a local action: the materialization of just that subtree discovers the
loss and must trigger the transparent refetch that heals the cache
entry.
A repo that is only reachable through a dependency edge is fetched
during the analysis of the target that depends on it. If such a fetch
fails because a file is no longer available in the remote cache, the
resulting REMOTE_CACHE_EVICTED exit code was replaced with a generic
one and the build was not retried, in both of the ways such an error
can be reported:

With --keep_going, the error is aggregated into an ErrorProcessingResult
and BuildView#createAnalysisFailureDetail turns it into a generic
loading or analysis phase failure. The detailed exit code of an analysis
root cause that can be resolved by a retry is now preserved and takes
precedence over these generic failure details.

With --nokeep_going, the error is thrown as a ViewCreationFailedException
whose failure detail does describe the lost file, but BuildTool
unconditionally paired it with the PARSING_FAILURE exit code. The exit
code implied by the failure detail is now kept if it allows a retry.
The remote worker used by the tests serves an action result only after
checking that all blobs it references are still present in the CAS. Real
remote caches commonly don't do this, which is precisely what allows a
repo's cached Tree to outlive the contents of its files and is the
situation the remote repo contents cache has to recover from.

Run the tests against a worker with --noaction_cache_integrity_check so
that a repo whose blobs have been deleted is served from the cache
rather than silently turning into a cache miss.
A repo that is only reachable through an implicit attribute of a
top-level aspect is fetched during the analysis of that aspect. If the
fetch fails because a file is no longer available in the remote cache,
the resulting REMOTE_CACHE_EVICTED exit code was replaced with a generic
one with --keep_going and the build was not retried.

Unlike a ConfiguredValueCreationException, whose root causes carry the
detailed exit code all the way into the ErrorProcessingResult, the root
causes of an AspectCreationException were dropped and the aspect's
IndividualErrorProcessingResult was always reported with an empty set of
analysis root causes. BuildView#createAnalysisFailureDetail then fell
back to NOT_ALL_TARGETS_ANALYZED.

Keep the causes of an AspectCreationException, just like those of a
ConfiguredValueCreationException, which both restores the retry and
populates the AnalysisFailureEvent posted for a top-level aspect.
A file lost from the remote cache is never specific to a single repo,
but it is only noticed when that particular file is read. Repos with
lost files were thus discovered one at a time, each costing one of the
bounded number of build retries: a module extension that materializes
one cached repo after another aborts at the first failure and only gets
to the next repo once the previous one has been refetched. With enough
affected repos, --experimental_remote_cache_eviction_retries is
exhausted before the build can recover.

Once a lost file has been observed, discard all repos whose contents
were restored from the cache, not just those in which a lost file was
found, and have the next command verify a repo's cached contents against
the remote cache before restoring them. A cached Tree that references a
blob the cache no longer has is treated as a miss, so the repo is
fetched and its contents are uploaded anew right away. All affected
repos therefore recover in the same retry.

Builds that don't encounter a lost file are unaffected: no additional
FindMissingBlobs requests are sent and no repos are discarded.
--nobuild turns off the merged analysis and execution phase, and so do
cquery and aquery, which imply it. Analysis errors are then reported
through AnalysisPhaseRunner and SkyframeErrorProcessor#processErrors
without the execution phase rather than through
AnalysisAndExecutionPhaseRunner.

Run the tests for the retry after a lost remote file discovered during
analysis with --nobuild as well. The aspect variant with --keep_going
fails without the preceding commit.
With --keep_going and a second, healthy target, the analysis error of a
repo that could not be fetched is reported together with the results of
actions that did execute. Release branches that still have
--experimental_skyframe_error_handling_refactor handle that case in a
separate code path that derives the exit code without looking at
analysis root causes, so the lost remote file has to survive it for the
build to be retried.
…ontents cache

When an action input is a source file in an external repository served
from the remote repo contents cache and the remote cache loses the
file's contents, the action fails with a lost inputs exception. Source
artifacts have no generating action to rewind, so this previously
crashed action rewinding (or fell back to restarting the build when
invocation retries are enabled).

With --rewind_lost_inputs, such lost source files are now recovered
within the build by rewinding the Skyframe chain that produced the
file's metadata: the source artifact, its file and file state nodes,
and the fetch of the repository containing it. The repo is marked as
having lost files in the RemoteExternalOverlayFileSystem, which makes
the next remote repo contents cache lookup report a miss so that the
repo rule is executed again. The refetch materializes the repo on the
local file system and re-uploads its contents to the remote cache,
after which the failed action is retried.

(cherry picked from commit 27dbb60)
SequencedSkyframeProgressReceiver assumed that FileKey nodes can only be
dirtied during loading and analysis, but rewinding a lost source file
served from the remote repo contents cache dirties them during the
execution phase, when the incremental build monitor is null.

(cherry picked from commit 8534b72)
Covers two genrules concurrently losing source file inputs served from
external repositories, both for distinct repositories and for the same
repository. The lost inputs are recovered by concurrently rewinding the
source artifacts' metadata nodes together with the repository fetches,
which re-execute the repo rules.

The test lives in a separate class from RewindingTest because that test
disables external repositories to preserve its action graph structure
between blaze and bazel.

(cherry picked from commit 2a798a0)
Adds two test cases to RepoRewindingTest:
* An action that is already executing when the repository providing one
  of its inputs is refetched due to another action's lost input still
  completes successfully and is not rewound itself.
* A repo that materializes another repo (by reading one of its files by
  label during its fetch) is unaffected by that repo being refetched to
  recover a lost input later in the build and picks up the refetched
  contents through its recorded inputs in the next build.

(cherry picked from commit afdbf34)
A file of a cached repo can be read at any point at which Bazel fetches
a repo or evaluates a module extension, none of which is an action that
ActionRewindStrategy could rewind. Such a read previously failed the
build with an exit code that made it start over from scratch, and since
each attempt only ever discovers the first lost file, recovering N
affected repos cost N of the bounded number of retries.

While rewinding is enabled, the overlay file system now reports a lost
file as a LostRepoFileException naming the repo that has to be fetched
again. The failing node returns a Reset that rewinds that repo's fetch
and is re-evaluated once the repo rule has run again, all within the
same build. A module extension that reads files of several such repos
aborts at the first one, so it simply rewinds once per repo instead of
restarting the build once per repo.

Rewinding the repo fetch alone is not enough: reading a file by label
resolves its package first, and a package lookup that stays valid hands
out the very repo root whose contents are stale, so the rewound fetch is
never consulted. The lost file therefore records the label it was read
through and the package lookup is rewound in between.

Materializing a repo moves to the RewindableRepoFileSystem interface so
that it no longer depends on the concrete remote file system, which also
lets tests exercise the path on which the loss surfaces in production.

Hooked up for module extension evaluation and repo fetching.
A BUILD file of a cached repo is read while loading its package, which
is not an action that ActionRewindStrategy could rewind, so losing it
from the remote cache still restarted the whole build.

PackageFunction now recovers such a file the same way repo rules and
module extensions already do: rewind the fetch of the repo containing it
and re-evaluate once the repo rule has run again. It knows the package
it read the file for, so the package lookup is rewound in between
without having to plumb a label through.

A file read on behalf of a different package, such as a .bzl file that a
BUILD file loads from another repo, is left alone: recovering it needs
the load in between rewound as well, and an incomplete rewind graph
leaves the package missing rather than merely unrecovered. Those still
fall back to restarting the build.

Note that a lost file does not surface when its metadata is read: the
overlay file system has that injected, so stat never reaches the cache
and only reading the contents fails. FileStateFunction is therefore not
involved.
fmeum added 6 commits July 28, 2026 15:38
Runs the remote repo contents cache tests with --rewind_lost_inputs, so
that every way in which the cache can lose a file is recovered within
the build rather than by restarting it. None of them expects a retry any
more, and the lost file is no longer reported as an error since the
build recovers from it.

Two gaps had to be closed for that:

A failed materialization was memoized in the map of in-flight
materializations, so a repo rule that ran again after its repo had been
rewound was handed the previous failure instead of materializing the
repo anew. Failures are no longer remembered.

The contents of a source directory input are materialized while
prefetching an action's inputs, where a lost file surfaced as a plain
I/O error that failed the action outright. It is now reported as a lost
input naming the source directory, which lets the existing action
rewinding recover it by refetching the repo, just like a lost regular
file input.
Rewinding recovers every way in which the remote cache can lose a file
of a cached repo within the build, so the machinery that instead
restarted the whole invocation is no longer needed.

Gone are the propagation of a retryable exit code out of analysis, both
for configured targets and for aspects, and the recovery that discarded
every cached repo and verified the remaining ones against the cache on
the following attempt so that a single retry sufficed.

Recovering from a lost repo file now requires --rewind_lost_inputs.
Rewinding refetches a repo whose file the remote cache has lost while
the build that needs it is still running, so remembering across commands
that a repo has to be fetched rather than restored from the cache no
longer serves a purpose.
Rewinding dirties file nodes to recover files that the remote repo
contents cache has lost, which is not a change to a source file and thus
must not be accrued by the incremental build monitor.

The monitor happens to be null at that point, since the changed files
are drained before the execution phase, but a null check says nothing
about why reporting would be wrong and would silently start reporting
again if the monitor ever outlived the loading phase. Skip rewound nodes
explicitly instead.

Also restores the .gemini configuration dropped by an earlier rebase.
A cache lookup cleared the record that the remote cache had lost files
of a repo, on the assumption that the lookup's cache miss would be
followed by a fetch that uploads the repo anew. A lookup is not that
promise: the fetch is skipped with --nofetch and restarted when the repo
rule's state object is dropped under memory pressure. The record is now
kept until the contents have actually been uploaded, which is also when
the previously lost files become available again.

This restores the behavior that the retry-based recovery had, whose
reasons for it apply just as much to recovery by rewinding.

Not covered by a test: with rewinding enabled, restoring stale contents
is not observable in the build's result, since reading the lost file
merely rewinds the repo's fetch a second time.
Lost repo files are recovered by rewinding, so none of the machinery that
made them surface as the exit code triggering a whole-build retry is used
any more. Revert it to its state before this branch: the causal chain walk
in `PackageLookupFunction`, `RepositoryFetchException`'s `DetailedExitCode`
constructor, `ExternalDepsException`'s transience, and `DetailedIOException`
becomes final again.

A lost `.bzl` file can't reach loading in the first place: `.bzl` and
`REPO.bazel` files are prefetched when a repo is injected and a lost blob
among them turns the injection into a plain cache miss. Drop the label
plumbing through `BzlLoadFunction` that existed to rewind such a file, and
with it `RepoRewinding`'s fallback to the whole-build retry when the label
resolves in another package.

`vfs/LostRepoFileException` and `LostRepoFileWhileReadingLabelException`
collapse into a single `LostRemoteRepoFileException` next to `RepoRewinding`,
holding the repo to refetch and, once `getPathFromLabel` has attached it,
the label the file was read through.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant