mysten_common/sync/execution_permit.rs
1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Per-thread execution permit, released when the thread blocks.
5//!
6//! Sui runs transaction execution on a pool of blocking threads whose concurrency is
7//! capped by a semaphore permit. Execution may need to block waiting for a value that
8//! another execution must produce (e.g. an object version). If a blocked thread keeps
9//! holding its permit, and enough threads block this way, no permit is left for the
10//! execution that would unblock them - a deadlock.
11//!
12//! To avoid this, the permit is installed on the thread with [`set_execution_permit`],
13//! and the blocking sync primitives in this crate call [`release_execution_permit`] the
14//! first time they must actually block (after a non-blocking `try_` check fails). The
15//! permit is intentionally *not* re-acquired afterwards: this may briefly exceed the
16//! configured concurrency, but the excess resolves itself as tasks complete.
17//!
18//! The permit is stored type-erased (`Box<dyn Send>`) so this crate need not depend on
19//! the specific semaphore; releasing it simply drops the box.
20
21use std::cell::RefCell;
22
23thread_local! {
24 static EXECUTION_PERMIT: RefCell<Option<Box<dyn Send>>> = const { RefCell::new(None) };
25}
26
27/// Guard returned by [`set_execution_permit`]. Releases the thread's permit on drop if a
28/// blocking primitive has not already done so.
29#[must_use = "the execution permit is released when this guard is dropped"]
30pub struct ExecutionPermitGuard(());
31
32/// Installs `permit` as the current thread's execution permit. Panics if one is already
33/// installed (each blocking-pool task installs exactly one permit for the duration of
34/// its run).
35pub fn set_execution_permit(permit: Box<dyn Send>) -> ExecutionPermitGuard {
36 EXECUTION_PERMIT.with(|slot| {
37 let previous = slot.borrow_mut().replace(permit);
38 assert!(
39 previous.is_none(),
40 "an execution permit is already installed on this thread"
41 );
42 });
43 ExecutionPermitGuard(())
44}
45
46/// Releases (drops) the current thread's execution permit if one is installed.
47/// Idempotent, and a no-op when no permit is installed. Called by blocking primitives
48/// immediately before they park or spin.
49pub fn release_execution_permit() {
50 // Take the permit out before dropping it so its `Drop` does not run while the
51 // thread-local is still borrowed (a permit's drop must not re-enter this module,
52 // but taking first keeps that guarantee local).
53 let permit = EXECUTION_PERMIT.with(|slot| slot.borrow_mut().take());
54 drop(permit);
55}
56
57impl Drop for ExecutionPermitGuard {
58 fn drop(&mut self) {
59 release_execution_permit();
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66 use std::sync::Arc;
67 use std::sync::atomic::{AtomicBool, Ordering};
68
69 /// Sets the flag when dropped, so tests can observe when the permit is released.
70 struct DropFlag(Arc<AtomicBool>);
71 impl Drop for DropFlag {
72 fn drop(&mut self) {
73 self.0.store(true, Ordering::SeqCst);
74 }
75 }
76
77 #[test]
78 fn guard_releases_on_drop() {
79 let released = Arc::new(AtomicBool::new(false));
80 {
81 let _guard = set_execution_permit(Box::new(DropFlag(released.clone())));
82 assert!(!released.load(Ordering::SeqCst));
83 }
84 assert!(released.load(Ordering::SeqCst));
85 }
86
87 #[test]
88 fn explicit_release_drops_permit_and_guard_is_noop() {
89 let released = Arc::new(AtomicBool::new(false));
90 let guard = set_execution_permit(Box::new(DropFlag(released.clone())));
91 release_execution_permit();
92 assert!(released.load(Ordering::SeqCst), "released immediately");
93 // Dropping the guard afterwards must be a harmless no-op.
94 drop(guard);
95 }
96
97 #[test]
98 fn release_without_permit_is_noop() {
99 release_execution_permit();
100 }
101}