Skip to content

Commit 9a3c73a

Browse files
Merge branch 'canary' into fix-docs-typos
2 parents 66cfb7c + e5d296c commit 9a3c73a

File tree

11 files changed

+412
-680
lines changed

11 files changed

+412
-680
lines changed

Cargo.lock

Lines changed: 54 additions & 545 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/napi/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ fn init() {
7272
use std::panic::{set_hook, take_hook};
7373

7474
use tokio::runtime::Builder;
75-
use turbo_tasks::handle_panic;
75+
use turbo_tasks::panic_hooks::handle_panic;
7676
use turbo_tasks_malloc::TurboMalloc;
7777

7878
let prev_hook = take_hook();

turbopack/crates/turbo-tasks-backend/src/kv_backing_storage.rs

Lines changed: 128 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
1-
use std::{borrow::Borrow, cmp::max, path::PathBuf, sync::Arc};
1+
use std::{
2+
borrow::Borrow,
3+
cmp::max,
4+
env,
5+
path::PathBuf,
6+
sync::{Arc, LazyLock, Mutex, PoisonError, Weak},
7+
};
28

39
use anyhow::{Context, Result, anyhow};
410
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
511
use serde::{Deserialize, Serialize};
612
use smallvec::SmallVec;
713
use tracing::Span;
8-
use turbo_tasks::{SessionId, TaskId, backend::CachedTaskType, turbo_tasks_scope};
14+
use turbo_tasks::{
15+
SessionId, TaskId,
16+
backend::CachedTaskType,
17+
panic_hooks::{PanicHookGuard, register_panic_hook},
18+
turbo_tasks_scope,
19+
};
920

1021
use crate::{
1122
GitVersionInfo,
@@ -83,18 +94,48 @@ fn as_u32(bytes: impl Borrow<[u8]>) -> Result<u32> {
8394
Ok(n)
8495
}
8596

86-
pub struct KeyValueDatabaseBackingStorage<T: KeyValueDatabase> {
97+
// We want to invalidate the cache on panic for most users, but this is a band-aid to underlying
98+
// problems in turbo-tasks.
99+
//
100+
// If we invalidate the cache upon panic and it "fixes" the issue upon restart, users typically
101+
// won't report bugs to us, and we'll never find root-causes for these problems.
102+
//
103+
// These overrides let us avoid the cache invalidation / error suppression within Vercel so that we
104+
// feel these pain points and fix the root causes of bugs.
105+
fn should_invalidate_on_panic() -> bool {
106+
fn env_is_falsy(key: &str) -> bool {
107+
env::var_os(key)
108+
.is_none_or(|value| ["".as_ref(), "0".as_ref(), "false".as_ref()].contains(&&*value))
109+
}
110+
static SHOULD_INVALIDATE: LazyLock<bool> = LazyLock::new(|| {
111+
env_is_falsy("TURBO_ENGINE_SKIP_INVALIDATE_ON_PANIC") && env_is_falsy("__NEXT_TEST_MODE")
112+
});
113+
*SHOULD_INVALIDATE
114+
}
115+
116+
pub struct KeyValueDatabaseBackingStorageInner<T: KeyValueDatabase> {
87117
database: T,
88118
/// Used when calling [`BackingStorage::invalidate`]. Can be `None` in the memory-only/no-op
89119
/// storage case.
90120
base_path: Option<PathBuf>,
121+
/// Used to skip calling [`invalidate_db`] when the database has already been invalidated.
122+
invalidated: Mutex<bool>,
123+
_panic_hook_guard: Option<PanicHookGuard>,
124+
}
125+
126+
pub struct KeyValueDatabaseBackingStorage<T: KeyValueDatabase> {
127+
inner: Arc<KeyValueDatabaseBackingStorageInner<T>>,
91128
}
92129

93130
impl<T: KeyValueDatabase> KeyValueDatabaseBackingStorage<T> {
94131
pub fn new_in_memory(database: T) -> Self {
95132
Self {
96-
database,
97-
base_path: None,
133+
inner: Arc::new(KeyValueDatabaseBackingStorageInner {
134+
database,
135+
base_path: None,
136+
invalidated: Mutex::new(false),
137+
_panic_hook_guard: None,
138+
}),
98139
}
99140
}
100141

@@ -103,15 +144,44 @@ impl<T: KeyValueDatabase> KeyValueDatabaseBackingStorage<T> {
103144
version_info: &GitVersionInfo,
104145
is_ci: bool,
105146
database: impl FnOnce(PathBuf) -> Result<T>,
106-
) -> Result<Self> {
147+
) -> Result<Self>
148+
where
149+
T: Send + Sync + 'static,
150+
{
107151
check_db_invalidation_and_cleanup(&base_path)?;
108152
let versioned_path = handle_db_versioning(&base_path, version_info, is_ci)?;
153+
let database = (database)(versioned_path)?;
109154
Ok(Self {
110-
database: (database)(versioned_path)?,
111-
base_path: Some(base_path),
155+
inner: Arc::new_cyclic(
156+
move |weak_inner: &Weak<KeyValueDatabaseBackingStorageInner<T>>| {
157+
let panic_hook_guard = if should_invalidate_on_panic() {
158+
let weak_inner = weak_inner.clone();
159+
Some(register_panic_hook(Box::new(move |_| {
160+
let Some(inner) = weak_inner.upgrade() else {
161+
return;
162+
};
163+
// If a panic happened that must mean something deep inside of turbopack
164+
// or turbo-tasks failed, and it may be hard to recover. We don't want
165+
// the cache to stick around, as that may persist bugs. Make a
166+
// best-effort attempt to invalidate the database (ignoring failures).
167+
let _ = inner.invalidate();
168+
})))
169+
} else {
170+
None
171+
};
172+
KeyValueDatabaseBackingStorageInner {
173+
database,
174+
base_path: Some(base_path),
175+
invalidated: Mutex::new(false),
176+
_panic_hook_guard: panic_hook_guard,
177+
}
178+
},
179+
),
112180
})
113181
}
182+
}
114183

184+
impl<T: KeyValueDatabase> KeyValueDatabaseBackingStorageInner<T> {
115185
fn with_tx<R>(
116186
&self,
117187
tx: Option<&T::ReadTransaction<'_>>,
@@ -126,14 +196,38 @@ impl<T: KeyValueDatabase> KeyValueDatabaseBackingStorage<T> {
126196
Ok(r)
127197
}
128198
}
129-
}
130199

131-
fn get_infra_u32(database: &impl KeyValueDatabase, key: u32) -> Result<Option<u32>> {
132-
let tx = database.begin_read_transaction()?;
133-
database
134-
.get(&tx, KeySpace::Infra, IntKey::new(key).as_ref())?
135-
.map(as_u32)
136-
.transpose()
200+
fn invalidate(&self) -> Result<()> {
201+
// `base_path` can be `None` for a `NoopKvDb`
202+
if let Some(base_path) = &self.base_path {
203+
// Invalidation could happen frequently if there's a bunch of panics. We only need to
204+
// invalidate once, so grab a lock.
205+
let mut invalidated_guard = self
206+
.invalidated
207+
.lock()
208+
.unwrap_or_else(PoisonError::into_inner);
209+
if *invalidated_guard {
210+
return Ok(());
211+
}
212+
// Invalidate first, as it's a very fast atomic operation. `prevent_writes` is allowed
213+
// to be slower (e.g. wait for a lock) and is allowed to corrupt the database with
214+
// partial writes.
215+
invalidate_db(base_path)?;
216+
self.database.prevent_writes();
217+
// Avoid redundant invalidations from future panics
218+
*invalidated_guard = true;
219+
}
220+
Ok(())
221+
}
222+
223+
/// Used to read the previous session id and the next free task ID from the database.
224+
fn get_infra_u32(&self, key: u32) -> Result<Option<u32>> {
225+
let tx = self.database.begin_read_transaction()?;
226+
self.database
227+
.get(&tx, KeySpace::Infra, IntKey::new(key).as_ref())?
228+
.map(as_u32)
229+
.transpose()
230+
}
137231
}
138232

139233
impl<T: KeyValueDatabase + Send + Sync + 'static> BackingStorage
@@ -149,15 +243,17 @@ impl<T: KeyValueDatabase + Send + Sync + 'static> BackingStorage
149243

150244
fn next_free_task_id(&self) -> Result<TaskId> {
151245
Ok(TaskId::try_from(
152-
get_infra_u32(&self.database, META_KEY_NEXT_FREE_TASK_ID)
246+
self.inner
247+
.get_infra_u32(META_KEY_NEXT_FREE_TASK_ID)
153248
.context("Unable to read next free task id from database")?
154249
.unwrap_or(1),
155250
)?)
156251
}
157252

158253
fn next_session_id(&self) -> Result<SessionId> {
159254
Ok(SessionId::try_from(
160-
get_infra_u32(&self.database, META_KEY_SESSION_ID)
255+
self.inner
256+
.get_infra_u32(META_KEY_SESSION_ID)
161257
.context("Unable to read session id from database")?
162258
.unwrap_or(0)
163259
+ 1,
@@ -178,7 +274,7 @@ impl<T: KeyValueDatabase + Send + Sync + 'static> BackingStorage
178274
let operations = deserialize_with_good_error(operations.borrow())?;
179275
Ok(operations)
180276
}
181-
get(&self.database).context("Unable to read uncompleted operations from database")
277+
get(&self.inner.database).context("Unable to read uncompleted operations from database")
182278
}
183279

184280
fn serialize(task: TaskId, data: &Vec<CachedDataItem>) -> Result<SmallVec<[u8; 16]>> {
@@ -203,7 +299,7 @@ impl<T: KeyValueDatabase + Send + Sync + 'static> BackingStorage
203299
+ Sync,
204300
{
205301
let _span = tracing::trace_span!("save snapshot", session_id = ?session_id, operations = operations.len());
206-
let mut batch = self.database.write_batch()?;
302+
let mut batch = self.inner.database.write_batch()?;
207303

208304
// Start organizing the updates in parallel
209305
match &mut batch {
@@ -380,14 +476,15 @@ impl<T: KeyValueDatabase + Send + Sync + 'static> BackingStorage
380476
}
381477

382478
fn start_read_transaction(&self) -> Option<Self::ReadTransaction<'_>> {
383-
self.database.begin_read_transaction().ok()
479+
self.inner.database.begin_read_transaction().ok()
384480
}
385481

386482
unsafe fn forward_lookup_task_cache(
387483
&self,
388484
tx: Option<&T::ReadTransaction<'_>>,
389485
task_type: &CachedTaskType,
390486
) -> Result<Option<TaskId>> {
487+
let inner = &*self.inner;
391488
fn lookup<D: KeyValueDatabase>(
392489
database: &D,
393490
tx: &D::ReadTransaction<'_>,
@@ -401,12 +498,13 @@ impl<T: KeyValueDatabase + Send + Sync + 'static> BackingStorage
401498
let id = TaskId::try_from(u32::from_le_bytes(bytes)).unwrap();
402499
Ok(Some(id))
403500
}
404-
if self.database.is_empty() {
501+
if inner.database.is_empty() {
405502
// Checking if the database is empty is a performance optimization
406503
// to avoid serializing the task type.
407504
return Ok(None);
408505
}
409-
self.with_tx(tx, |tx| lookup(&self.database, tx, task_type))
506+
inner
507+
.with_tx(tx, |tx| lookup(&self.inner.database, tx, task_type))
410508
.with_context(|| format!("Looking up task id for {task_type:?} from database failed"))
411509
}
412510

@@ -415,6 +513,7 @@ impl<T: KeyValueDatabase + Send + Sync + 'static> BackingStorage
415513
tx: Option<&T::ReadTransaction<'_>>,
416514
task_id: TaskId,
417515
) -> Result<Option<Arc<CachedTaskType>>> {
516+
let inner = &*self.inner;
418517
fn lookup<D: KeyValueDatabase>(
419518
database: &D,
420519
tx: &D::ReadTransaction<'_>,
@@ -430,7 +529,8 @@ impl<T: KeyValueDatabase + Send + Sync + 'static> BackingStorage
430529
};
431530
Ok(Some(deserialize_with_good_error(bytes.borrow())?))
432531
}
433-
self.with_tx(tx, |tx| lookup(&self.database, tx, task_id))
532+
inner
533+
.with_tx(tx, |tx| lookup(&inner.database, tx, task_id))
434534
.with_context(|| format!("Looking up task type for {task_id} from database failed"))
435535
}
436536

@@ -440,6 +540,7 @@ impl<T: KeyValueDatabase + Send + Sync + 'static> BackingStorage
440540
task_id: TaskId,
441541
category: TaskDataCategory,
442542
) -> Result<Vec<CachedDataItem>> {
543+
let inner = &*self.inner;
443544
fn lookup<D: KeyValueDatabase>(
444545
database: &D,
445546
tx: &D::ReadTransaction<'_>,
@@ -461,24 +562,17 @@ impl<T: KeyValueDatabase + Send + Sync + 'static> BackingStorage
461562
let result: Vec<CachedDataItem> = deserialize_with_good_error(bytes.borrow())?;
462563
Ok(result)
463564
}
464-
self.with_tx(tx, |tx| lookup(&self.database, tx, task_id, category))
565+
inner
566+
.with_tx(tx, |tx| lookup(&inner.database, tx, task_id, category))
465567
.with_context(|| format!("Looking up data for {task_id} from database failed"))
466568
}
467569

468570
fn invalidate(&self) -> Result<()> {
469-
// `base_path` can be `None` for a `NoopKvDb`
470-
if let Some(base_path) = &self.base_path {
471-
// Invalidate first, as it's a very fast atomic operation. `prevent_writes` is allowed
472-
// to be slower (e.g. wait for a lock) and is allowed to corrupt the database with
473-
// partial writes.
474-
invalidate_db(base_path)?;
475-
self.database.prevent_writes()
476-
}
477-
Ok(())
571+
self.inner.invalidate()
478572
}
479573

480574
fn shutdown(&self) -> Result<()> {
481-
self.database.shutdown()
575+
self.inner.database.shutdown()
482576
}
483577
}
484578

turbopack/crates/turbo-tasks-backend/tests/panics.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,49 @@
11
use core::panic;
22
use std::{
33
panic::{set_hook, take_hook},
4-
sync::LazyLock,
4+
sync::{
5+
Arc, LazyLock,
6+
atomic::{AtomicBool, Ordering},
7+
},
58
};
69

710
use anyhow::Result;
811
use regex::Regex;
9-
use turbo_tasks::{Vc, backend::TurboTasksExecutionError, handle_panic};
12+
use turbo_tasks::{
13+
Vc,
14+
backend::TurboTasksExecutionError,
15+
panic_hooks::{handle_panic, register_panic_hook},
16+
};
1017
use turbo_tasks_testing::{Registration, register, run_without_cache_check};
1118

1219
static REGISTRATION: Registration = register!();
1320

1421
static FILE_PATH_REGEX: LazyLock<Regex> =
1522
LazyLock::new(|| Regex::new(r"panics\.rs:\d+:\d+$").unwrap());
1623

24+
// DO NOT ADD MORE TESTS TO THIS FILE!
25+
//
26+
// This test depends on the process-wide global panic handler. This test must be run in its own
27+
// process in isolation of any other tests.
1728
#[tokio::test]
18-
async fn test_panics_include_location() {
29+
async fn test_panic_hook() {
1930
let prev_hook = take_hook();
2031
set_hook(Box::new(move |info| {
2132
handle_panic(info);
2233
prev_hook(info);
2334
}));
2435

36+
let hook_was_called = Arc::new(AtomicBool::new(false));
37+
let _hook_guard = register_panic_hook({
38+
let hook_was_called = hook_was_called.clone();
39+
Box::new(move |_| hook_was_called.store(true, Ordering::SeqCst))
40+
});
41+
2542
let result =
2643
run_without_cache_check(&REGISTRATION, async move { anyhow::Ok(*double(3).await?) }).await;
2744

45+
assert!(hook_was_called.load(Ordering::SeqCst));
46+
2847
let error = result.unwrap_err();
2948
let root_cause = error.root_cause();
3049

turbopack/crates/turbo-tasks-fetch/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ turbo-tasks-fs = { workspace = true }
3333
turbopack-core = { workspace = true }
3434

3535
[dev-dependencies]
36-
httpmock = { workspace = true }
36+
mockito = { version = "1.7.0", default-features = false }
3737
tokio = { workspace = true, features = ["full"] }
3838
turbo-tasks-testing = { workspace = true }
3939
turbo-tasks-backend = { workspace = true }

turbopack/crates/turbo-tasks-fetch/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ pub async fn fetch(
8787

8888
Ok(Vc::cell(Ok(HttpResponse {
8989
status,
90-
body: HttpResponseBody::resolved_cell(HttpResponseBody(body)),
90+
body: HttpResponseBody(body).resolved_cell(),
9191
}
9292
.resolved_cell())))
9393
}

0 commit comments

Comments
 (0)