blob: 6f275d106d64076bb42ac8d61188466b5809fd78 [file]
use std::sync::{Arc, LazyLock, OnceLock};
pub use jobserver_crate::Acquired;
use jobserver_crate::{Client, FromEnv, FromEnvErrorKind, HelperThread};
use parking_lot::{Condvar, Mutex};
// We stick the jobserver client into a global and initialize it once, because there could be
// multiple compiler instances in this process, and the jobserver is per-process.
static GLOBAL_CLIENT: LazyLock<Result<Client, String>> = LazyLock::new(|| {
// Safety: the checked client construction ensures that the jobserver file descriptors
// (if any) are open and valid. We also try to initialize the jobserver as early as possible
// to avoid unrelated file descriptors with matching values becoming open and valid between
// the process start and the jobserver initialization.
let FromEnv { client, var } = unsafe { Client::from_env_ext(true) };
let error = match client {
Ok(client) => return Ok(client),
Err(e) => e,
};
if matches!(
error.kind(),
FromEnvErrorKind::NoEnvVar
| FromEnvErrorKind::NoJobserver
| FromEnvErrorKind::NegativeFd
| FromEnvErrorKind::Unsupported
) {
return Ok(default_client());
}
// Environment specifies jobserver, but it looks incorrect.
// Safety: `error.kind()` should be `NoEnvVar` if `var == None`.
let (name, value) = var.unwrap();
Err(format!(
"failed to connect to jobserver from environment variable `{name}={:?}`: {error}",
value
))
});
// Creates a new jobserver if there's no inherited one.
fn default_client() -> Client {
// Pick a "reasonable maximum" capping out at 32
// so we don't take everything down by hogging the process run queue.
// The fixed number is used to have deterministic compilation across machines.
let client = Client::new(32).expect("failed to create jobserver");
// Acquire the single token that is always held by the rustc process.
// This is an equivalent of the single token held by a higher level build tool while running
// this instance of rustc. This token is never released - if we are here, then rustc owns the
// jobserver, it is teared down when rustc exits, and there's no one to return the token to.
client.acquire_raw().ok();
client
}
static GLOBAL_CLIENT_CHECKED: OnceLock<Client> = OnceLock::new();
/// Initializes a jobserver client for the current rustc process.
/// If inheriting jobserver from the environment fails for some reason, an new jobserver owned by
/// the current rustc process will be created. If the inheritance failure reason is non-benign,
/// the passed callback will be used to report the error.
pub fn initialize_checked(report: impl FnOnce(&'static str)) {
let client_checked = match &*GLOBAL_CLIENT {
Ok(client) => client.clone(),
Err(e) => {
report(e);
default_client()
}
};
GLOBAL_CLIENT_CHECKED.set(client_checked).ok();
}
/// Returns the jobserver client previously initialized by `initialize_checked`.
///
/// # Assumptions about holding jobserver tokens
///
/// Rustc process must always hold a single token to avoid being permanently starved and blocked.
/// - If the jobserver is inherited from a higher level build tool, the assumption is that the tool
/// will hold the token and not release it until the rustc process exits.
/// - If the jobserver is owned by the current rustc, the token is acquired by `default_client`.
///
/// To avoid releasing the last token, users of the client returned by this function must ensure
/// that they never release more tokens than was previously explicitly acquired.
/// Example of a sequence that can accidentally release the last token:
/// `release_raw` -> `wait` -> `acquire_raw`.
/// To avoid situations like this use the `jobserver::Proxy` wrapper instead,
/// it will ensure that the last token is never released.
pub fn client() -> Client {
GLOBAL_CLIENT_CHECKED.get().expect("uninitialized jobserver client").clone()
}
struct ProxyData {
/// The number of tokens assigned to actively working threads,
/// possibly including the single permanently held token.
/// If this number is 0, the single token is still held by the process,
/// but is not currently used for active CPU work.
/// This can happen, for example, if the main thread is waiting for something,
/// in that case some other thread can start using this token to do work.
used: u16,
/// The number of threads currently requesting a token and waiting.
/// If the proxy releases a token it can immediately give it to one of these threads
/// without going through the real jobserver.
pending: u16,
}
/// A wrapper around jobserver client used for two purposes:
/// - Ensuring that the single token that must be permanently held by the rustc process
/// cannot be accidentally released.
/// - "Token buffering", immediately acquiring freshly released tokens if necessary,
/// without going through the real jobserver.
pub struct Proxy {
/// The wrapped jobserver client.
client: Client,
/// Helper thread associated with the wrapped client.
helper: OnceLock<HelperThread>,
/// The proxy's own data.
data: Mutex<ProxyData>,
/// Threads which are currently waiting for a token will wait on this condvar.
wake_pending: Condvar,
}
impl Proxy {
pub fn new() -> Arc<Self> {
let proxy = Arc::new(Proxy {
client: client(),
// Assume that the main thread is actively doing work when it creates the proxy.
data: Mutex::new(ProxyData { used: 1, pending: 0 }),
wake_pending: Condvar::new(),
helper: OnceLock::new(),
});
let proxy_ = Arc::clone(&proxy);
let helper = proxy
.client
.clone()
.into_helper_thread(move |token| {
// Reminder: this callback runs when the helper acquires a token.
if let Ok(token) = token {
let mut data = proxy_.data.lock();
if data.pending > 0 {
// The token is still needed, give it to one of the waiting threads.
token.drop_without_releasing();
assert!(data.used > 0);
data.used += 1;
data.pending -= 1;
proxy_.wake_pending.notify_one();
} else {
// The token is no longer needed, release it by dropping.
drop(data);
drop(token);
}
}
})
.expect("failed to spawn helper thread");
proxy.helper.set(helper).unwrap();
proxy
}
/// Acquires a token, possibly using some buffered tokens as an optimization.
/// May block and wait until the token is available.
pub fn acquire_thread(&self) {
let mut data = self.data.lock();
if data.used == 0 {
// No threads are doing any active work, but we are still holding the last token.
// Give that token to the current thread.
assert_eq!(data.pending, 0);
data.used += 1;
} else {
// Request a token from the helper thread, this is a non-blocking operation.
// Then wait until this or some other request succeeds.
self.helper.get().unwrap().request_token();
data.pending += 1;
self.wake_pending.wait(&mut data);
}
}
/// Releases a token, possibly immediately giving it to some other thread as an optimization.
/// Makes sure that the last token is never actually released to the wrapped jobserver.
pub fn release_thread(&self) {
let mut data = self.data.lock();
if data.pending > 0 {
// Immediately give the released token to one of the waiting threads.
data.pending -= 1;
self.wake_pending.notify_one();
} else {
data.used -= 1;
// Release the token to the wrapped jobserver, unless it's the last one in the process.
if data.used > 0 {
drop(data);
self.client.release_raw().ok();
}
}
}
}