Skip to content

Commit d8a0f0a

Browse files
authored
[ty] Fix Salsa panic propagation (#24141)
## Summary This PR fixes probably the most likely case why users saw astral-sh/ty#1565 in their IDE. I added handling to convert panics to diagnostics in #17631 to `check_file_impl`. However, this was before `check_file_impl` became a Salsa query. We have to be a bit more careful, now that `check_file_impl` is a Salsa query. 1. We have to add an untracked read whenever we suppress a panic because Salsa only carries over dependencies of queries that run to completion and not of queries that panic (the assumption is that all queries unwind). Adding an `untracked_read` ensures that the `check_file_impl` reruns after every change. This is more often than necessary, but it is the best we can do here without knowing the exact dependencies that were collected up to when `check_file_impl`'s dependency panicked. 2. Suppressing all Salsa panics in `check_files_impl` is unlikely to be what we want because it means the function still runs to completion even when the query was cancelled. Instead, we want to propagate a cancellation so that its `db` handle gets released as quickly as possible to unblock any pending mutation. However, we do need some special handling for Salsa's propagating panic to avoid regressing #17631. For a query `A` running on thread `a` that depends on query `B` running on thread `b`. If `B` panics, Salsa throws the original panic on thread `b` but throws a `Cancelled::PropagatingPanic` panic on thread `a`. Thread `b`'s panic is the more useful one because it contains the actual panic information. We already convert `b`'s panic to a `Diagnostic`, but we silently ignore any `Cancelled::PropagatingPanic`. This PR also creates a propagating panic to a diagnostic. While these panics don't contain any useful information, they at least indicate to a user that `A` was only partially checked. They should have a second diagnostic for `B` that contains the full panic information (unless `B` is a query that didn't run as part of `project.check`, e.g. `hover`). ## Test plan I used @AlexWaygood's panda-stubs reproducer and I was no longer able to reproduce the Salsa panic. I plan on doing some CLI `--watch` testing tomorrow morning but this should block code review.
1 parent 84ff94b commit d8a0f0a

2 files changed

Lines changed: 39 additions & 9 deletions

File tree

crates/ruff_db/src/panic.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ impl Payload {
3232
}
3333

3434
impl PanicError {
35+
pub fn resume_unwind(self) -> ! {
36+
std::panic::resume_unwind(self.payload.0)
37+
}
38+
3539
pub fn to_diagnostic_message(&self, path: Option<impl std::fmt::Display>) -> String {
3640
use std::fmt::Write;
3741

crates/ty_project/src/lib.rs

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ use ruff_db::parsed::parsed_module;
2020
use ruff_db::source::{SourceTextError, source_text};
2121
use ruff_db::system::{SystemPath, SystemPathBuf};
2222
use rustc_hash::FxHashSet;
23-
use salsa::Durability;
24-
use salsa::Setter;
23+
use salsa::{Database, Durability, Setter};
2524
use std::backtrace::BacktraceStatus;
2625
use std::collections::hash_set;
2726
use std::iter::FusedIterator;
@@ -319,6 +318,9 @@ impl Project {
319318
for file in &files {
320319
let db = db.clone();
321320
let reporter = &*reporter;
321+
322+
db.unwind_if_revision_cancelled();
323+
322324
scope.spawn(move |_| {
323325
let check_file_span =
324326
tracing::debug_span!(parent: project_span, "check_file", ?file);
@@ -646,10 +648,9 @@ pub(crate) fn check_file_impl(db: &dyn Db, file: File) -> Result<Box<[Diagnostic
646648
{
647649
let db = AssertUnwindSafe(db);
648650
match catch(&**db, file, || check_types(*db, file)) {
649-
Ok(Some(type_check_diagnostics)) => {
651+
Ok(type_check_diagnostics) => {
650652
diagnostics.extend(type_check_diagnostics);
651653
}
652-
Ok(None) => {}
653654
Err(diagnostic) => diagnostics.push(diagnostic),
654655
}
655656
}
@@ -749,16 +750,37 @@ enum IOErrorKind {
749750
SourceText(#[from] SourceTextError),
750751
}
751752

752-
fn catch<F, R>(db: &dyn Db, file: File, f: F) -> Result<Option<R>, Diagnostic>
753+
fn catch<F, R>(db: &dyn Db, file: File, f: F) -> Result<R, Diagnostic>
753754
where
754755
F: FnOnce() -> R + UnwindSafe,
755756
{
756-
match ruff_db::panic::catch_unwind(|| {
757-
// Ignore salsa errors
758-
salsa::Cancelled::catch(f).ok()
759-
}) {
757+
match ruff_db::panic::catch_unwind(f) {
760758
Ok(result) => Ok(result),
761759
Err(error) => {
760+
match error.payload.downcast_ref::<salsa::Cancelled>() {
761+
None => {
762+
// Add a diagnostic (by not early returning) for
763+
// any non Salsa panic (a bug in ty)
764+
}
765+
Some(salsa::Cancelled::PropagatedPanic) => {
766+
// Add a diagnostic for propagated Salsa panics. That is, query `A`
767+
// running on thread `a` depends on query `B` running on thread `b`
768+
// and query `B` panics. However, avoid adding such a diagnostic
769+
// if query `B` panicked because of a cancellation by calling
770+
// `unwind_if_revision_cancelled`.
771+
//
772+
// The propagated Salsa panic isn't very actionable for users,
773+
// but it can be useful to know that file A failed to type check
774+
// because file B panicked (both files will have a panic-diagnostic).
775+
db.unwind_if_revision_cancelled();
776+
}
777+
778+
// For any pending write or local cancellation, resume the panic to abort the outer query.
779+
Some(_) => {
780+
error.resume_unwind();
781+
}
782+
}
783+
762784
let message = error.to_diagnostic_message(Some(file.path(db)));
763785
let mut diagnostic = Diagnostic::new(DiagnosticId::Panic, Severity::Fatal, message);
764786
diagnostic.add_bug_sub_diagnostics("%5Bpanic%5D");
@@ -790,6 +812,10 @@ where
790812
});
791813
}
792814

815+
// Report an untracked read because Salsa didn't carry over
816+
// the dependencies of any query called by `f` because it panicked.
817+
db.report_untracked_read();
818+
793819
Err(diagnostic)
794820
}
795821
}

0 commit comments

Comments
 (0)