pub fn hitInner(self: *Manifest) HitError!bool {
assert(self.manifest_file == null);
self.diagnostic = .none;
const ext = ".txt";
var manifest_file_path: [hex_digest_len + ext.len]u8 = undefined;
var bin_digest: BinDigest = undefined;
self.hash.hasher.final(&bin_digest);
self.hex_digest = binToHex(bin_digest);
@memcpy(manifest_file_path[0..self.hex_digest.len], &self.hex_digest);
manifest_file_path[hex_digest_len..][0..ext.len].* = ext.*;
const io = self.cache.io;
// We'll try to open the cache with an exclusive lock, but if that would block
// and `want_shared_lock` is set, a shared lock might be sufficient, so we'll
// open with a shared lock instead.
while (true) {
if (self.cache.manifest_dir.createFile(io, &manifest_file_path, .{
.read = true,
.truncate = false,
.lock = .exclusive,
.lock_nonblocking = self.want_shared_lock,
})) |manifest_file| {
self.manifest_file = manifest_file;
self.have_exclusive_lock = true;
break;
} else |err| switch (err) {
error.WouldBlock => {
self.manifest_file = self.cache.manifest_dir.openFile(io, &manifest_file_path, .{
.mode = .read_write,
.lock = .shared,
}) catch |e| {
self.diagnostic = .{ .manifest_create = e };
return error.CacheCheckFailed;
};
break;
},
error.FileNotFound => {
// There are no dir components, so the only possibility
// should be that the directory behind the handle has been
// deleted, however we have observed on macOS two processes
// racing to do openat() with O_CREAT manifest in ENOENT.
//
// As a workaround, we retry with exclusive=true which
// disambiguates by returning EEXIST, indicating original
// failure was a race, or ENOENT, indicating deletion of
// the directory of our open handle.
if (!builtin.os.tag.isDarwin()) {
self.diagnostic = .{ .manifest_create = error.FileNotFound };
return error.CacheCheckFailed;
}
if (self.cache.manifest_dir.createFile(io, &manifest_file_path, .{
.read = true,
.truncate = false,
.lock = .exclusive,
.lock_nonblocking = self.want_shared_lock,
.exclusive = true,
})) |manifest_file| {
self.manifest_file = manifest_file;
self.have_exclusive_lock = true;
break;
} else |excl_err| switch (excl_err) {
error.WouldBlock, error.PathAlreadyExists => continue,
error.FileNotFound => {
self.diagnostic = .{ .manifest_create = error.FileNotFound };
return error.CacheCheckFailed;
},
error.Canceled => |e| return e,
else => |e| {
self.diagnostic = .{ .manifest_create = e };
return error.CacheCheckFailed;
},
}
},
error.Canceled => |e| return e,
else => |e| {
self.diagnostic = .{ .manifest_create = e };
return error.CacheCheckFailed;
},
}
}
self.want_refresh_timestamp = true;
const input_file_count = self.files.entries.len;
// We're going to construct a second hash. Its input will begin with the digest we've
// already computed (`bin_digest`), and then it'll have the digests of each input file,
// including "post" files (see `addFilePost`). If this is a hit, we learn the set of "post"
// files from the manifest on disk. If this is a miss, we'll learn those from future calls
// to `addFilePost` etc. As such, the state of `self.hash.hasher` after this function
// depends on whether this is a hit or a miss.
//
// If we return `true` indicating a cache hit, then `self.hash.hasher` must already include
// the digests of the "post" files, so the caller can call `final`. Otherwise, on a cache
// miss, `self.hash.hasher` will include the digests of all non-"post" files -- that is,
// the ones we've already been told about. The rest will be discovered through calls to
// `addFilePost` etc, which will update the hasher. After all files are added, the user can
// use `final`, and will at some point `writeManifest` the file list to disk.
self.hash.hasher = hasher_init;
self.hash.hasher.update(&bin_digest);
hit: {
const file_digests_populated: usize = digests: {
switch (try self.hitWithCurrentLock()) {
.hit => break :hit,
.miss => |m| if (!try self.upgradeToExclusiveLock()) {
break :digests m.file_digests_populated;
},
}
// We've just had a miss with the shared lock, and upgraded to an exclusive lock. Someone
// else might have modified the digest, so we need to check again before deciding to miss.
// Before trying again, we must reset `self.hash.hasher` and `self.files`.
// This is basically just the first half of `unhit`.
self.hash.hasher = hasher_init;
self.hash.hasher.update(&bin_digest);
while (self.files.count() != input_file_count) {
var file = self.files.pop().?;
file.key.deinit(self.cache.gpa);
}
switch (try self.hitWithCurrentLock()) {
.hit => break :hit,
.miss => |m| break :digests m.file_digests_populated,
}
};
// This is a guaranteed cache miss. We're almost ready to return `false`, but there's a
// little bookkeeping to do first. The first `file_digests_populated` entries in `files`
// have their `bin_digest` populated; there may be some left in `input_file_count` which
// we'll need to populate ourselves. Other than that, this is basically `unhit`.
self.manifest_dirty = true;
self.hash.hasher = hasher_init;
self.hash.hasher.update(&bin_digest);
while (self.files.count() != input_file_count) {
var file = self.files.pop().?;
file.key.deinit(self.cache.gpa);
}
for (self.files.keys(), 0..) |*file, idx| {
if (idx < file_digests_populated) {
// `bin_digest` is already populated by `hitWithCurrentLock`, so we can use it directly.
self.hash.hasher.update(&file.bin_digest);
} else {
self.populateFileHash(file) catch |err| {
self.diagnostic = .{ .file_hash = .{
.file_index = idx,
.err = err,
} };
return error.CacheCheckFailed;
};
}
}
return false;
}
if (self.want_shared_lock) {
self.downgradeToSharedLock() catch |err| {
self.diagnostic = .{ .manifest_lock = err };
return error.CacheCheckFailed;
};
}
return true;
}