stage2: Cache system handles shared objects

Fixes #9139
Fixes #9187
This commit is contained in:
Andrew Kelley 2021-06-27 22:33:17 -07:00
parent 6ba6b98b72
commit 4e61af404e

View File

@ -181,6 +181,12 @@ pub const Manifest = struct {
hash: HashHelper, hash: HashHelper,
manifest_file: ?fs.File, manifest_file: ?fs.File,
manifest_dirty: bool, manifest_dirty: bool,
/// Set this flag to true before calling hit() in order to indicate that
/// upon a cache hit, the code using the cache will not modify the files
/// within the cache directory. This allows multiple processes to utilize
/// the same cache directory at the same time.
want_shared_lock: bool = true,
have_exclusive_lock: bool = false,
files: std.ArrayListUnmanaged(File) = .{}, files: std.ArrayListUnmanaged(File) = .{},
hex_digest: [hex_digest_len]u8, hex_digest: [hex_digest_len]u8,
/// Populated when hit() returns an error because of one /// Populated when hit() returns an error because of one
@ -257,7 +263,9 @@ pub const Manifest = struct {
/// ///
/// This function will also acquire an exclusive lock to the manifest file. This means /// This function will also acquire an exclusive lock to the manifest file. This means
/// that a process holding a Manifest will block any other process attempting to /// that a process holding a Manifest will block any other process attempting to
/// acquire the lock. /// acquire the lock. If `want_shared_lock` is `true`, a cache hit guarantees the
/// manifest file to be locked in shared mode, and a cache miss guarantees the manifest
/// file to be locked in exclusive mode.
/// ///
/// The lock on the manifest file is released when `deinit` is called. As another /// The lock on the manifest file is released when `deinit` is called. As another
/// option, one may call `toOwnedLock` to obtain a smaller object which can represent /// option, one may call `toOwnedLock` to obtain a smaller object which can represent
@ -285,31 +293,62 @@ pub const Manifest = struct {
mem.copy(u8, &manifest_file_path, &self.hex_digest); mem.copy(u8, &manifest_file_path, &self.hex_digest);
manifest_file_path[self.hex_digest.len..][0..ext.len].* = ext.*; manifest_file_path[self.hex_digest.len..][0..ext.len].* = ext.*;
if (self.files.items.len != 0) { if (self.files.items.len == 0) {
self.manifest_file = try self.cache.manifest_dir.createFile(&manifest_file_path, .{
.read = true,
.truncate = false,
.lock = .Exclusive,
});
} else {
// If there are no file inputs, we check if the manifest file exists instead of // If there are no file inputs, we check if the manifest file exists instead of
// comparing the hashes on the files used for the cached item // comparing the hashes on the files used for the cached item
self.manifest_file = self.cache.manifest_dir.openFile(&manifest_file_path, .{ while (true) {
if (self.cache.manifest_dir.openFile(&manifest_file_path, .{
.read = true, .read = true,
.write = true, .write = true,
.lock = .Exclusive, .lock = .Exclusive,
}) catch |err| switch (err) { .lock_nonblocking = self.want_shared_lock,
})) |manifest_file| {
self.manifest_file = manifest_file;
self.have_exclusive_lock = true;
break;
} else |open_err| switch (open_err) {
error.WouldBlock => {
self.manifest_file = try self.cache.manifest_dir.openFile(&manifest_file_path, .{
.lock = .Shared,
});
break;
},
error.FileNotFound => { error.FileNotFound => {
self.manifest_dirty = true; if (self.cache.manifest_dir.createFile(&manifest_file_path, .{
self.manifest_file = try self.cache.manifest_dir.createFile(&manifest_file_path, .{
.read = true, .read = true,
.truncate = false, .truncate = false,
.lock = .Exclusive, .lock = .Exclusive,
}); .lock_nonblocking = self.want_shared_lock,
return false; })) |manifest_file| {
self.manifest_file = manifest_file;
self.manifest_dirty = true;
self.have_exclusive_lock = true;
return false; // cache miss; exclusive lock already held
} else |err| switch (err) {
error.WouldBlock => continue,
else => |e| return e,
}
}, },
else => |e| return e, else => |e| return e,
}; }
}
} else {
if (self.cache.manifest_dir.createFile(&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;
} else |err| switch (err) {
error.WouldBlock => {
self.manifest_file = try self.cache.manifest_dir.openFile(&manifest_file_path, .{
.lock = .Shared,
});
},
else => |e| return e,
}
} }
const file_contents = try self.manifest_file.?.reader().readAllAlloc(self.cache.gpa, manifest_file_size_max); const file_contents = try self.manifest_file.?.reader().readAllAlloc(self.cache.gpa, manifest_file_size_max);
@ -360,7 +399,10 @@ pub const Manifest = struct {
} }
const this_file = fs.cwd().openFile(cache_hash_file.path.?, .{ .read = true }) catch |err| switch (err) { const this_file = fs.cwd().openFile(cache_hash_file.path.?, .{ .read = true }) catch |err| switch (err) {
error.FileNotFound => return false, error.FileNotFound => {
try self.upgradeToExclusiveLock();
return false;
},
else => return error.CacheUnavailable, else => return error.CacheUnavailable,
}; };
defer this_file.close(); defer this_file.close();
@ -405,6 +447,7 @@ pub const Manifest = struct {
// cache miss // cache miss
// keep the manifest file open // keep the manifest file open
self.unhit(bin_digest, input_file_count); self.unhit(bin_digest, input_file_count);
try self.upgradeToExclusiveLock();
return false; return false;
} }
@ -417,9 +460,11 @@ pub const Manifest = struct {
return err; return err;
}; };
} }
try self.upgradeToExclusiveLock();
return false; return false;
} }
try self.downgradeToSharedLock();
return true; return true;
} }
@ -585,9 +630,12 @@ pub const Manifest = struct {
return out_digest; return out_digest;
} }
/// If `want_shared_lock` is true, this function automatically downgrades the
/// lock from exclusive to shared.
pub fn writeManifest(self: *Manifest) !void { pub fn writeManifest(self: *Manifest) !void {
const manifest_file = self.manifest_file.?; const manifest_file = self.manifest_file.?;
if (!self.manifest_dirty) return; if (self.manifest_dirty) {
self.manifest_dirty = false;
var contents = std.ArrayList(u8).init(self.cache.gpa); var contents = std.ArrayList(u8).init(self.cache.gpa);
defer contents.deinit(); defer contents.deinit();
@ -612,7 +660,26 @@ pub const Manifest = struct {
try manifest_file.setEndPos(contents.items.len); try manifest_file.setEndPos(contents.items.len);
try manifest_file.pwriteAll(contents.items, 0); try manifest_file.pwriteAll(contents.items, 0);
self.manifest_dirty = false; }
if (self.want_shared_lock) {
try self.downgradeToSharedLock();
}
}
fn downgradeToSharedLock(self: *Manifest) !void {
if (!self.have_exclusive_lock) return;
const manifest_file = self.manifest_file.?;
try manifest_file.setLock(.Shared, false);
self.have_exclusive_lock = false;
}
fn upgradeToExclusiveLock(self: *Manifest) !void {
if (self.have_exclusive_lock) return;
const manifest_file = self.manifest_file.?;
try manifest_file.setLock(.None, false);
try manifest_file.setLock(.Exclusive, false);
self.have_exclusive_lock = true;
} }
/// Obtain only the data needed to maintain a lock on the manifest file. /// Obtain only the data needed to maintain a lock on the manifest file.
@ -881,27 +948,27 @@ test "no file inputs" {
defer cache.manifest_dir.close(); defer cache.manifest_dir.close();
{ {
var ch = cache.obtain(); var man = cache.obtain();
defer ch.deinit(); defer man.deinit();
ch.hash.addBytes("1234"); man.hash.addBytes("1234");
// There should be nothing in the cache // There should be nothing in the cache
try testing.expectEqual(false, try ch.hit()); try testing.expectEqual(false, try man.hit());
digest1 = ch.final(); digest1 = man.final();
try ch.writeManifest(); try man.writeManifest();
} }
{ {
var ch = cache.obtain(); var man = cache.obtain();
defer ch.deinit(); defer man.deinit();
ch.hash.addBytes("1234"); man.hash.addBytes("1234");
try testing.expect(try ch.hit()); try testing.expect(try man.hit());
digest2 = ch.final(); digest2 = man.final();
try ch.writeManifest(); try man.writeManifest();
} }
try testing.expectEqual(digest1, digest2); try testing.expectEqual(digest1, digest2);