mirror of
https://github.com/hanzoai/next.js.git
synced 2026-10-30 14:46:38 +00:00
### What? Replaces the silent "make root node" promotion in the turbo-tasks backend with a panic that enforces tasks to already have the `root` attribute before performing strongly consistent reads, reading task collectibles, or removing collectibles. ### Why? Previously, the backend would silently promote any non-root task to a root node (aggregation number `u32::MAX`) when these operations were requested. This masked incorrect task configuration — tasks that needed root-level aggregation weren't explicitly declared as such, making it harder to reason about the aggregation graph and hiding potential performance issues from implicit promotions. ### How? **Backend enforcement (3 locations):** - **Strongly consistent read** (`backend/mod.rs`): Checks `NativeFunction.is_root` on the target task. If it's a persistent task without `root`, panics with both the target and reader task descriptions. - **Read task collectibles** (`backend/mod.rs`): Same check when reading collectibles from a task. - **Remove collectibles** (`operation/update_collectible.rs`): Same check when removing collectibles (count < 0). All three locations drop the task guard before panicking to avoid deadlocking with `debug_get_task_description`. **Macro support for `root` on methods:** - `value_impl_macro.rs`: Now reads the `root` attribute from `#[turbo_tasks::function(root)]` on inherent impl and trait impl methods (previously hardcoded to `false`). - `value_trait_macro.rs`: Same for trait default methods. **`root` attribute additions:** - All `#[turbo_tasks::function(operation)]` in test files, benchmarks, and fuzz code - Production operations in `crates/next-api/` that are read with strong consistency - Regular functions and methods in tests that use `.strongly_consistent()` - Trait methods in `value_impl` and `value_trait` blocks used with strong consistency **New test:** - `non_root_task_panic.rs`: Verifies the panic fires when attempting a strongly consistent read on a non-root operation task. Captures the panic from the worker thread via a panic hook since the panic propagates as a channel error to the test thread. <!-- NEXT_JS_LLM_PR --> --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
180 lines
5.7 KiB
Rust
180 lines
5.7 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 rustc_hash::{FxHashMap, FxHashSet};
|
|
use turbo_tasks::{OperationVc, ResolvedVc, State, Vc};
|
|
use turbo_tasks_testing::{Registration, register, run};
|
|
|
|
static REGISTRATION: Registration = register!();
|
|
|
|
#[turbo_tasks::value(transparent)]
|
|
struct Step(State<u32>);
|
|
|
|
#[turbo_tasks::function(operation, root)]
|
|
fn create_state_operation() -> Vc<Step> {
|
|
Step(State::new(0)).cell()
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_invalidation_map() {
|
|
run(®ISTRATION, || async {
|
|
let state_op = create_state_operation();
|
|
let state_vc = state_op.resolve().strongly_consistent().await?;
|
|
let state = state_op.read_strongly_consistent().await?;
|
|
state.set(1);
|
|
|
|
let map = create_map(state_vc);
|
|
let a = get_value(map, "a".to_string());
|
|
let b = get_value(map, "b".to_string());
|
|
let c = get_value(map, "c".to_string());
|
|
|
|
let a_ref = a.read_strongly_consistent().await?;
|
|
let b_ref = b.read_strongly_consistent().await?;
|
|
let c_ref = c.read_strongly_consistent().await?;
|
|
|
|
assert_eq!(a_ref.value, Some(1));
|
|
assert_eq!(b_ref.value, Some(2));
|
|
assert_eq!(c_ref.value, None);
|
|
|
|
state.set(2);
|
|
|
|
let a_ref2 = a.read_strongly_consistent().await?;
|
|
let b_ref2 = b.read_strongly_consistent().await?;
|
|
let c_ref2 = c.read_strongly_consistent().await?;
|
|
|
|
assert_eq!(a_ref2.value, Some(1));
|
|
assert_eq!(b_ref2.value, Some(22));
|
|
assert_eq!(c_ref2.value, None);
|
|
assert_eq!(a_ref.random, a_ref2.random);
|
|
assert_eq!(c_ref.random, c_ref2.random);
|
|
|
|
state.set(3);
|
|
|
|
let a_ref3 = a.read_strongly_consistent().await?;
|
|
let b_ref3 = b.read_strongly_consistent().await?;
|
|
let c_ref3 = c.read_strongly_consistent().await?;
|
|
|
|
assert_eq!(a_ref3.value, None);
|
|
assert_eq!(b_ref3.value, Some(22));
|
|
assert_eq!(c_ref3.value, Some(3));
|
|
assert_eq!(b_ref2.random, b_ref3.random);
|
|
|
|
anyhow::Ok(())
|
|
})
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
#[turbo_tasks::value(transparent, cell = "keyed")]
|
|
struct Map(FxHashMap<String, u32>);
|
|
|
|
#[turbo_tasks::function(operation, root)]
|
|
async fn create_map(step: ResolvedVc<Step>) -> Result<Vc<Map>> {
|
|
let step = step.await?;
|
|
let step_value = step.get();
|
|
|
|
Ok(Vc::cell(match *step_value {
|
|
1 => FxHashMap::from_iter([("a".to_string(), 1), ("b".to_string(), 2)]),
|
|
2 => FxHashMap::from_iter([("a".to_string(), 1), ("b".to_string(), 22)]),
|
|
3 => FxHashMap::from_iter([("c".to_string(), 3), ("b".to_string(), 22)]),
|
|
_ => FxHashMap::default(),
|
|
}))
|
|
}
|
|
|
|
#[turbo_tasks::value]
|
|
struct GetValueResult {
|
|
value: Option<u32>,
|
|
random: u32,
|
|
}
|
|
|
|
#[turbo_tasks::function(operation, root)]
|
|
async fn get_value(map: OperationVc<Map>, key: String) -> Result<Vc<GetValueResult>> {
|
|
let map = map.connect();
|
|
let value = map.get(&key).await?.as_deref().copied();
|
|
let random = rand::random::<u32>();
|
|
Ok(GetValueResult { value, random }.cell())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_invalidation_set() {
|
|
run(®ISTRATION, || async {
|
|
let state_op = create_state_operation();
|
|
let state_vc = state_op.resolve().strongly_consistent().await?;
|
|
let state = state_op.read_strongly_consistent().await?;
|
|
state.set(1);
|
|
|
|
let set = create_set(state_vc);
|
|
let a = has_value(set, "a".to_string());
|
|
let b = has_value(set, "b".to_string());
|
|
let c = has_value(set, "c".to_string());
|
|
|
|
let a_ref = a.read_strongly_consistent().await?;
|
|
let b_ref = b.read_strongly_consistent().await?;
|
|
let c_ref = c.read_strongly_consistent().await?;
|
|
|
|
assert!(a_ref.value);
|
|
assert!(b_ref.value);
|
|
assert!(!c_ref.value);
|
|
|
|
state.set(2);
|
|
|
|
let a_ref2 = a.read_strongly_consistent().await?;
|
|
let b_ref2 = b.read_strongly_consistent().await?;
|
|
let c_ref2 = c.read_strongly_consistent().await?;
|
|
|
|
assert!(a_ref2.value);
|
|
assert!(b_ref2.value);
|
|
assert!(!c_ref2.value);
|
|
assert_eq!(a_ref.random, a_ref2.random);
|
|
assert_eq!(b_ref.random, b_ref2.random);
|
|
assert_eq!(c_ref.random, c_ref2.random);
|
|
|
|
state.set(3);
|
|
|
|
let a_ref3 = a.read_strongly_consistent().await?;
|
|
let b_ref3 = b.read_strongly_consistent().await?;
|
|
let c_ref3 = c.read_strongly_consistent().await?;
|
|
|
|
assert!(!a_ref3.value);
|
|
assert!(b_ref3.value);
|
|
assert!(c_ref3.value);
|
|
assert_eq!(b_ref2.random, b_ref3.random);
|
|
|
|
anyhow::Ok(())
|
|
})
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
#[turbo_tasks::value(transparent, cell = "keyed")]
|
|
struct Set(FxHashSet<String>);
|
|
|
|
#[turbo_tasks::function(operation, root)]
|
|
async fn create_set(step: ResolvedVc<Step>) -> Result<Vc<Set>> {
|
|
let step = step.await?;
|
|
let step_value = step.get();
|
|
|
|
Ok(Vc::cell(match *step_value {
|
|
1 => FxHashSet::from_iter(["a".to_string(), "b".to_string()]),
|
|
2 => FxHashSet::from_iter(["e".to_string(), "a".to_string(), "b".to_string()]),
|
|
3 => FxHashSet::from_iter(["c".to_string(), "b".to_string()]),
|
|
_ => FxHashSet::default(),
|
|
}))
|
|
}
|
|
|
|
#[turbo_tasks::value]
|
|
struct HasValueResult {
|
|
value: bool,
|
|
random: u32,
|
|
}
|
|
|
|
#[turbo_tasks::function(operation, root)]
|
|
async fn has_value(set: OperationVc<Set>, key: String) -> Result<Vc<HasValueResult>> {
|
|
let set = set.connect();
|
|
let value = set.contains_key(&key).await?;
|
|
let random = rand::random::<u32>();
|
|
Ok(HasValueResult { value, random }.cell())
|
|
}
|