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 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(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 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 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}