Skip to main content

rustc_data_structures/
jobserver.rs

1use std::sync::{Arc, LazyLock, OnceLock};
2
3pub use jobserver_crate::Acquired;
4use jobserver_crate::{Client, FromEnv, FromEnvErrorKind, HelperThread};
5use parking_lot::{Condvar, Mutex};
6
7// We stick the jobserver client into a global and initialize it once, because there could be
8// multiple compiler instances in this process, and the jobserver is per-process.
9static GLOBAL_CLIENT: LazyLock<Result<Client, String>> = LazyLock::new(|| {
10    // Safety: the checked client construction ensures that the jobserver file descriptors
11    // (if any) are open and valid. We also try to initialize the jobserver as early as possible
12    // to avoid unrelated file descriptors with matching values becoming open and valid between
13    // the process start and the jobserver initialization.
14    let FromEnv { client, var } = unsafe { Client::from_env_ext(true) };
15
16    let error = match client {
17        Ok(client) => return Ok(client),
18        Err(e) => e,
19    };
20
21    if #[allow(non_exhaustive_omitted_patterns)] match error.kind() {
    FromEnvErrorKind::NoEnvVar | FromEnvErrorKind::NoJobserver |
        FromEnvErrorKind::NegativeFd | FromEnvErrorKind::Unsupported => true,
    _ => false,
}matches!(
22        error.kind(),
23        FromEnvErrorKind::NoEnvVar
24            | FromEnvErrorKind::NoJobserver
25            | FromEnvErrorKind::NegativeFd
26            | FromEnvErrorKind::Unsupported
27    ) {
28        return Ok(default_client());
29    }
30
31    // Environment specifies jobserver, but it looks incorrect.
32    // Safety: `error.kind()` should be `NoEnvVar` if `var == None`.
33    let (name, value) = var.unwrap();
34    Err(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("failed to connect to jobserver from environment variable `{1}={0:?}`: {2}",
                value, name, error))
    })format!(
35        "failed to connect to jobserver from environment variable `{name}={:?}`: {error}",
36        value
37    ))
38});
39
40// Creates a new jobserver if there's no inherited one.
41fn default_client() -> Client {
42    // Pick a "reasonable maximum" capping out at 32
43    // so we don't take everything down by hogging the process run queue.
44    // The fixed number is used to have deterministic compilation across machines.
45    let client = Client::new(32).expect("failed to create jobserver");
46
47    // Acquire the single token that is always held by the rustc process.
48    // This is an equivalent of the single token held by a higher level build tool while running
49    // this instance of rustc. This token is never released - if we are here, then rustc owns the
50    // jobserver, it is teared down when rustc exits, and there's no one to return the token to.
51    client.acquire_raw().ok();
52
53    client
54}
55
56static GLOBAL_CLIENT_CHECKED: OnceLock<Client> = OnceLock::new();
57
58/// Initializes a jobserver client for the current rustc process.
59/// If inheriting jobserver from the environment fails for some reason, an new jobserver owned by
60/// the current rustc process will be created. If the inheritance failure reason is non-benign,
61/// the passed callback will be used to report the error.
62pub fn initialize_checked(report: impl FnOnce(&'static str)) {
63    let client_checked = match &*GLOBAL_CLIENT {
64        Ok(client) => client.clone(),
65        Err(e) => {
66            report(e);
67            default_client()
68        }
69    };
70    GLOBAL_CLIENT_CHECKED.set(client_checked).ok();
71}
72
73/// Returns the jobserver client previously initialized by `initialize_checked`.
74///
75/// # Assumptions about holding jobserver tokens
76///
77/// Rustc process must always hold a single token to avoid being permanently starved and blocked.
78/// - If the jobserver is inherited from a higher level build tool, the assumption is that the tool
79///   will hold the token and not release it until the rustc process exits.
80/// - If the jobserver is owned by the current rustc, the token is acquired by `default_client`.
81///
82/// To avoid releasing the last token, users of the client returned by this function must ensure
83/// that they never release more tokens than was previously explicitly acquired.
84/// Example of a sequence that can accidentally release the last token:
85/// `release_raw` -> `wait` -> `acquire_raw`.
86/// To avoid situations like this use the `jobserver::Proxy` wrapper instead,
87/// it will ensure that the last token is never released.
88pub fn client() -> Client {
89    GLOBAL_CLIENT_CHECKED.get().expect("uninitialized jobserver client").clone()
90}
91
92struct ProxyData {
93    /// The number of tokens assigned to actively working threads,
94    /// possibly including the single permanently held token.
95    /// If this number is 0, the single token is still held by the process,
96    /// but is not currently used for active CPU work.
97    /// This can happen, for example, if the main thread is waiting for something,
98    /// in that case some other thread can start using this token to do work.
99    used: u16,
100    /// The number of threads currently requesting a token and waiting.
101    /// If the proxy releases a token it can immediately give it to one of these threads
102    /// without going through the real jobserver.
103    pending: u16,
104}
105
106/// A wrapper around jobserver client used for two purposes:
107/// - Ensuring that the single token that must be permanently held by the rustc process
108///   cannot be accidentally released.
109/// - "Token buffering", immediately acquiring freshly released tokens if necessary,
110///   without going through the real jobserver.
111pub struct Proxy {
112    /// The wrapped jobserver client.
113    client: Client,
114    /// Helper thread associated with the wrapped client.
115    helper: OnceLock<HelperThread>,
116    /// The proxy's own data.
117    data: Mutex<ProxyData>,
118    /// Threads which are currently waiting for a token will wait on this condvar.
119    wake_pending: Condvar,
120}
121
122impl Proxy {
123    pub fn new() -> Arc<Self> {
124        let proxy = Arc::new(Proxy {
125            client: client(),
126            // Assume that the main thread is actively doing work when it creates the proxy.
127            data: Mutex::new(ProxyData { used: 1, pending: 0 }),
128            wake_pending: Condvar::new(),
129            helper: OnceLock::new(),
130        });
131        let proxy_ = Arc::clone(&proxy);
132        let helper = proxy
133            .client
134            .clone()
135            .into_helper_thread(move |token| {
136                // Reminder: this callback runs when the helper acquires a token.
137                if let Ok(token) = token {
138                    let mut data = proxy_.data.lock();
139                    if data.pending > 0 {
140                        // The token is still needed, give it to one of the waiting threads.
141                        token.drop_without_releasing();
142                        if !(data.used > 0) {
    ::core::panicking::panic("assertion failed: data.used > 0")
};assert!(data.used > 0);
143                        data.used += 1;
144                        data.pending -= 1;
145                        proxy_.wake_pending.notify_one();
146                    } else {
147                        // The token is no longer needed, release it by dropping.
148                        drop(data);
149                        drop(token);
150                    }
151                }
152            })
153            .expect("failed to spawn helper thread");
154        proxy.helper.set(helper).unwrap();
155        proxy
156    }
157
158    /// Acquires a token, possibly using some buffered tokens as an optimization.
159    /// May block and wait until the token is available.
160    pub fn acquire_thread(&self) {
161        let mut data = self.data.lock();
162
163        if data.used == 0 {
164            // No threads are doing any active work, but we are still holding the last token.
165            // Give that token to the current thread.
166            {
    match (&data.pending, &0) {
        (left_val, right_val) => {
            if !(*left_val == *right_val) {
                let kind = ::core::panicking::AssertKind::Eq;
                ::core::panicking::assert_failed(kind, &*left_val,
                    &*right_val, ::core::option::Option::None);
            }
        }
    }
};assert_eq!(data.pending, 0);
167            data.used += 1;
168        } else {
169            // Request a token from the helper thread, this is a non-blocking operation.
170            // Then wait until this or some other request succeeds.
171            self.helper.get().unwrap().request_token();
172            data.pending += 1;
173            self.wake_pending.wait(&mut data);
174        }
175    }
176
177    /// Releases a token, possibly immediately giving it to some other thread as an optimization.
178    /// Makes sure that the last token is never actually released to the wrapped jobserver.
179    pub fn release_thread(&self) {
180        let mut data = self.data.lock();
181
182        if data.pending > 0 {
183            // Immediately give the released token to one of the waiting threads.
184            data.pending -= 1;
185            self.wake_pending.notify_one();
186        } else {
187            data.used -= 1;
188
189            // Release the token to the wrapped jobserver, unless it's the last one in the process.
190            if data.used > 0 {
191                drop(data);
192                self.client.release_raw().ok();
193            }
194        }
195    }
196}