authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-04-24 01:17:32+01:00
committergravatar for alex@alexrp.comAlex Rønne Petersen <alex@alexrp.com> 2025-04-27 14:08:21+02:00
log160f2dabed9a465075ddf9c46afadca96e415468
tree02ef3e3a693aa0a927f3459ef3277f9dcd1cf287
parent53f298cffa5bbec4c4faa854afb56a5f2e23de48
signaturebadge-check Signed by SSH key SHA256:7B/LJ7bpR1eX8aCXSr4mtd5M45VMPKcx9zY8e95b5QM

std.Build.Cache: fix several bugs

Aside from adding comments to document the logic in `Cache.Manifest.hit` better, this commit fixes two serious bugs. The first, spotted by Andrew, is that when upgrading from a shared to an exclusive lock on the manifest file, we do not seek it back to the start. This is a simple fix. The second is more subtle, and has to do with the computation of file digests. Broadly speaking, the goal of the main loop in `hit` is to iterate the files listed in the manifest file, and check if they've changed, based on stat and a file hash. While doing this, the `bin_digest` field of `std.Build.Cache.File`, which is initially `undefined`, is populated for all files, either straight from the manifest (if the stat matches) or recomputed from the file on-disk. This file digest is then used to update `man.hash.hasher`, which is building the final hash used as, for instance, the output directory name when the compiler emits into the cache directory. When `hit` returns a cache miss, it is expected that `man.hash.hasher` includes the digests of all "initial files"; that is, those which have been already added with e.g. `addFilePath`, but not those which will later be added with `addFilePost` (even though the manifest file has told us about some such files). Previously, `hit` was using the `unhit` function to do this in a few cases. However, this is incorrect, because `hit` assumes that all files already have their `bin_digest` field populated; this function is only valid to call *after* `hit` returns. Instead, we need to actually compute the hashes which haven't yet been populated. Even if this logic has been working, there was still a bug here, because we called `unhit` when upgrading from a shared to an exclusive lock, writing the (potentially `undefined`) file digests, but the loop itself writes the file digests *again*! All in all, the hashing logic here was actually incredibly broken. I've taken the opportunity to restructure this section of the code into what I think is a more readable format. A new function, `hitWithCurrentLock`, uses the open manifest file to try and find a cache hit. It returns a tagged union which, in the miss case, tells the caller (`hit`) how many files already have their hash populated. This avoids redundant work recomputing the same hash multiple times in situations where the lock needs upgrading. This also eliminates the outer loop from `hit`, which was a little confusing because it iterated no more than twice! The bugs fixed here could manifest in several different ways depending on how contended file locks were satisfied. Most notably, on a cache miss, the Zig compiler might have written the compilation output to the incorrect directory (because it incorrectly constructed a hash using `undefined` or repeated file digests), resulting in all future hits on this manifest causing `error.FileNotFound`. This is #23110. I have been able to reproduce #23110 on `master`, and have not been able to after this commit, so I am relatively sure this commit resolves that issue. Resolves: #23110

3 files changed, 233 insertions(+), 165 deletions(-)

lib/std/Build/Cache.zig+231-163
...@@ -337,6 +337,7 @@ pub const Manifest = struct {...@@ -337,6 +337,7 @@ pub const Manifest = struct {
337 manifest_create: fs.File.OpenError,337 manifest_create: fs.File.OpenError,
338 manifest_read: fs.File.ReadError,338 manifest_read: fs.File.ReadError,
339 manifest_lock: fs.File.LockError,339 manifest_lock: fs.File.LockError,
340 manifest_seek: fs.File.SeekError,
340 file_open: FileOp,341 file_open: FileOp,
341 file_stat: FileOp,342 file_stat: FileOp,
342 file_read: FileOp,343 file_read: FileOp,
...@@ -488,7 +489,6 @@ pub const Manifest = struct {...@@ -488,7 +489,6 @@ pub const Manifest = struct {
488 /// option, one may call `toOwnedLock` to obtain a smaller object which can represent489 /// option, one may call `toOwnedLock` to obtain a smaller object which can represent
489 /// the lock. `deinit` is safe to call whether or not `toOwnedLock` has been called.490 /// the lock. `deinit` is safe to call whether or not `toOwnedLock` has been called.
490 pub fn hit(self: *Manifest) HitError!bool {491 pub fn hit(self: *Manifest) HitError!bool {
491 const gpa = self.cache.gpa;
492 assert(self.manifest_file == null);492 assert(self.manifest_file == null);
493493
494 self.diagnostic = .none;494 self.diagnostic = .none;
...@@ -501,12 +501,12 @@ pub const Manifest = struct {...@@ -501,12 +501,12 @@ pub const Manifest = struct {
501501
502 self.hex_digest = binToHex(bin_digest);502 self.hex_digest = binToHex(bin_digest);
503503
504 self.hash.hasher = hasher_init;
505 self.hash.hasher.update(&bin_digest);
506
507 @memcpy(manifest_file_path[0..self.hex_digest.len], &self.hex_digest);504 @memcpy(manifest_file_path[0..self.hex_digest.len], &self.hex_digest);
508 manifest_file_path[hex_digest_len..][0..ext.len].* = ext.*;505 manifest_file_path[hex_digest_len..][0..ext.len].* = ext.*;
509506
507 // We'll try to open the cache with an exclusive lock, but if that would block
508 // and `want_shared_lock` is set, a shared lock might be sufficient, so we'll
509 // open with a shared lock instead.
510 while (true) {510 while (true) {
511 if (self.cache.manifest_dir.createFile(&manifest_file_path, .{511 if (self.cache.manifest_dir.createFile(&manifest_file_path, .{
512 .read = true,512 .read = true,
...@@ -575,26 +575,71 @@ pub const Manifest = struct {...@@ -575,26 +575,71 @@ pub const Manifest = struct {
575 self.want_refresh_timestamp = true;575 self.want_refresh_timestamp = true;
576576
577 const input_file_count = self.files.entries.len;577 const input_file_count = self.files.entries.len;
578 while (true) : (self.unhit(bin_digest, input_file_count)) {578
579 const file_contents = self.manifest_file.?.reader().readAllAlloc(gpa, manifest_file_size_max) catch |err| switch (err) {579 // We're going to construct a second hash. Its input will begin with the digest we've
580 error.OutOfMemory => return error.OutOfMemory,580 // already computed (`bin_digest`), and then it'll have the digests of each input file,
581 error.StreamTooLong => return error.OutOfMemory,581 // including "post" files (see `addFilePost`). If this is a hit, we learn the set of "post"
582 else => |e| {582 // files from the manifest on disk. If this is a miss, we'll learn those from future calls
583 self.diagnostic = .{ .manifest_read = e };583 // to `addFilePost` etc. As such, the state of `self.hash.hasher` after this function
584 // depends on whether this is a hit or a miss.
585 //
586 // If we return `true` indicating a cache hit, then `self.hash.hasher` must already include
587 // the digests of the "post" files, so the caller can call `final`. Otherwise, on a cache
588 // miss, `self.hash.hasher` will include the digests of all non-"post" files -- that is,
589 // the ones we've already been told about. The rest will be discovered through calls to
590 // `addFilePost` etc, which will update the hasher. After all files are added, the user can
591 // use `final`, and will at some point `writeManifest` the file list to disk.
592
593 self.hash.hasher = hasher_init;
594 self.hash.hasher.update(&bin_digest);
595
596 hit: {
597 const file_digests_populated: usize = digests: {
598 switch (try self.hitWithCurrentLock()) {
599 .hit => break :hit,
600 .miss => |m| if (!try self.upgradeToExclusiveLock()) {
601 break :digests m.file_digests_populated;
602 },
603 }
604 // We've just had a miss with the shared lock, and upgraded to an exclusive lock. Someone
605 // else might have modified the digest, so we need to check again before deciding to miss.
606 // Before trying again, we must reset `self.hash.hasher` and `self.files`.
607 // This is basically just the first half of `unhit`.
608 self.hash.hasher = hasher_init;
609 self.hash.hasher.update(&bin_digest);
610 while (self.files.count() != input_file_count) {
611 var file = self.files.pop().?;
612 file.key.deinit(self.cache.gpa);
613 }
614 // Also, seek the file back to the start.
615 self.manifest_file.?.seekTo(0) catch |err| {
616 self.diagnostic = .{ .manifest_seek = err };
584 return error.CacheCheckFailed;617 return error.CacheCheckFailed;
585 },618 };
619
620 switch (try self.hitWithCurrentLock()) {
621 .hit => break :hit,
622 .miss => |m| break :digests m.file_digests_populated,
623 }
586 };624 };
587 defer gpa.free(file_contents);625
588626 // This is a guaranteed cache miss. We're almost ready to return `false`, but there's a
589 var any_file_changed = false;627 // little bookkeeping to do first. The first `file_digests_populated` entries in `files`
590 var line_iter = mem.tokenizeScalar(u8, file_contents, '\n');628 // have their `bin_digest` populated; there may be some left in `input_file_count` which
591 var idx: usize = 0;629 // we'll need to populate ourselves. Other than that, this is basically `unhit`.
592 if (if (line_iter.next()) |line| !std.mem.eql(u8, line, manifest_header) else true) {630 self.manifest_dirty = true;
593 if (try self.upgradeToExclusiveLock()) continue;631 self.hash.hasher = hasher_init;
594 self.manifest_dirty = true;632 self.hash.hasher.update(&bin_digest);
595 while (idx < input_file_count) : (idx += 1) {633 while (self.files.count() != input_file_count) {
596 const ch_file = &self.files.keys()[idx];634 var file = self.files.pop().?;
597 self.populateFileHash(ch_file) catch |err| {635 file.key.deinit(self.cache.gpa);
636 }
637 for (self.files.keys(), 0..) |*file, idx| {
638 if (idx < file_digests_populated) {
639 // `bin_digest` is already populated by `hitWithCurrentLock`, so we can use it directly.
640 self.hash.hasher.update(&file.bin_digest);
641 } else {
642 self.populateFileHash(file) catch |err| {
598 self.diagnostic = .{ .file_hash = .{643 self.diagnostic = .{ .file_hash = .{
599 .file_index = idx,644 .file_index = idx,
600 .err = err,645 .err = err,
...@@ -602,172 +647,195 @@ pub const Manifest = struct {...@@ -602,172 +647,195 @@ pub const Manifest = struct {
602 return error.CacheCheckFailed;647 return error.CacheCheckFailed;
603 };648 };
604 }649 }
605 return false;
606 }650 }
607 while (line_iter.next()) |line| {651 return false;
608 defer idx += 1;652 }
609653
610 var iter = mem.tokenizeScalar(u8, line, ' ');654 if (self.want_shared_lock) {
611 const size = iter.next() orelse return error.InvalidFormat;655 self.downgradeToSharedLock() catch |err| {
612 const inode = iter.next() orelse return error.InvalidFormat;656 self.diagnostic = .{ .manifest_lock = err };
613 const mtime_nsec_str = iter.next() orelse return error.InvalidFormat;657 return error.CacheCheckFailed;
614 const digest_str = iter.next() orelse return error.InvalidFormat;658 };
615 const prefix_str = iter.next() orelse return error.InvalidFormat;659 }
616 const file_path = iter.rest();
617
618 const stat_size = fmt.parseInt(u64, size, 10) catch return error.InvalidFormat;
619 const stat_inode = fmt.parseInt(fs.File.INode, inode, 10) catch return error.InvalidFormat;
620 const stat_mtime = fmt.parseInt(i64, mtime_nsec_str, 10) catch return error.InvalidFormat;
621 const file_bin_digest = b: {
622 if (digest_str.len != hex_digest_len) return error.InvalidFormat;
623 var bd: BinDigest = undefined;
624 _ = fmt.hexToBytes(&bd, digest_str) catch return error.InvalidFormat;
625 break :b bd;
626 };
627660
628 const prefix = fmt.parseInt(u8, prefix_str, 10) catch return error.InvalidFormat;661 return true;
629 if (prefix >= self.cache.prefixes_len) return error.InvalidFormat;662 }
630663
631 if (file_path.len == 0) return error.InvalidFormat;664 /// Assumes that `self.hash.hasher` has been updated only with the original digest, that
665 /// `self.files` contains only the original input files, and that `self.manifest_file.?` is
666 /// seeked to the start of the file.
667 fn hitWithCurrentLock(self: *Manifest) HitError!union(enum) {
668 hit,
669 miss: struct {
670 file_digests_populated: usize,
671 },
672 } {
673 const gpa = self.cache.gpa;
674 const input_file_count = self.files.entries.len;
632675
633 const cache_hash_file = f: {676 const file_contents = self.manifest_file.?.reader().readAllAlloc(gpa, manifest_file_size_max) catch |err| switch (err) {
634 const prefixed_path: PrefixedPath = .{677 error.OutOfMemory => return error.OutOfMemory,
635 .prefix = prefix,678 error.StreamTooLong => return error.OutOfMemory,
636 .sub_path = file_path, // expires with file_contents679 else => |e| {
637 };680 self.diagnostic = .{ .manifest_read = e };
638 if (idx < input_file_count) {681 return error.CacheCheckFailed;
639 const file = &self.files.keys()[idx];682 },
640 if (!file.prefixed_path.eql(prefixed_path))683 };
641 return error.InvalidFormat;684 defer gpa.free(file_contents);
685
686 var any_file_changed = false;
687 var line_iter = mem.tokenizeScalar(u8, file_contents, '\n');
688 var idx: usize = 0;
689 const header_valid = valid: {
690 const line = line_iter.next() orelse break :valid false;
691 break :valid std.mem.eql(u8, line, manifest_header);
692 };
693 if (!header_valid) {
694 return .{ .miss = .{ .file_digests_populated = 0 } };
695 }
696 while (line_iter.next()) |line| {
697 defer idx += 1;
698
699 var iter = mem.tokenizeScalar(u8, line, ' ');
700 const size = iter.next() orelse return error.InvalidFormat;
701 const inode = iter.next() orelse return error.InvalidFormat;
702 const mtime_nsec_str = iter.next() orelse return error.InvalidFormat;
703 const digest_str = iter.next() orelse return error.InvalidFormat;
704 const prefix_str = iter.next() orelse return error.InvalidFormat;
705 const file_path = iter.rest();
706
707 const stat_size = fmt.parseInt(u64, size, 10) catch return error.InvalidFormat;
708 const stat_inode = fmt.parseInt(fs.File.INode, inode, 10) catch return error.InvalidFormat;
709 const stat_mtime = fmt.parseInt(i64, mtime_nsec_str, 10) catch return error.InvalidFormat;
710 const file_bin_digest = b: {
711 if (digest_str.len != hex_digest_len) return error.InvalidFormat;
712 var bd: BinDigest = undefined;
713 _ = fmt.hexToBytes(&bd, digest_str) catch return error.InvalidFormat;
714 break :b bd;
715 };
716
717 const prefix = fmt.parseInt(u8, prefix_str, 10) catch return error.InvalidFormat;
718 if (prefix >= self.cache.prefixes_len) return error.InvalidFormat;
719
720 if (file_path.len == 0) return error.InvalidFormat;
642721
643 file.stat = .{722 const cache_hash_file = f: {
723 const prefixed_path: PrefixedPath = .{
724 .prefix = prefix,
725 .sub_path = file_path, // expires with file_contents
726 };
727 if (idx < input_file_count) {
728 const file = &self.files.keys()[idx];
729 if (!file.prefixed_path.eql(prefixed_path))
730 return error.InvalidFormat;
731
732 file.stat = .{
733 .size = stat_size,
734 .inode = stat_inode,
735 .mtime = stat_mtime,
736 };
737 file.bin_digest = file_bin_digest;
738 break :f file;
739 }
740 const gop = try self.files.getOrPutAdapted(gpa, prefixed_path, FilesAdapter{});
741 errdefer _ = self.files.pop();
742 if (!gop.found_existing) {
743 gop.key_ptr.* = .{
744 .prefixed_path = .{
745 .prefix = prefix,
746 .sub_path = try gpa.dupe(u8, file_path),
747 },
748 .contents = null,
749 .max_file_size = null,
750 .handle = null,
751 .stat = .{
644 .size = stat_size,752 .size = stat_size,
645 .inode = stat_inode,753 .inode = stat_inode,
646 .mtime = stat_mtime,754 .mtime = stat_mtime,
647 };755 },
648 file.bin_digest = file_bin_digest;756 .bin_digest = file_bin_digest,
649 break :f file;757 };
650 }758 }
651 const gop = try self.files.getOrPutAdapted(gpa, prefixed_path, FilesAdapter{});759 break :f gop.key_ptr;
652 errdefer _ = self.files.pop();760 };
653 if (!gop.found_existing) {
654 gop.key_ptr.* = .{
655 .prefixed_path = .{
656 .prefix = prefix,
657 .sub_path = try gpa.dupe(u8, file_path),
658 },
659 .contents = null,
660 .max_file_size = null,
661 .handle = null,
662 .stat = .{
663 .size = stat_size,
664 .inode = stat_inode,
665 .mtime = stat_mtime,
666 },
667 .bin_digest = file_bin_digest,
668 };
669 }
670 break :f gop.key_ptr;
671 };
672
673 const pp = cache_hash_file.prefixed_path;
674 const dir = self.cache.prefixes()[pp.prefix].handle;
675 const this_file = dir.openFile(pp.sub_path, .{ .mode = .read_only }) catch |err| switch (err) {
676 error.FileNotFound => {
677 if (try self.upgradeToExclusiveLock()) continue;
678 return false;
679 },
680 else => |e| {
681 self.diagnostic = .{ .file_open = .{
682 .file_index = idx,
683 .err = e,
684 } };
685 return error.CacheCheckFailed;
686 },
687 };
688 defer this_file.close();
689761
690 const actual_stat = this_file.stat() catch |err| {762 const pp = cache_hash_file.prefixed_path;
691 self.diagnostic = .{ .file_stat = .{763 const dir = self.cache.prefixes()[pp.prefix].handle;
764 const this_file = dir.openFile(pp.sub_path, .{ .mode = .read_only }) catch |err| switch (err) {
765 error.FileNotFound => {
766 // Every digest before this one has been populated successfully.
767 return .{ .miss = .{ .file_digests_populated = idx } };
768 },
769 else => |e| {
770 self.diagnostic = .{ .file_open = .{
692 .file_index = idx,771 .file_index = idx,
693 .err = err,772 .err = e,
694 } };773 } };
695 return error.CacheCheckFailed;774 return error.CacheCheckFailed;
696 };775 },
697 const size_match = actual_stat.size == cache_hash_file.stat.size;776 };
698 const mtime_match = actual_stat.mtime == cache_hash_file.stat.mtime;777 defer this_file.close();
699 const inode_match = actual_stat.inode == cache_hash_file.stat.inode;
700
701 if (!size_match or !mtime_match or !inode_match) {
702 self.manifest_dirty = true;
703
704 cache_hash_file.stat = .{
705 .size = actual_stat.size,
706 .mtime = actual_stat.mtime,
707 .inode = actual_stat.inode,
708 };
709
710 if (self.isProblematicTimestamp(cache_hash_file.stat.mtime)) {
711 // The actual file has an unreliable timestamp, force it to be hashed
712 cache_hash_file.stat.mtime = 0;
713 cache_hash_file.stat.inode = 0;
714 }
715
716 var actual_digest: BinDigest = undefined;
717 hashFile(this_file, &actual_digest) catch |err| {
718 self.diagnostic = .{ .file_read = .{
719 .file_index = idx,
720 .err = err,
721 } };
722 return error.CacheCheckFailed;
723 };
724778
725 if (!mem.eql(u8, &cache_hash_file.bin_digest, &actual_digest)) {779 const actual_stat = this_file.stat() catch |err| {
726 cache_hash_file.bin_digest = actual_digest;780 self.diagnostic = .{ .file_stat = .{
727 // keep going until we have the input file digests781 .file_index = idx,
728 any_file_changed = true;782 .err = err,
729 }783 } };
730 }784 return error.CacheCheckFailed;
785 };
786 const size_match = actual_stat.size == cache_hash_file.stat.size;
787 const mtime_match = actual_stat.mtime == cache_hash_file.stat.mtime;
788 const inode_match = actual_stat.inode == cache_hash_file.stat.inode;
789
790 if (!size_match or !mtime_match or !inode_match) {
791 cache_hash_file.stat = .{
792 .size = actual_stat.size,
793 .mtime = actual_stat.mtime,
794 .inode = actual_stat.inode,
795 };
731796
732 if (!any_file_changed) {797 if (self.isProblematicTimestamp(cache_hash_file.stat.mtime)) {
733 self.hash.hasher.update(&cache_hash_file.bin_digest);798 // The actual file has an unreliable timestamp, force it to be hashed
799 cache_hash_file.stat.mtime = 0;
800 cache_hash_file.stat.inode = 0;
734 }801 }
735 }
736802
737 if (any_file_changed) {803 var actual_digest: BinDigest = undefined;
738 if (try self.upgradeToExclusiveLock()) continue;804 hashFile(this_file, &actual_digest) catch |err| {
739 // cache miss805 self.diagnostic = .{ .file_read = .{
740 // keep the manifest file open806 .file_index = idx,
741 self.unhit(bin_digest, input_file_count);807 .err = err,
742 return false;808 } };
743 }809 return error.CacheCheckFailed;
810 };
744811
745 if (idx < input_file_count) {812 if (!mem.eql(u8, &cache_hash_file.bin_digest, &actual_digest)) {
746 if (try self.upgradeToExclusiveLock()) continue;813 cache_hash_file.bin_digest = actual_digest;
747 self.manifest_dirty = true;814 // keep going until we have the input file digests
748 while (idx < input_file_count) : (idx += 1) {815 any_file_changed = true;
749 self.populateFileHash(&self.files.keys()[idx]) catch |err| {
750 self.diagnostic = .{ .file_hash = .{
751 .file_index = idx,
752 .err = err,
753 } };
754 return error.CacheCheckFailed;
755 };
756 }816 }
757 return false;
758 }817 }
759818
760 if (self.want_shared_lock) {819 if (!any_file_changed) {
761 self.downgradeToSharedLock() catch |err| {820 self.hash.hasher.update(&cache_hash_file.bin_digest);
762 self.diagnostic = .{ .manifest_lock = err };
763 return error.CacheCheckFailed;
764 };
765 }821 }
822 }
766823
767 return true;824 // If the manifest was somehow missing one of our input files, or if any file hash has changed,
825 // then this is a cache miss. However, we have successfully populated some or all of the file
826 // digests.
827 if (any_file_changed or idx < input_file_count) {
828 return .{ .miss = .{ .file_digests_populated = idx } };
768 }829 }
830
831 return .hit;
769 }832 }
770833
834 /// Reset `self.hash.hasher` to the state it should be in after `hit` returns `false`.
835 /// The hasher contains the original input digest, and all original input file digests (i.e.
836 /// not including post files).
837 /// Assumes that `bin_digest` is populated for all files up to `input_file_count`. As such,
838 /// this is not necessarily safe to call within `hit`.
771 pub fn unhit(self: *Manifest, bin_digest: BinDigest, input_file_count: usize) void {839 pub fn unhit(self: *Manifest, bin_digest: BinDigest, input_file_count: usize) void {
772 // Reset the hash.840 // Reset the hash.
773 self.hash.hasher = hasher_init;841 self.hash.hasher = hasher_init;
lib/std/Build/Step.zig+1-1
...@@ -759,7 +759,7 @@ fn failWithCacheError(s: *Step, man: *const Build.Cache.Manifest, err: Build.Cac...@@ -759,7 +759,7 @@ fn failWithCacheError(s: *Step, man: *const Build.Cache.Manifest, err: Build.Cac
759 switch (err) {759 switch (err) {
760 error.CacheCheckFailed => switch (man.diagnostic) {760 error.CacheCheckFailed => switch (man.diagnostic) {
761 .none => unreachable,761 .none => unreachable,
762 .manifest_create, .manifest_read, .manifest_lock => |e| return s.fail("failed to check cache: {s} {s}", .{762 .manifest_create, .manifest_read, .manifest_lock, .manifest_seek => |e| return s.fail("failed to check cache: {s} {s}", .{
763 @tagName(man.diagnostic), @errorName(e),763 @tagName(man.diagnostic), @errorName(e),
764 }),764 }),
765 .file_open, .file_stat, .file_read, .file_hash => |op| {765 .file_open, .file_stat, .file_read, .file_hash => |op| {
src/Compilation.zig+1-1
...@@ -2129,7 +2129,7 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) !void {...@@ -2129,7 +2129,7 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) !void {
2129 const is_hit = man.hit() catch |err| switch (err) {2129 const is_hit = man.hit() catch |err| switch (err) {
2130 error.CacheCheckFailed => switch (man.diagnostic) {2130 error.CacheCheckFailed => switch (man.diagnostic) {
2131 .none => unreachable,2131 .none => unreachable,
2132 .manifest_create, .manifest_read, .manifest_lock => |e| return comp.setMiscFailure(2132 .manifest_create, .manifest_read, .manifest_lock, .manifest_seek => |e| return comp.setMiscFailure(
2133 .check_whole_cache,2133 .check_whole_cache,
2134 "failed to check cache: {s} {s}",2134 "failed to check cache: {s} {s}",
2135 .{ @tagName(man.diagnostic), @errorName(e) },2135 .{ @tagName(man.diagnostic), @errorName(e) },