Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rt: unhandled panic config for current thread rt #4518

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
88 changes: 71 additions & 17 deletions tokio/src/runtime/basic_scheduler.rs
Expand Up @@ -57,13 +57,30 @@ struct Core {

/// Metrics batch
metrics: MetricsBatch,

/// True if a task panicked without being handled and the runtime is
/// configured to shutdown on unhandled panic.
unhandled_panic: bool,
}

#[derive(Clone)]
pub(crate) struct Spawner {
shared: Arc<Shared>,
}

/// Configuration settings passed in from the runtime builder.
pub(crate) struct Config {
/// Callback for a worker parking itself
pub(crate) before_park: Option<Callback>,

/// Callback for a worker unparking itself
pub(crate) after_unpark: Option<Callback>,

#[cfg(tokio_unstable)]
/// How to respond to unhandled task panics.
pub(crate) unhandled_panic: crate::runtime::UnhandledPanic,
}

/// Scheduler state shared between threads.
struct Shared {
/// Remote run queue. None if the `Runtime` has been dropped.
Expand All @@ -78,11 +95,8 @@ struct Shared {
/// Indicates whether the blocked on thread was woken.
woken: AtomicBool,

/// Callback for a worker parking itself
before_park: Option<Callback>,

/// Callback for a worker unparking itself
after_unpark: Option<Callback>,
/// Scheduler configuration options
config: Config,

/// Keeps track of various runtime metrics.
scheduler_metrics: SchedulerMetrics,
Expand Down Expand Up @@ -117,11 +131,7 @@ const REMOTE_FIRST_INTERVAL: u8 = 31;
scoped_thread_local!(static CURRENT: Context);

impl BasicScheduler {
pub(crate) fn new(
driver: Driver,
before_park: Option<Callback>,
after_unpark: Option<Callback>,
) -> BasicScheduler {
pub(crate) fn new(driver: Driver, config: Config) -> BasicScheduler {
let unpark = driver.unpark();

let spawner = Spawner {
Expand All @@ -130,8 +140,7 @@ impl BasicScheduler {
owned: OwnedTasks::new(),
unpark,
woken: AtomicBool::new(false),
before_park,
after_unpark,
config,
scheduler_metrics: SchedulerMetrics::new(),
worker_metrics: WorkerMetrics::new(),
}),
Expand All @@ -143,6 +152,7 @@ impl BasicScheduler {
tick: 0,
driver: Some(driver),
metrics: MetricsBatch::new(),
unhandled_panic: false,
})));

BasicScheduler {
Expand All @@ -157,6 +167,7 @@ impl BasicScheduler {
&self.spawner
}

#[track_caller]
pub(crate) fn block_on<F: Future>(&self, future: F) -> F::Output {
pin!(future);

Expand Down Expand Up @@ -296,7 +307,7 @@ impl Context {
fn park(&self, mut core: Box<Core>) -> Box<Core> {
let mut driver = core.driver.take().expect("driver missing");

if let Some(f) = &self.spawner.shared.before_park {
if let Some(f) = &self.spawner.shared.config.before_park {
// Incorrect lint, the closures are actually different types so `f`
// cannot be passed as an argument to `enter`.
#[allow(clippy::redundant_closure)]
Expand All @@ -319,7 +330,7 @@ impl Context {
core.metrics.returned_from_park();
}

if let Some(f) = &self.spawner.shared.after_unpark {
if let Some(f) = &self.spawner.shared.config.after_unpark {
// Incorrect lint, the closures are actually different types so `f`
// cannot be passed as an argument to `enter`.
#[allow(clippy::redundant_closure)]
Expand Down Expand Up @@ -460,6 +471,35 @@ impl Schedule for Arc<Shared> {
}
});
}

cfg_unstable! {
fn unhandled_panic(&self) {
use crate::runtime::UnhandledPanic;

match self.config.unhandled_panic {
UnhandledPanic::Ignore => {
// Do nothing
}
UnhandledPanic::ShutdownRuntime => {
// This hook is only called from within the runtime, so
// `CURRENT` should match with `&self`, i.e. there is no
// opportunity for a nested scheduler to be called.
CURRENT.with(|maybe_cx| match maybe_cx {
Some(cx) if Arc::ptr_eq(self, &cx.spawner.shared) => {
let mut core = cx.core.borrow_mut();

// If `None`, the runtime is shutting down, so there is no need to signal shutdown
if let Some(core) = core.as_mut() {
core.unhandled_panic = true;
self.owned.close_and_shutdown_all();
}
}
_ => panic!("runtime core not set in CURRENT thread-local"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT, this will only happen in the event of a Tokio bug, so this should probably be

Suggested change
_ => panic!("runtime core not set in CURRENT thread-local"),
_ => unreachable!("runtime core not set in CURRENT thread-local"),

})
}
}
}
}
}

impl Wake for Shared {
Expand All @@ -484,8 +524,9 @@ struct CoreGuard<'a> {
}

impl CoreGuard<'_> {
#[track_caller]
fn block_on<F: Future>(self, future: F) -> F::Output {
self.enter(|mut core, context| {
let ret = self.enter(|mut core, context| {
let _enter = crate::runtime::enter(false);
let waker = context.spawner.waker_ref();
let mut cx = std::task::Context::from_waker(&waker);
Expand All @@ -501,11 +542,16 @@ impl CoreGuard<'_> {
core = c;

if let Ready(v) = res {
return (core, v);
return (core, Some(v));
}
}

for _ in 0..MAX_TASKS_PER_TICK {
// Make sure we didn't hit an unhandled_panic
if core.unhandled_panic {
return (core, None);
}

// Get and increment the current tick
let tick = core.tick;
core.tick = core.tick.wrapping_add(1);
Expand Down Expand Up @@ -539,7 +585,15 @@ impl CoreGuard<'_> {
// pending I/O events.
core = context.park_yield(core);
}
})
});

match ret {
Some(ret) => ret,
None => {
// `block_on` panicked.
panic!("a spawned task panicked and the runtime is configured to shutdown on unhandled panic");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's kind of a bummer that we just create a new panic here, rather than continuing the original panic...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but I don't think its a big deal. The panic is still printed to stderr by the panic handler.

}
}
}

/// Enters the scheduler context. This sets the queue and other necessary
Expand Down
154 changes: 152 additions & 2 deletions tokio/src/runtime/builder.rs
Expand Up @@ -78,6 +78,86 @@ pub struct Builder {

/// Customizable keep alive timeout for BlockingPool
pub(super) keep_alive: Option<Duration>,

#[cfg(tokio_unstable)]
pub(super) unhandled_panic: UnhandledPanic,
}

cfg_unstable! {
/// How the runtime should respond to unhandled panics.
///
/// Instances of `UnhandledPanic` are passed to `Builder::unhandled_panic`
/// to configure the runtime behavior when a spawned task panics.
///
/// See [`Builder::unhandled_panic`] for more details.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum UnhandledPanic {
/// The runtime should ignore panics on spawned tasks.
///
/// The panic is forwarded to the task's `JoinHandle` and all spawned
/// tasks continue running normally.
///
/// This is the default behavior.
///
/// # Examples
///
/// ```
/// use tokio::runtime::{self, UnhandledPanic};
///
/// # pub fn main() {
/// let rt = runtime::Builder::new_current_thread()
/// .unhandled_panic(UnhandledPanic::ShutdownRuntime)
/// .build()
/// .unwrap();
///
/// let task1 = rt.spawn(async { panic!("boom"); });
/// let task2 = rt.spawn(async {
/// // This task completes normally
/// "done"
/// });
///
/// rt.block_on(async {
/// // The panic on the first task is forwarded to the `JoinHandle`
/// assert!(task1.await.is_err());
///
/// // The second task completes normally
/// assert!(task2.await.is_ok());
/// })
/// # }
/// ```
Ignore,

/// The runtime should immediately shutdown if a spawned task panics.
///
/// The runtime will immediately shutdown even if the panicked task's
/// [`JoinHandle`] is still available. All further spawned tasks will be
/// immediately dropped and call to [`Runtime::block_on`] will panic.
///
/// # Examples
///
/// ```should_panic
/// use tokio::runtime::{self, UnhandledPanic};
///
/// # pub fn main() {
/// let rt = runtime::Builder::new_current_thread()
/// .unhandled_panic(UnhandledPanic::ShutdownRuntime)
/// .build()
/// .unwrap();
///
/// rt.spawn(async { panic!("boom"); });
/// rt.spawn(async {
/// // This task never completes.
/// });
///
/// rt.block_on(async {
/// // Do some work
/// # loop { tokio::task::yield_now().await; }
/// })
/// # }
/// ```
ShutdownRuntime,
}
}

pub(crate) type ThreadNameFn = std::sync::Arc<dyn Fn() -> String + Send + Sync + 'static>;
Expand Down Expand Up @@ -145,6 +225,9 @@ impl Builder {
after_unpark: None,

keep_alive: None,

#[cfg(tokio_unstable)]
unhandled_panic: UnhandledPanic::Ignore,
}
}

Expand Down Expand Up @@ -554,7 +637,67 @@ impl Builder {
self
}

cfg_unstable! {
/// Configure how the runtime responds to an unhandled panic on a
/// spawned task.
///
/// By default, an unhandled panic (i.e. a panic not caught by
/// [`std::panic::catch_unwind`]) has no impact on the runtime's
/// execution. The panic is error value is forwarded to the task's
/// [`Joinhandle`] and all other spawned tasks continue running.
///
/// The `unhandled_panic` option enables configuring this behavior.
///
/// * `UnhandledPanic::Ignore` is the default behavior. Panics on
/// spawned tasks have no impact on the runtime's execution.
/// * `UnhandledPanic::ShutdownRuntime` will force the runtime to
/// shutdown immediately when a spawned task panics even if that
/// task's `JoinHandle` has not been dropped. All other spawned tasks
/// will immediatetly terminate and further calls to
/// [`Runtime::block_on`] will panic.
///
/// # Unstable
///
/// This option is currently unstable and its implementation is
/// incomplete. The API may change or be removed in the future. See
/// tokio-rs/tokio#4516 for more details.
///
/// # Examples
///
/// The following demonstrates a runtime configured to shutdown on
/// panic. The first spawned task panics and results in the runtime
/// shutting down. The second spawned task never has a chance to
/// execute. The call to `block_on` will panic due to the runtime being
/// forcibly shutdown.
///
/// ```should_panic
/// use tokio::runtime::{self, UnhandledPanic};
///
/// # pub fn main() {
/// let rt = runtime::Builder::new_current_thread()
/// .unhandled_panic(UnhandledPanic::ShutdownRuntime)
/// .build()
/// .unwrap();
///
/// rt.spawn(async { panic!("boom"); });
/// rt.spawn(async {
/// // This task never completes.
/// });
///
/// rt.block_on(async {
/// // Do some work
/// # loop { tokio::task::yield_now().await; }
/// })
/// # }
/// ```
pub fn unhandled_panic(&mut self, behavior: UnhandledPanic) -> &mut Self {
self.unhandled_panic = behavior;
self
}
}

fn build_basic_runtime(&mut self) -> io::Result<Runtime> {
use crate::runtime::basic_scheduler::Config;
use crate::runtime::{BasicScheduler, Kind};

let (driver, resources) = driver::Driver::new(self.get_cfg())?;
Expand All @@ -563,8 +706,15 @@ impl Builder {
// there are no futures ready to do something, it'll let the timer or
// the reactor to generate some new stimuli for the futures to continue
// in their life.
let scheduler =
BasicScheduler::new(driver, self.before_park.clone(), self.after_unpark.clone());
let scheduler = BasicScheduler::new(
driver,
Config {
before_park: self.before_park.clone(),
after_unpark: self.after_unpark.clone(),
#[cfg(tokio_unstable)]
unhandled_panic: self.unhandled_panic.clone(),
},
);
let spawner = Spawner::Basic(scheduler.spawner().clone());

// Blocking pool
Expand Down
3 changes: 3 additions & 0 deletions tokio/src/runtime/mod.rs
Expand Up @@ -212,6 +212,9 @@ cfg_rt! {

mod builder;
pub use self::builder::Builder;
cfg_unstable! {
pub use self::builder::UnhandledPanic;
}

pub(crate) mod context;
pub(crate) mod driver;
Expand Down