1use 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#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
28pub enum TaskFailed {
29 #[error(transparent)]
31 Panicked(Bug),
32 #[error(transparent)]
35 Shutdown(Shutdown),
36}
37
38#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
42#[error("{operation} panicked: {message}")]
43pub struct Bug {
44 pub operation: &'static str,
46 pub message: String,
48 pub location: Option<SourceLocation>,
54 pub kind: BugKind,
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60pub enum BugKind {
61 Panic,
64 Found,
67}
68
69impl Bug {
70 #[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#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
87#[error("{operation} {outcome}: its runtime is shutting down")]
88pub struct Shutdown {
89 pub operation: &'static str,
91 pub outcome: ShutdownOutcome,
93}
94
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97pub enum ShutdownOutcome {
98 NotRun,
102 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 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 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 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 pub fn is_panic(&self) -> bool {
152 matches!(self, Self::Panicked(_))
153 }
154}
155
156pub 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
168pub 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 Err(_dropped) => {
180 Err(TaskFailed::Shutdown(Shutdown { operation, outcome: ShutdownOutcome::NotRun }))
181 }
182 }
183}
184
185pub 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
199pub 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
211pub 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
219pub trait BugSink: Send + Sync {
224 fn report(&self, bug: &Bug);
226}
227
228pub type BugSinkRef = std::sync::Arc<dyn BugSink>;
230
231pub 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#[cfg(any(test, feature = "test-utils"))]
250pub struct HeardBugs {
251 heard: std::sync::Mutex<Vec<Bug>>,
252 notice: tokio::sync::Notify,
254 next_report: std::sync::Mutex<Option<ReportHook>>,
256}
257
258#[cfg(any(test, feature = "test-utils"))]
260type ReportHook = Box<dyn FnOnce(&Bug) + Send>;
261
262#[cfg(any(test, feature = "test-utils"))]
263impl HeardBugs {
264 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 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 pub fn take(&self) -> Vec<Bug> {
281 std::mem::take(&mut *crate::locks::lock_unpoisoned(&self.heard))
282 }
283
284 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 if !untaken.is_empty() && !std::thread::panicking() {
323 panic!("coven reported bugs no test took: {untaken:?}");
324 }
325 }
326}
327
328pub trait CarriesBug {
332 fn bug(&self) -> Option<&Bug>;
334}
335
336impl CarriesBug for Bug {
337 fn bug(&self) -> Option<&Bug> {
338 Some(self)
339 }
340}
341
342impl CarriesBug for std::io::Error {
344 fn bug(&self) -> Option<&Bug> {
345 self.get_ref()?.downcast_ref::<Bug>()
346 }
347}
348
349impl 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
365pub 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
373pub 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
384pub fn reported<E: CarriesBug>(bugs: &dyn BugSink, error: E) -> E {
387 report_carried(bugs, &error);
388 error
389}
390
391pub 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#[derive(Debug)]
422pub struct OwnedTask {
423 operation: &'static str,
424 task: tokio::task::JoinHandle<()>,
425}
426
427impl OwnedTask {
428 pub fn abort(&self) {
430 self.task.abort();
431 }
432
433 pub fn is_finished(&self) -> bool {
435 self.task.is_finished()
436 }
437
438 pub async fn ended(self) {
441 if let Err(error) = self.task.await {
444 tracing::debug!("{}", TaskFailed::of_join(self.operation, error));
445 }
446 }
447}
448
449pub 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 CompletingTask {
472 operation,
473 dropped: ShutdownOutcome::Unknown,
474 bugs,
475 unread: report_unheard::<T, E>,
476 completion,
477 }
478}
479
480pub 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 #[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
515fn 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
527pub struct CompletingTask<T> {
532 operation: &'static str,
533 dropped: ShutdownOutcome,
535 bugs: BugSinkRef,
537 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 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 Err(_dropped) => Err(TaskFailed::Shutdown(Shutdown { operation, outcome })),
571 })
572 }
573}
574
575fn failed_with(operation: &'static str, payload: Box<dyn Any + Send>) -> TaskFailed {
578 TaskFailed::Panicked(bug_of(operation, payload, None))
579}
580
581fn 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 std::mem::forget(again);
608 }
609 bug
610}
611
612struct Caught<F: Future> {
613 operation: &'static str,
614 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;