Skip to content

Fix Build::compile_objects deadlock on parallel #829

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 20, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 29 additions & 19 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1258,15 +1258,16 @@ impl Build {

#[cfg(feature = "parallel")]
fn compile_objects(&self, objs: &[Object], print: &PrintThread) -> Result<(), Error> {
use std::sync::Once;
use std::sync::{mpsc::channel, Once};

// Limit our parallelism globally with a jobserver. Start off by
// releasing our own token for this process so we can have a bit of an
// easier to write loop below. If this fails, though, then we're likely
// on Windows with the main implicit token, so we just have a bit extra
// parallelism for a bit and don't reacquire later.
let server = jobserver();
let reacquire = server.release_raw().is_ok();
// Reacquire our process's token on drop
let _reacquire = server.release_raw().ok().map(|_| JobserverToken(server));

// When compiling objects in parallel we do a few dirty tricks to speed
// things up:
@@ -1287,29 +1288,31 @@ impl Build {
// acquire the appropriate tokens, Once all objects have been compiled
// we wait on all the processes and propagate the results of compilation.

let children = objs
.iter()
.map(|obj| {
let (mut cmd, program) = self.create_compile_object_cmd(obj)?;
let token = server.acquire()?;
let (tx, rx) = channel::<(_, String, KillOnDrop, _)>();

let child = spawn(&mut cmd, &program, print.pipe_writer_cloned()?.unwrap())?;
// Since jobserver::Client::acquire can block, waiting
// must be done in parallel so that acquire won't block forever.
let wait_thread = thread::Builder::new().spawn(move || {
for (cmd, program, mut child, _token) in rx {
wait_on_child(&cmd, &program, &mut child.0)?;
}

Ok((cmd, program, KillOnDrop(child), token))
})
.collect::<Result<Vec<_>, Error>>()?;
Ok(())
})?;

for (cmd, program, mut child, _token) in children {
wait_on_child(&cmd, &program, &mut child.0)?;
}
for obj in objs {
let (mut cmd, program) = self.create_compile_object_cmd(obj)?;
let token = server.acquire()?;

// Reacquire our process's token before we proceed, which we released
// before entering the loop above.
if reacquire {
server.acquire_raw()?;
let child = spawn(&mut cmd, &program, print.pipe_writer_cloned()?.unwrap())?;

if tx.send((cmd, program, KillOnDrop(child), token)).is_err() {
break;
}
}
drop(tx);

return Ok(());
return wait_thread.join().expect("wait_thread panics");

/// Returns a suitable `jobserver::Client` used to coordinate
/// parallelism between build scripts.
@@ -1365,6 +1368,13 @@ impl Build {
child.kill().ok();
}
}

struct JobserverToken(&'static jobserver::Client);
impl Drop for JobserverToken {
fn drop(&mut self) {
let _ = self.0.acquire_raw();
}
}
}

#[cfg(not(feature = "parallel"))]