From 44007a5cfd05fc8415b4b0e1a1a4629f2fd191c1 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 3 Sep 2026 20:12:52 -0700 Subject: [PATCH] std.Build.Cache: fix deadlock from using Io.Select Io.Select.async has a fatal flaw: if the queue size isn't big enough to hold the result of an eager async call, then it deadlocks. The workaround in this thread is using concurrent() which should be reverted because it will spawn too many threads. --- lib/std/Build/Cache.zig | 43 +++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/lib/std/Build/Cache.zig b/lib/std/Build/Cache.zig index 84c7fb5a4e1718993a09af3bbec65654d27a3661..16d2e3a1c5c46061592f95fb121b63f446270b76 100644 --- a/lib/std/Build/Cache.zig +++ b/lib/std/Build/Cache.zig @@ -795,10 +795,12 @@ pub const Manifest = struct { fn shrinkFilesToInput(m: *Manifest) void { if (m.files.count() <= m.input_paths.items.len) return; + // Reads from files hash map whose data is destroyed on the next line. const off = m.files.keys()[m.input_paths.items.len]; - m.contents.shrinkRetainingCapacity(@backingInt(off)); - assert(mem.isAligned(m.contents.items.len, @alignOf(File))); + // Reads from the unshrunken contents whose data is destroyed on the next line. m.files.shrinkRetainingCapacityContext(m.input_paths.items.len, .{ .contents = m.contents.items }); + m.contents.shrinkRetainingCapacity(@backingInt(off)); + assert(mem.isAligned(m.contents.items.len, @alignOf(File))); } /// Assumes that `self.hash.hasher` has been updated only with the original digest and that @@ -852,7 +854,7 @@ pub const Manifest = struct { const PostResult = union(enum) { checkFile: CheckFileError!Check.Status, }; - var post_select_buffer: [10]PostResult = undefined; + var post_select_buffer: [16]PostResult = undefined; var post_select: Io.Select(PostResult) = .init(io, &post_select_buffer); var post_select_remaining: usize = 0; defer post_select.cancelDiscard(); @@ -866,8 +868,33 @@ pub const Manifest = struct { try m.files.putContext(gpa, file_off, {}, .{ .contents = contents }); - post_select.async(.checkFile, checkFile, .{ m, &c, file_off, path }); - post_select_remaining += 1; + // In order to call async here we would need to ensure `post_select_buffer` + // has capacity for as many elements as files being checked. Since we don't want + // to dynamically allocate that buffer, we use concurrent + fallback here. + if (post_select.concurrent(.checkFile, checkFile, .{ m, &c, file_off, path })) |_| { + post_select_remaining += 1; + } else |err| switch (err) { + error.ConcurrencyUnavailable => { + // Detect if input group already had a miss. In this case we still wait + // for those digests to be updated, but cancel the non input group. + switch (@atomicLoad(Check.Status, &c.status, .unordered)) { + .miss => { + post_select.cancelDiscard(); + try input_group.await(io); + return .miss; + }, + .hit => {}, + } + switch (try checkFile(m, &c, file_off, path)) { + .hit => continue, + .miss => { + post_select.cancelDiscard(); + try input_group.await(io); + return .miss; + }, + } + }, + } off += File.sizeOf(path.len); } @@ -883,7 +910,7 @@ pub const Manifest = struct { // Don't track the trailing zero byte in contents. m.contents.items.len -= 1; - var post_await_buffer: [10]PostResult = undefined; + var post_await_buffer: [16]PostResult = undefined; while (post_select_remaining > 0) { const n = try post_select.awaitMany(&post_await_buffer, 1); post_select_remaining -= n; @@ -896,11 +923,11 @@ pub const Manifest = struct { try input_group.await(io); return .miss; }, - .hit => continue, + .hit => {}, } for (post_await_buffer[0..n]) |u| switch (u) { - .checkFile => |result| switch (result) { + .checkFile => |result| switch (try result) { .hit => continue, .miss => { post_select.cancelDiscard(); -- 2.54.0