Skip to main content

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}