Skip to main content

rustc_data_structures/
jobserver.rs

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