Files
next.js/turbopack/crates/turbo-tasks-backend/tests/resolved_vc.rs
Benjamin WoodruffandGitHub 89f3649a00 Turbopack: Panic if a top-level task attempts an eventually consistent read (#89735)
We've had some really bad bugs in the past, which can be hard to root-cause due to eventually-consistent reads a the top-level, e.g. https://github.com/vercel/next.js/pull/77511

Outside of tests, there's no good justification for doing an eventually-consistent read at the top level. It may leak incomplete computations, which can contain errors that would otherwise be resolved by a strongly consistent read.

The basic idea here is simple:

- Store a flag in `CURRENT_TASK_STATE` indicating that we're in a top-level task.
- When reading a task output or cell, check for that flag.

But, there are a few complications:

- `RawVc`'s read and resolution logic is wrong, and internally uses multiple eventually consistent operations after a strongly consistent read.
    - For this to be correct, it needs to happen as one atomic operation inside of the task backend, but it would change the backend API contract, so it's not straightforward to do that.
    - I added a `SUPPRESS_EVENTUAL_CONSISTENCY_TOP_LEVEL_TASK_CHECK` flag that I'm using for now.
- There are a ton of places that are doing eventually consistent reads for no good reason. These aren't causing problems today, but we should fix them instead of suppressing them. Claude helped with this, but it's still a lot.
- Some tests actually do need eventually-consistent reads because they're testing some internal behavior of turbo-tasks. I added an intentionally long-named `unmark_top_level_task_may_leak_eventually_consistent_state` helper for this.

I manually tested `turbopack-cli` against our bundler-benchmark repository that uses it.
2026-02-26 16:14:30 -08:00

150 lines
4.9 KiB
Rust

#![feature(arbitrary_self_types)]
#![feature(arbitrary_self_types_pointers)]
#![allow(clippy::needless_return)] // tokio macro-generated code doesn't respect this
use anyhow::Result;
use turbo_tasks::{ReadRef, ResolvedVc, Vc};
use turbo_tasks_testing::{Registration, register, run_once};
static REGISTRATION: Registration = register!();
#[turbo_tasks::value]
struct Wrapper(u32);
#[turbo_tasks::function]
fn returns_int(value: u32) -> Vc<u32> {
Vc::cell(value)
}
#[turbo_tasks::function]
fn assert_resolved(input: ResolvedVc<u32>) {
// double-check that this `ResolvedVc` is *actually* resolved
let input_vc: Vc<u32> = *input;
assert!(input_vc.is_resolved());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_conversion() -> Result<()> {
// pass a nonce to re-run the test body on every turbo-tasks restart
let mut nonce = 0;
run_once(&REGISTRATION, move || {
nonce += 1;
async move {
#[turbo_tasks::function(operation)]
async fn test_operation(nonce: u32) -> Result<Vc<()>> {
let _ = nonce; // ensure the nonce is part of our cache key
let unresolved: Vc<u32> = Vc::cell(42);
let resolved: ResolvedVc<u32> = unresolved.to_resolved().await?;
let _: Vc<u32> = *resolved;
let _: ReadRef<u32> = resolved.await?;
let _: ReadRef<u32> = (&resolved).await?;
let _: u32 = *resolved.await?;
let _: u32 = *(&resolved).await?;
Ok(Vc::cell(()))
}
test_operation(nonce).read_strongly_consistent().await
}
})
.await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_cell_construction() -> Result<()> {
let mut nonce = 0;
run_once(&REGISTRATION, move || {
nonce += 1;
async move {
#[turbo_tasks::function(operation)]
async fn test_operation(nonce: u32) -> Result<Vc<()>> {
let _ = nonce;
let a: ResolvedVc<u32> = ResolvedVc::cell(42);
assert_eq!(*a.await?, 42);
let b: ResolvedVc<Wrapper> = Wrapper(42).resolved_cell();
assert_eq!(b.await?.0, 42);
Ok(Vc::cell(()))
}
test_operation(nonce).read_strongly_consistent().await
}
})
.await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_resolved_vc_as_arg() -> Result<()> {
let mut nonce = 0;
run_once(&REGISTRATION, move || {
nonce += 1;
async move {
#[turbo_tasks::function(operation)]
async fn test_operation(nonce: u32) -> Result<Vc<()>> {
dbg!(nonce);
let _ = nonce;
let unresolved: Vc<u32> = returns_int(42);
assert!(!unresolved.is_resolved());
// calling a function should cause it's arguments to get resolved automatically
assert_resolved(unresolved).await?;
Ok(Vc::cell(()))
}
test_operation(nonce).read_strongly_consistent().await
}
})
.await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_into_future() -> Result<()> {
let mut nonce = 0;
run_once(&REGISTRATION, move || {
nonce += 1;
async move {
#[turbo_tasks::function(operation)]
async fn test_operation(nonce: u32) -> Result<Vc<()>> {
let _ = nonce;
let mut resolved = ResolvedVc::cell(42);
let _: ReadRef<u32> = resolved.await?;
let _: ReadRef<u32> = (&resolved).await?;
let _: ReadRef<u32> = (&mut resolved).await?;
let mut unresolved = Vc::cell(42);
let _: ReadRef<u32> = unresolved.await?;
let _: ReadRef<u32> = (&unresolved).await?;
let _: ReadRef<u32> = (&mut unresolved).await?;
Ok(Vc::cell(()))
}
test_operation(nonce).read_strongly_consistent().await
}
})
.await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sidecast() -> Result<()> {
run_once(&REGISTRATION, || async {
let concrete_value = ImplementsAAndB.resolved_cell();
let as_a = ResolvedVc::upcast::<Box<dyn TraitA>>(concrete_value);
let as_b = ResolvedVc::try_sidecast::<Box<dyn TraitB>>(as_a);
assert!(as_b.is_some());
let as_c = ResolvedVc::try_sidecast::<Box<dyn TraitC>>(as_a);
assert!(as_c.is_none());
Ok(())
})
.await
}
#[turbo_tasks::value_trait]
trait TraitA {}
#[turbo_tasks::value_trait]
trait TraitB {}
#[turbo_tasks::value_trait]
trait TraitC {}
#[turbo_tasks::value]
struct ImplementsAAndB;
#[turbo_tasks::value_impl]
impl TraitA for ImplementsAAndB {}
#[turbo_tasks::value_impl]
impl TraitB for ImplementsAAndB {}