Skip to main content

coven_foundation/
task.rs

1//! Work run apart from its caller — on the blocking pool, as its own task, or
2//! as one message of a long-lived loop — whose panic comes back to that
3//! caller as a value instead of an unwind (api.md E15).
4//!
5//! A panic turned into an `Err` at the nearest join lets the caller's own
6//! failure path run: the rollback or cleanup it does on `Err`, which an unwind
7//! would skip. The caller reports the panic as a bug. Work nobody waits for,
8//! a long-lived worker or work its caller stopped waiting for, reports its
9//! panic to a [`BugSink`] instead, the app's own reporter.
10
11use std::any::Any;
12use std::future::Future;
13use std::panic::{catch_unwind, AssertUnwindSafe};
14use std::pin::Pin;
15use std::task::{Context, Poll};
16
17#[path = "task_location.rs"]
18mod location;
19
20#[cfg(any(test, feature = "test-utils"))]
21#[path = "task_in_flight.rs"]
22mod in_flight;
23
24pub use location::{catching_operation, SourceLocation};
25
26/// Why work apart from its caller gave no result.
27#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
28pub enum TaskFailed {
29    /// The work panicked: a bug, which the caller reports.
30    #[error(transparent)]
31    Panicked(Bug),
32    /// The runtime it ran on is shutting down and dropped it before it
33    /// finished. Not a bug: the process is ending the work.
34    #[error(transparent)]
35    Shutdown(Shutdown),
36}
37
38/// Work that panicked: a bug in it, not a failure its caller can act on
39/// (E15). The caller reports it wherever it reports its own bugs, and the
40/// call that ran the work returns it as its error.
41#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
42#[error("{operation} panicked: {message}")]
43pub struct Bug {
44    /// What the work was doing, as its caller named it.
45    pub operation: &'static str,
46    /// The text the panic carried ([`panic_message`]).
47    pub message: String,
48    /// Where the bug showed itself: where the panic began, or where the code
49    /// that found the bug made it ([`Bug::new`]). `None` for a panic no catch
50    /// of coven's saw begin: one that reached a join uncaught
51    /// ([`TaskFailed::of_join`]), or one resumed with
52    /// `std::panic::resume_unwind` from a panic elsewhere.
53    pub location: Option<SourceLocation>,
54    /// Whether the bug showed itself as a panic or code found it.
55    pub kind: BugKind,
56}
57
58/// How a bug showed itself (E15).
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60pub enum BugKind {
61    /// Work panicked. The process's panic hook saw the panic begin, so an
62    /// app whose own hook reports every panic has reported this one.
63    Panic,
64    /// Code found state its guarantee rules out, without a panic
65    /// ([`Bug::new`]). No hook saw it: whoever the bug reaches reports it.
66    Found,
67}
68
69impl Bug {
70    /// The bug code found in `operation` without a panic, such as state a
71    /// `Drop` finds wrong, located where this is called.
72    #[track_caller]
73    pub fn new(operation: &'static str, message: impl Into<String>) -> Self {
74        Self {
75            operation,
76            message: message.into(),
77            location: Some(SourceLocation::of(std::panic::Location::caller())),
78            kind: BugKind::Found,
79        }
80    }
81}
82
83/// Work its runtime dropped as it shut down, before the work finished. Not a
84/// bug: the process is ending the work. The caller gets it as a value and
85/// returns it as its own error (E15).
86#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
87#[error("{operation} {outcome}: its runtime is shutting down")]
88pub struct Shutdown {
89    /// What the work was doing, as its caller named it.
90    pub operation: &'static str,
91    /// How far the work got, so whether anything it would change changed.
92    pub outcome: ShutdownOutcome,
93}
94
95/// How far work its runtime dropped got (E15).
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97pub enum ShutdownOutcome {
98    /// The work never started, so nothing it would change changed. tokio
99    /// can't stop a closure on the blocking pool once it has started, so a
100    /// closure the pool dropped is always one it never ran.
101    NotRun,
102    /// The work may have stopped at any of its waits, after some of its
103    /// steps: a task the runtime dropped partway. What it would change may
104    /// be changed in part, so the caller reads that state again before
105    /// relying on it.
106    Unknown,
107}
108
109impl std::fmt::Display for ShutdownOutcome {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        f.write_str(match self {
112            Self::NotRun => "did not run",
113            Self::Unknown => "may have run in part",
114        })
115    }
116}
117
118impl TaskFailed {
119    /// The failure a joined task's `error` stands for. A task the runtime
120    /// dropped may have run in part: `ShutdownOutcome::Unknown`. A task
121    /// whose work runs under [`caught`] hands its panic back as a value, so
122    /// its join fails only as its runtime ends it; a panic that reached the
123    /// join uncaught gives a bug that can't say where it began.
124    pub fn of_join(operation: &'static str, error: tokio::task::JoinError) -> Self {
125        if error.is_panic() {
126            failed_with(operation, error.into_panic())
127        } else {
128            Self::Shutdown(Shutdown { operation, outcome: ShutdownOutcome::Unknown })
129        }
130    }
131
132    /// This failure as the error of the call whose work it ended: its
133    /// `Bug` for a panic, its `Shutdown` for the runtime dropping the work.
134    pub fn into_error<E: From<Bug> + From<Shutdown>>(self) -> E {
135        match self {
136            Self::Panicked(bug) => E::from(bug),
137            Self::Shutdown(shutdown) => E::from(shutdown),
138        }
139    }
140
141    /// What the failed work was doing, as its caller named it.
142    pub fn operation(&self) -> &'static str {
143        match self {
144            Self::Panicked(Bug { operation, .. }) | Self::Shutdown(Shutdown { operation, .. }) => {
145                operation
146            }
147        }
148    }
149
150    /// Whether the work panicked, rather than being dropped by its runtime.
151    pub fn is_panic(&self) -> bool {
152        matches!(self, Self::Panicked(_))
153    }
154}
155
156/// The text a panic carried: what `panic!` and `expect` were given, or a
157/// placeholder for a payload that is not text.
158pub fn panic_message(payload: &(dyn Any + Send)) -> String {
159    if let Some(text) = payload.downcast_ref::<&'static str>() {
160        (*text).to_string()
161    } else if let Some(text) = payload.downcast_ref::<String>() {
162        text.clone()
163    } else {
164        "a panic whose payload is not text".to_string()
165    }
166}
167
168/// Run `work` on the blocking pool. A panic in it, or the runtime dropping it
169/// unstarted (`ShutdownOutcome::NotRun`), comes back as `TaskFailed`, never
170/// as an unwind.
171pub async fn blocking<T: Send + 'static>(
172    operation: &'static str,
173    work: impl FnOnce() -> T + Send + 'static,
174) -> Result<T, TaskFailed> {
175    match tokio::task::spawn_blocking(move || caught_sync(operation, work)).await {
176        Ok(done) => done.map_err(TaskFailed::Panicked),
177        // The closure catches its own panic, and the pool can't stop a
178        // closure once it has started, so the pool dropped it unstarted.
179        Err(_dropped) => {
180            Err(TaskFailed::Shutdown(Shutdown { operation, outcome: ShutdownOutcome::NotRun }))
181        }
182    }
183}
184
185/// Wait for a task whose work runs under [`caught`], as
186/// `tokio::spawn(caught(operation, work))` starts it. Its panic comes back as
187/// `TaskFailed::Panicked`, naming where it began; the runtime cancelling it,
188/// as `Shutdown`.
189pub async fn joined<T: Send + 'static>(
190    operation: &'static str,
191    task: tokio::task::JoinHandle<Result<T, Bug>>,
192) -> Result<T, TaskFailed> {
193    match task.await {
194        Ok(caught) => caught.map_err(TaskFailed::Panicked),
195        Err(error) => Err(TaskFailed::of_join(operation, error)),
196    }
197}
198
199/// Run `work` inline, catching a panic in any of its polls. For one message of
200/// a long-lived loop: the panic fails that message and the loop serves the
201/// next. Run inline, the work can't be dropped by its runtime, so a panic is
202/// its only failure.
203///
204/// After a panic `work` is dropped at once, inside a catch of its own, so a
205/// drop that panics on what the first panic left behind does not unwind into
206/// the caller either.
207pub async fn caught<T>(operation: &'static str, work: impl Future<Output = T>) -> Result<T, Bug> {
208    Caught { operation, work: Some(Box::pin(work)) }.await
209}
210
211/// [`caught`] for a synchronous call: an FFI callback, a dedicated thread's
212/// body, a `Drop`. Unwinding out of an `extern "C"` function aborts the
213/// process, so such a function catches here and answers with its own failure
214/// value.
215pub fn caught_sync<T>(operation: &'static str, work: impl FnOnce() -> T) -> Result<T, Bug> {
216    location::catch_located(operation, work).map_err(|(payload, at)| bug_of(operation, payload, at))
217}
218
219/// Where a bug in work nobody waits for goes (E15): a store's long-lived
220/// workers, such as its sync loop and upload worker, and work whose caller
221/// stopped waiting for it. The app passes its own crash reporter to
222/// `Coven::new`.
223pub trait BugSink: Send + Sync {
224    /// Report `bug`, which no caller hears of otherwise.
225    fn report(&self, bug: &Bug);
226}
227
228/// A shared [`BugSink`].
229pub type BugSinkRef = std::sync::Arc<dyn BugSink>;
230
231/// Logs each bug at error: the sink of an app that wants its bugs only
232/// logged.
233pub struct LoggedBugs;
234
235impl BugSink for LoggedBugs {
236    fn report(&self, bug: &Bug) {
237        tracing::error!(
238            operation = bug.operation,
239            location = bug.location.as_ref().map(tracing::field::display),
240            "a bug in work nobody waits for: {bug}"
241        );
242    }
243}
244
245/// Keeps every bug it hears of, and logs each at error, as an app's crash
246/// reporter would: the sink a test gives coven. A test that expects a bug
247/// takes it; one left untaken when the sink drops fails the test, so a bug
248/// no test expected never passes unseen.
249#[cfg(any(test, feature = "test-utils"))]
250pub struct HeardBugs {
251    heard: std::sync::Mutex<Vec<Bug>>,
252    /// Told of each bug as it is heard.
253    notice: tokio::sync::Notify,
254    /// Runs as the next bug is heard, inside its report.
255    next_report: std::sync::Mutex<Option<ReportHook>>,
256}
257
258/// What a test runs as [`HeardBugs`] hears its next bug.
259#[cfg(any(test, feature = "test-utils"))]
260type ReportHook = Box<dyn FnOnce(&Bug) + Send>;
261
262#[cfg(any(test, feature = "test-utils"))]
263impl HeardBugs {
264    /// A sink that has heard nothing yet, to share with coven.
265    pub fn shared() -> std::sync::Arc<Self> {
266        std::sync::Arc::new(Self {
267            heard: std::sync::Mutex::new(Vec::new()),
268            notice: tokio::sync::Notify::new(),
269            next_report: std::sync::Mutex::new(None),
270        })
271    }
272
273    /// Run `hook` as the next bug is heard, inside its report: for a test
274    /// that acts at the moment the reporter hands a bug over.
275    pub fn on_next_report(&self, hook: impl FnOnce(&Bug) + Send + 'static) {
276        *crate::locks::lock_unpoisoned(&self.next_report) = Some(Box::new(hook));
277    }
278
279    /// Every bug heard since the last take, in the order heard.
280    pub fn take(&self) -> Vec<Bug> {
281        std::mem::take(&mut *crate::locks::lock_unpoisoned(&self.heard))
282    }
283
284    /// Wait for the first bug not yet taken, and take it: for a test whose
285    /// bug reaches the sink after the call it made returned.
286    pub async fn next(&self) -> Bug {
287        loop {
288            let notified = self.notice.notified();
289            {
290                let mut heard = crate::locks::lock_unpoisoned(&self.heard);
291                if !heard.is_empty() {
292                    return heard.remove(0);
293                }
294            }
295            notified.await;
296        }
297    }
298}
299
300#[cfg(any(test, feature = "test-utils"))]
301impl BugSink for HeardBugs {
302    fn report(&self, bug: &Bug) {
303        tracing::error!(
304            operation = bug.operation,
305            location = bug.location.as_ref().map(tracing::field::display),
306            "a bug in work nobody waits for: {bug}"
307        );
308        let hook = crate::locks::lock_unpoisoned(&self.next_report).take();
309        if let Some(hook) = hook {
310            hook(bug);
311        }
312        crate::locks::lock_unpoisoned(&self.heard).push(bug.clone());
313        self.notice.notify_waiters();
314    }
315}
316
317#[cfg(any(test, feature = "test-utils"))]
318impl Drop for HeardBugs {
319    fn drop(&mut self) {
320        let untaken = self.take();
321        // A test already failing says why itself.
322        if !untaken.is_empty() && !std::thread::panicking() {
323            panic!("coven reported bugs no test took: {untaken:?}");
324        }
325    }
326}
327
328/// An error that may carry a bug: work its call ran panicked, and the call's
329/// error says so (E15). Work nobody waits for reports the bug its error
330/// carries, as it reports its own panic.
331pub trait CarriesBug {
332    /// The bug this error carries, itself or through the failure it keeps.
333    fn bug(&self) -> Option<&Bug>;
334}
335
336impl CarriesBug for Bug {
337    fn bug(&self) -> Option<&Bug> {
338        Some(self)
339    }
340}
341
342/// An I/O error carries the bug it was made from, if any.
343impl CarriesBug for std::io::Error {
344    fn bug(&self) -> Option<&Bug> {
345        self.get_ref()?.downcast_ref::<Bug>()
346    }
347}
348
349/// An error that can't happen carries no bug.
350impl CarriesBug for std::convert::Infallible {
351    fn bug(&self) -> Option<&Bug> {
352        match *self {}
353    }
354}
355
356impl CarriesBug for TaskFailed {
357    fn bug(&self) -> Option<&Bug> {
358        match self {
359            Self::Panicked(bug) => Some(bug),
360            Self::Shutdown(_) => None,
361        }
362    }
363}
364
365/// Hand `bug` to `bugs`. A sink that panics too is logged, and its panic
366/// goes no further: it runs where nothing above it catches.
367pub fn report(bugs: &dyn BugSink, bug: &Bug) {
368    if let Err(again) = caught_sync("report a bug", || bugs.report(bug)) {
369        tracing::error!(operation = bug.operation, "reporting {bug} panicked: {again}");
370    }
371}
372
373/// Hand the bug `error` carries, if it carries one, to `bugs`: the error of
374/// work nobody waits for, which no caller hears of. Whether it carried one,
375/// so the work logs an error that carried none.
376pub fn report_carried(bugs: &dyn BugSink, error: &impl CarriesBug) -> bool {
377    let bug = error.bug();
378    if let Some(bug) = bug {
379        report(bugs, bug);
380    }
381    bug.is_some()
382}
383
384/// `error`, once the bug it carries, if any, has gone to `bugs`: the error
385/// of work nobody waits for, which it records where readers see it.
386pub fn reported<E: CarriesBug>(bugs: &dyn BugSink, error: E) -> E {
387    report_carried(bugs, &error);
388    error
389}
390
391/// Run `work`, named `operation`, as its own task that nobody waits for: a
392/// worker its owner stops. A panic in it goes to `bugs`, then `ended` runs,
393/// so the owner ends what the work served in a way its readers see, such as
394/// recording that sync failed. A panic in `ended` goes to `bugs` too, and no
395/// further.
396pub fn spawn_owned<E, F>(
397    operation: &'static str,
398    bugs: BugSinkRef,
399    work: impl Future<Output = ()> + Send + 'static,
400    ended: E,
401) -> OwnedTask
402where
403    E: FnOnce(Bug) -> F + Send + 'static,
404    F: Future<Output = ()> + Send,
405{
406    OwnedTask {
407        operation,
408        task: tokio::spawn(async move {
409            let Err(bug) = caught(operation, work).await else {
410                return;
411            };
412            report(bugs.as_ref(), &bug);
413            if let Err(again) = caught(operation, ended(bug)).await {
414                report(bugs.as_ref(), &again);
415            }
416        }),
417    }
418}
419
420/// A task [`spawn_owned`] started. Dropping it leaves the task running.
421#[derive(Debug)]
422pub struct OwnedTask {
423    operation: &'static str,
424    task: tokio::task::JoinHandle<()>,
425}
426
427impl OwnedTask {
428    /// End the work at its next wait. Its `ended` does not run.
429    pub fn abort(&self) {
430        self.task.abort();
431    }
432
433    /// Whether the work has ended.
434    pub fn is_finished(&self) -> bool {
435        self.task.is_finished()
436    }
437
438    /// Wait until the work has ended: finished, panicked and reported, or
439    /// dropped by its runtime as it shuts down, which ends it too.
440    pub async fn ended(self) {
441        // The task catches every panic in it, so only its runtime ending it
442        // is left.
443        if let Err(error) = self.task.await {
444            tracing::debug!("{}", TaskFailed::of_join(self.operation, error));
445        }
446    }
447}
448
449/// Run `work`, named `operation`, as its own task, so it finishes even when
450/// its caller stops waiting, as closing a store does. Its panic comes back
451/// as `TaskFailed::Panicked` to a caller still waiting. Once none is, the
452/// panic goes to `bugs`, as does a bug its error carries.
453pub fn spawn_completing<T: Send + 'static, E: CarriesBug + Send + 'static>(
454    operation: &'static str,
455    bugs: BugSinkRef,
456    work: impl Future<Output = Result<T, E>> + Send + 'static,
457) -> CompletingTask<Result<T, E>> {
458    let (done, completion) = tokio::sync::oneshot::channel();
459    let heard = bugs.clone();
460    #[cfg(any(test, feature = "test-utils"))]
461    let flight = in_flight::Flight::begin(operation, in_flight::State::Task);
462    tokio::spawn(async move {
463        let answer = caught(operation, work).await;
464        #[cfg(any(test, feature = "test-utils"))]
465        drop(flight);
466        if let Err(unheard) = done.send(answer) {
467            report_unheard(heard.as_ref(), &unheard);
468        }
469    });
470    // The runtime drops a task partway as it shuts down.
471    CompletingTask {
472        operation,
473        dropped: ShutdownOutcome::Unknown,
474        bugs,
475        unread: report_unheard::<T, E>,
476        completion,
477    }
478}
479
480/// Run `work`, named `operation`, on the blocking pool, where it runs to its
481/// end even when its caller stops waiting. Its panic comes back as
482/// `TaskFailed::Panicked` to a caller still waiting, and the pool dropping it
483/// unstarted as `Shutdown` with `ShutdownOutcome::NotRun`. Once no caller
484/// waits, the panic goes to `bugs`, as does a bug its error carries.
485pub fn blocking_completing<T: Send + 'static, E: CarriesBug + Send + 'static>(
486    operation: &'static str,
487    bugs: BugSinkRef,
488    work: impl FnOnce() -> Result<T, E> + Send + 'static,
489) -> CompletingTask<Result<T, E>> {
490    let (done, completion) = tokio::sync::oneshot::channel();
491    let heard = bugs.clone();
492    // The pool runs the closure to its end once it starts, so the closure
493    // answers unless the pool dropped it unstarted.
494    #[cfg(any(test, feature = "test-utils"))]
495    let flight = in_flight::Flight::begin(operation, in_flight::State::Queued);
496    tokio::task::spawn_blocking(move || {
497        #[cfg(any(test, feature = "test-utils"))]
498        flight.started();
499        let answer = caught_sync(operation, work);
500        #[cfg(any(test, feature = "test-utils"))]
501        drop(flight);
502        if let Err(unheard) = done.send(answer) {
503            report_unheard(heard.as_ref(), &unheard);
504        }
505    });
506    CompletingTask {
507        operation,
508        dropped: ShutdownOutcome::NotRun,
509        bugs,
510        unread: report_unheard::<T, E>,
511        completion,
512    }
513}
514
515/// Hand `bugs` the panic of work nobody waited for, or the bug its error
516/// carries.
517fn report_unheard<T, E: CarriesBug>(bugs: &dyn BugSink, unheard: &Result<Result<T, E>, Bug>) {
518    match unheard {
519        Err(bug) => report(bugs, bug),
520        Ok(Err(error)) => {
521            report_carried(bugs, error);
522        }
523        Ok(Ok(_)) => {}
524    }
525}
526
527/// The result of work [`spawn_completing`] or [`blocking_completing`] runs.
528/// Dropping it leaves the work running, and the work's panic, or a bug its
529/// error carries, goes to the sink: once the work ends, or as it drops, for
530/// an answer the work gave that no one read.
531pub struct CompletingTask<T> {
532    operation: &'static str,
533    /// How far the work got if its runtime dropped it.
534    dropped: ShutdownOutcome,
535    /// Where an answer no one read reports its bug.
536    bugs: BugSinkRef,
537    /// Hands `bugs` the bug of an answer no one read.
538    unread: fn(&dyn BugSink, &Result<T, Bug>),
539    completion: tokio::sync::oneshot::Receiver<Result<T, Bug>>,
540}
541
542impl<T> std::fmt::Debug for CompletingTask<T> {
543    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
544        f.debug_struct("CompletingTask")
545            .field("operation", &self.operation)
546            .field("dropped", &self.dropped)
547            .finish_non_exhaustive()
548    }
549}
550
551impl<T> Drop for CompletingTask<T> {
552    fn drop(&mut self) {
553        // Closed first, so the work either answered already, and the answer
554        // is read here, or finds no one waiting and reports it itself.
555        self.completion.close();
556        if let Ok(answer) = self.completion.try_recv() {
557            (self.unread)(self.bugs.as_ref(), &answer);
558        }
559    }
560}
561
562impl<T> Future for CompletingTask<T> {
563    type Output = Result<T, TaskFailed>;
564
565    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
566        let (operation, outcome) = (self.operation, self.dropped);
567        Pin::new(&mut self.completion).poll(cx).map(|completed| match completed {
568            Ok(result) => result.map_err(TaskFailed::Panicked),
569            // The runtime dropped the work as it shut down.
570            Err(_dropped) => Err(TaskFailed::Shutdown(Shutdown { operation, outcome })),
571        })
572    }
573}
574
575/// The failure a panic that reached a join uncaught, with `payload`, stands
576/// for. No catch of coven's saw it begin, so its bug can't say where.
577fn failed_with(operation: &'static str, payload: Box<dyn Any + Send>) -> TaskFailed {
578    TaskFailed::Panicked(bug_of(operation, payload, None))
579}
580
581/// The bug a caught panic's `payload`, begun at `location`, stands for. The
582/// payload is dropped here inside a catch of its own: a payload whose `Drop`
583/// panics would otherwise unwind out of the helper that caught the first
584/// panic.
585fn bug_of(
586    operation: &'static str,
587    payload: Box<dyn Any + Send>,
588    location: Option<SourceLocation>,
589) -> Bug {
590    let bug =
591        Bug { operation, message: panic_message(payload.as_ref()), location, kind: BugKind::Panic };
592    if bug.location.is_none() {
593        tracing::warn!(
594            operation,
595            "{bug}, where no catch of coven's saw the panic begin, so its bug names no place in \
596             the code"
597        );
598    }
599    if let Err(again) = catch_unwind(AssertUnwindSafe(move || drop(payload))) {
600        tracing::error!(
601            operation,
602            "dropping the payload of a panic panicked again: {}",
603            panic_message(again.as_ref())
604        );
605        // This payload's own drop may panic too; leaking it is what keeps
606        // the unwind from reaching the caller.
607        std::mem::forget(again);
608    }
609    bug
610}
611
612struct Caught<F: Future> {
613    operation: &'static str,
614    /// `None` once `work` has finished or panicked.
615    work: Option<Pin<Box<F>>>,
616}
617
618impl<F: Future> Future for Caught<F> {
619    type Output = Result<F::Output, Bug>;
620
621    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
622        let operation = self.operation;
623        let Some(work) = self.work.as_mut() else {
624            panic!("{operation} polled after it finished");
625        };
626        match location::catch_located(operation, || work.as_mut().poll(cx)) {
627            Ok(Poll::Pending) => Poll::Pending,
628            Ok(Poll::Ready(value)) => {
629                self.work = None;
630                Poll::Ready(Ok(value))
631            }
632            Err((payload, at)) => {
633                let bug = bug_of(operation, payload, at);
634                let work = self.work.take();
635                if let Err(payload) = catch_unwind(AssertUnwindSafe(move || drop(work))) {
636                    tracing::error!(
637                        operation,
638                        "dropping work that panicked panicked again: {}",
639                        panic_message(payload.as_ref())
640                    );
641                    std::mem::forget(payload);
642                }
643                Poll::Ready(Err(bug))
644            }
645        }
646    }
647}
648
649#[cfg(test)]
650#[path = "task_tests.rs"]
651pub(crate) mod tests;