fix(wiki): retry a commit whose walk read a file that vanished mid-scan

is_racy_read only recognized libgit2's Filesystem-class "file changed before
we could read it", so the Os-class "failed to read file into stream" (ENOENT:
listed by the walk, gone when streamed in — e.g. an atomic writer's temp file
renamed away) failed the checkpoint outright instead of taking the bounded
racy-read retry. Seen as an intermittent
concurrent_commits_queue_instead_of_failing failure on a loaded CI runner
(v2.5.1 RC dispatch run). Classifier unit test covers both racy shapes and
two non-racy controls; it fails with the old single-branch check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
This commit is contained in:
AkitaOnRails
2026-10-01 02:50:21 -03:00
co-authored by Claude Opus 5.5
parent 695805eb88
commit af8ed952e7
2 changed files with 46 additions and 2 deletions
+8
View File
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
- Fixed a wiki checkpoint failing instead of retrying when a full walk listed a
file that was gone by the time it was read (libgit2's `Os`-class "failed to
read file into stream", e.g. an atomic writer's temp file renamed away
mid-walk). It now takes the same bounded racy-read retry as a file changed
mid-write; unrelated I/O errors still fail fast. This was also the source of
an intermittent `concurrent_commits_queue_instead_of_failing` CI failure.
## [2.5.1] - 2026-10-01
### Fixed
+38 -2
View File
@@ -79,9 +79,18 @@ const UNREPORTED_SAMPLE: usize = 5;
const RACY_READ_ATTEMPTS: u32 = 4;
const RACY_READ_BACKOFF: Duration = Duration::from_millis(25);
/// libgit2's "file changed before we could read it": a writer was mid-write.
/// A staging read that raced a writer outside the commit lock: libgit2's
/// "file changed before we could read it" (mid-write), or a file that was
/// listed by the walk but gone by the time libgit2 streamed it in (an `Os`
/// "failed to read file into stream" — e.g. an atomic writer's temp file
/// renamed away between the scan and the read). Both settle within
/// milliseconds; a rescan no longer sees a vanished file.
fn is_racy_read(e: &git2::Error) -> bool {
e.class() == git2::ErrorClass::Filesystem && e.message().contains("changed before")
match e.class() {
git2::ErrorClass::Filesystem => e.message().contains("changed before"),
git2::ErrorClass::Os => e.message().contains("failed to read file into stream"),
_ => false,
}
}
/// Writes that reached the tree without a report: a writer bypassed the wiki.
@@ -1436,6 +1445,33 @@ mod tests {
/// Two session ends at once used to collide on libgit2's index lock and
/// one of them lost its snapshot; now they queue.
/// The CI flake behind `concurrent_commits_queue_instead_of_failing`: a
/// walk listed a file that was gone when libgit2 read it, and libgit2
/// reports that as an `Os`-class "failed to read file into stream" — not
/// the "changed before" the retry recognized — so the commit failed
/// instead of retrying. Unrelated errors must still fail fast.
#[test]
fn a_file_vanishing_mid_walk_is_a_racy_read_but_other_errors_are_not() {
let err =
|class, message: &str| git2::Error::new(git2::ErrorCode::GenericError, class, message);
assert!(is_racy_read(&err(
git2::ErrorClass::Os,
"failed to read file into stream: "
)));
assert!(is_racy_read(&err(
git2::ErrorClass::Filesystem,
"file changed before we could read it"
)));
assert!(!is_racy_read(&err(
git2::ErrorClass::Os,
"failed to open file"
)));
assert!(!is_racy_read(&err(
git2::ErrorClass::Index,
"failed to read file into stream: "
)));
}
#[test]
fn concurrent_commits_queue_instead_of_failing() {
let tmp = tempdir();