Add a jobserver proxy to ensure at least one token is always held
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
use std::sync::{LazyLock, OnceLock};
|
||||
use std::sync::{Arc, LazyLock, OnceLock};
|
||||
|
||||
pub use jobserver_crate::{Acquired, Client, HelperThread};
|
||||
use jobserver_crate::{FromEnv, FromEnvErrorKind};
|
||||
use parking_lot::{Condvar, Mutex};
|
||||
|
||||
// We can only call `from_env_ext` once per process
|
||||
|
||||
@@ -71,10 +72,93 @@ pub fn client() -> Client {
|
||||
GLOBAL_CLIENT_CHECKED.get().expect(ACCESS_ERROR).clone()
|
||||
}
|
||||
|
||||
pub fn acquire_thread() {
|
||||
GLOBAL_CLIENT_CHECKED.get().expect(ACCESS_ERROR).acquire_raw().ok();
|
||||
struct ProxyData {
|
||||
/// The number of tokens assigned to threads.
|
||||
/// If this is 0, a single token is still assigned to this process, but is unused.
|
||||
used: u16,
|
||||
|
||||
/// The number of threads requesting a token
|
||||
pending: u16,
|
||||
}
|
||||
|
||||
pub fn release_thread() {
|
||||
GLOBAL_CLIENT_CHECKED.get().expect(ACCESS_ERROR).release_raw().ok();
|
||||
/// This is a jobserver proxy used to ensure that we hold on to at least one token.
|
||||
pub struct Proxy {
|
||||
client: Client,
|
||||
data: Mutex<ProxyData>,
|
||||
|
||||
/// Threads which are waiting on a token will wait on this.
|
||||
wake_pending: Condvar,
|
||||
|
||||
helper: OnceLock<HelperThread>,
|
||||
}
|
||||
|
||||
impl Proxy {
|
||||
pub fn new() -> Arc<Self> {
|
||||
let proxy = Arc::new(Proxy {
|
||||
client: client(),
|
||||
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| {
|
||||
if let Ok(token) = token {
|
||||
let mut data = proxy_.data.lock();
|
||||
if data.pending > 0 {
|
||||
// Give the token to a waiting thread
|
||||
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, drop it.
|
||||
drop(data);
|
||||
drop(token);
|
||||
}
|
||||
}
|
||||
})
|
||||
.expect("failed to create helper thread");
|
||||
proxy.helper.set(helper).unwrap();
|
||||
proxy
|
||||
}
|
||||
|
||||
pub fn acquire_thread(&self) {
|
||||
let mut data = self.data.lock();
|
||||
|
||||
if data.used == 0 {
|
||||
// There was a free token around. This can
|
||||
// happen when all threads release their token.
|
||||
assert_eq!(data.pending, 0);
|
||||
data.used += 1;
|
||||
} else {
|
||||
// Request a token from the helper thread. We can't directly use `acquire_raw`
|
||||
// as we also need to be able to wait for the final token in the process which
|
||||
// does not get a corresponding `release_raw` call.
|
||||
self.helper.get().unwrap().request_token();
|
||||
data.pending += 1;
|
||||
self.wake_pending.wait(&mut data);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn release_thread(&self) {
|
||||
let mut data = self.data.lock();
|
||||
|
||||
if data.pending > 0 {
|
||||
// Give the token to a waiting thread
|
||||
data.pending -= 1;
|
||||
self.wake_pending.notify_one();
|
||||
} else {
|
||||
data.used -= 1;
|
||||
|
||||
// Release the token unless it's the last one in the process
|
||||
if data.used > 0 {
|
||||
drop(data);
|
||||
self.client.release_raw().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user