1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
//! Throttle tasks if they poll too many I/O operations without yielding. //! //! This is used to prevent futures from running forever. Once a certain number of I/O operation is //! hit in a single run, I/O operations will begin returning //! [`Poll::Pending`][`std::task::Poll::Pending`] even if they're ready. use std::cell::Cell; use std::task::{Context, Poll}; use scoped_tls_hkt::scoped_thread_local; scoped_thread_local! { /// Number of times the current task is allowed to poll I/O operations. /// /// When this budget is used up, I/O operations will wake the current task and return /// [`Poll::Pending`][`std::task::Poll::Pending`]. /// /// This thread-local is set before running any task. static BUDGET: Cell<u32> } /// Sets an I/O budget for polling a future. /// /// Once this budget is exceeded, polled I/O operations will always wake the current task and /// return [`Poll::Pending`][`std::task::Poll::Pending`]. /// /// We throttle I/O this way in order to prevent futures from running for /// too long and thus starving other futures. pub(crate) fn setup<T>(poll: impl FnOnce() -> T) -> T { // This is a fairly arbitrary number that seems to work well in practice. BUDGET.set(&Cell::new(200), poll) } /// Returns [`Poll::Pending`] if the I/O budget has been used up. /// /// [`Poll::Pending`]: `std::task::Poll::Pending` pub(crate) fn poll(cx: &mut Context<'_>) -> Poll<()> { // Decrement the budget and check if it was zero. if BUDGET.is_set() && BUDGET.with(|b| b.replace(b.get().saturating_sub(1))) == 0 { // Make sure to wake the current task. The task is not *really* pending, we're just // artificially throttling it to let other tasks be run. cx.waker().wake_by_ref(); return Poll::Pending; } Poll::Ready(()) }