mirror of
https://github.com/hanzoai/next.js.git
synced 2026-10-30 14:46:38 +00:00
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.
84 lines
2.6 KiB
Rust
84 lines
2.6 KiB
Rust
#![feature(arbitrary_self_types)]
|
|
#![feature(arbitrary_self_types_pointers)]
|
|
|
|
use anyhow::Result;
|
|
use bincode::{Decode, Encode};
|
|
use turbo_tasks::{NonLocalValue, ResolvedVc, TaskInput, Vc, trace::TraceRawVcs};
|
|
use turbo_tasks_testing::{Registration, register, run_once_without_cache_check};
|
|
|
|
static REGISTRATION: Registration = register!();
|
|
|
|
const EXPECTED_TRACE: &str = "\
|
|
Adder::add_method (read cell of type u64)
|
|
self:
|
|
Adder::new (read cell of type trace_transient::Adder)
|
|
args:
|
|
unknown transient task (read cell of type ())
|
|
args:
|
|
unknown transient task (read cell of type u16)
|
|
unknown transient task (read cell of type u32)";
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_trace_transient() {
|
|
let result = run_once_without_cache_check(®ISTRATION, async {
|
|
test_trace_transient_operation(
|
|
ResolvedVc::cell(()),
|
|
ResolvedVc::<u16>::cell(2),
|
|
ResolvedVc::<u32>::cell(3),
|
|
)
|
|
.read_strongly_consistent()
|
|
.await
|
|
})
|
|
.await;
|
|
|
|
let message = format!("{:#}", result.unwrap_err());
|
|
assert!(message.contains(&EXPECTED_TRACE.to_string()));
|
|
}
|
|
|
|
#[turbo_tasks::function(operation)]
|
|
async fn test_trace_transient_operation(
|
|
arg1: ResolvedVc<()>,
|
|
arg2: ResolvedVc<u16>,
|
|
arg3: ResolvedVc<u32>,
|
|
) -> Result<Vc<u64>> {
|
|
let resolved = Adder::new(*arg1)
|
|
.add_method(*arg2, *arg3)
|
|
.to_resolved()
|
|
.await?;
|
|
Ok(read_incorrect_task_input(IncorrectTaskInput(resolved)))
|
|
}
|
|
|
|
#[turbo_tasks::value]
|
|
struct Adder;
|
|
|
|
#[turbo_tasks::value_impl]
|
|
impl Adder {
|
|
#[turbo_tasks::function]
|
|
fn new(arg: ResolvedVc<()>) -> Vc<Adder> {
|
|
let _ = arg; // Make sure unused argument filtering doesn't remove the arg
|
|
Adder.cell()
|
|
}
|
|
|
|
#[turbo_tasks::function]
|
|
async fn add_method(&self, arg1: ResolvedVc<u16>, arg2: ResolvedVc<u32>) -> Result<Vc<u64>> {
|
|
let _ = self; // Make sure unused argument filtering doesn't remove the arg
|
|
Ok(Vc::cell(u64::from(*arg1.await?) + u64::from(*arg2.await?)))
|
|
}
|
|
}
|
|
|
|
#[turbo_tasks::function]
|
|
async fn read_incorrect_task_input(value: IncorrectTaskInput) -> Result<Vc<u64>> {
|
|
Ok(Vc::cell(*value.0.await?))
|
|
}
|
|
|
|
/// Has an intentionally incorrect `TaskInput` implementation, representing some code that the debug
|
|
/// tracing might be particularly useful with.
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, TraceRawVcs, Encode, Decode, NonLocalValue)]
|
|
struct IncorrectTaskInput(ResolvedVc<u64>);
|
|
|
|
impl TaskInput for IncorrectTaskInput {
|
|
fn is_transient(&self) -> bool {
|
|
false
|
|
}
|
|
}
|