turbo_tasks/effect.rs
1use std::{
2 collections::hash_map,
3 error::Error as StdError,
4 future::Future,
5 mem::{forget, replace},
6 sync::Arc,
7};
8
9use anyhow::Result;
10use async_trait::async_trait;
11use futures::{StreamExt, TryStreamExt};
12use parking_lot::{Mutex, MutexGuard};
13use rustc_hash::FxHashMap;
14use tracing::Instrument;
15
16use crate::{
17 self as turbo_tasks, CollectiblesSource, NonLocalValue, OperationVc, ReadRef, ResolvedVc,
18 TryJoinIterExt, Upcast, VcRead, VcValueType, emit,
19 event::Event,
20 invalidation::{Invalidator, get_invalidator},
21 manager::{
22 debug_assert_in_top_level_task, debug_assert_not_in_top_level_task, mark_top_level_task,
23 unmark_top_level_task_may_leak_eventually_consistent_state, with_turbo_tasks,
24 },
25 spawn,
26 trace::TraceRawVcs,
27};
28
29const APPLY_EFFECTS_CONCURRENCY_LIMIT: usize = 1024;
30
31/// An IO Side effect to be computed by turbo tasks and then executed outside of turbo tasks.
32#[async_trait]
33#[turbo_tasks::value_trait]
34pub trait Effect {
35 /// Read any Vc data needed for `apply()` and return the [`CapturedEffect`] that performs it.
36 ///
37 /// An implementation may elect to elide capturing data if the `EffectStateStorage` state is
38 /// already up to date.
39 async fn capture(&self) -> Result<Box<dyn CapturedEffect>>;
40}
41
42pub trait EffectExt {
43 fn emit(self);
44}
45
46impl<T> EffectExt for ResolvedVc<T>
47where
48 T: Upcast<Box<dyn Effect>>,
49{
50 fn emit(self) {
51 emit::<Box<dyn Effect>>(ResolvedVc::upcast_non_strict(self));
52 }
53}
54
55/// Post-capture effect. Holds data needed to perform the actual side effect in a top level context.
56///
57/// `apply()` is responsible for coordinating with [`EffectStateStorage`] via
58/// [`EffectStateStorage::run_apply`] (which handles the per-key state machine, in-progress
59/// coordination, dedup-hit short-circuit, and panic recovery).
60#[async_trait]
61pub trait CapturedEffect: TraceRawVcs + NonLocalValue + Send + Sync + 'static {
62 /// Unique key identifying this effect's target (e.g., absolute path bytes).
63 fn key(&self) -> Box<[u8]>;
64
65 /// Extract the hash of the value part of this effect for comparison.
66 fn value_hash(&self) -> u128;
67
68 /// Perform the side effect
69 ///
70 /// Implementations typically dispatch into [`EffectStateStorage::run_apply`].
71 async fn apply(&self) -> Result<(), ApplyError>;
72}
73
74/// Outcome of [`CapturedEffect::apply`]. Distinguishes a side-effect failure (terminal) from a
75/// soft failure where the captured form had no content and storage state diverged between
76/// capture and apply (recoverable via [`Effects::apply`]'s invalidator path).
77#[derive(Debug)]
78pub enum ApplyError {
79 /// The side effect itself failed.
80 Failed(Arc<dyn EffectError>),
81 /// Capture short-circuited content materialization (observed `Applied { matching }` in
82 /// storage), but by apply time the storage state had diverged and we have no content to
83 /// re-apply. [`Effects::apply`] should invalidate the producing operation and return
84 /// [`EffectsError::Retry`].
85 Retry,
86}
87
88/// The error type that an effect can return. We use `dyn std::error::Error` (instead of
89/// [`anyhow::Error`] or [`SharedError`]) to encourage use of structured error types that can
90/// potentially be transformed into `Issue`s.
91///
92/// We can't require that the returned error implements `Issue`:
93/// - `Issue` uses `FileSystemPath`
94/// - `turbo-tasks-fs` returns effect errors that should be transformed into `Issue`s.
95/// - It logically doesn't make sense to define `Issue` in `turbo-tasks-fs`, `Issue` can't be
96/// defined in a base crate either because it would form a circular crate dependency.
97///
98/// So instead, we leave it up to the caller to figure out how to downcast these errors themselves.
99///
100/// [`SharedError`]: crate::util::SharedError
101pub trait EffectError: StdError + TraceRawVcs + NonLocalValue + Send + Sync + 'static {}
102impl<T> EffectError for T where T: StdError + TraceRawVcs + NonLocalValue + Send + Sync + 'static {}
103
104enum EffectLastApplied {
105 Unapplied,
106 InProgress {
107 write_event: Event,
108 },
109 Applied {
110 value_hash: u128,
111 result: Result<(), Arc<dyn EffectError>>,
112 },
113}
114
115/// Per-key entry in the effect state storage.
116type EffectStateEntry = Arc<Mutex<EffectLastApplied>>;
117/// Shared state storage for tracking applied effects. Stored on the filesystem implementation
118/// (e.g. DiskFileSystemInner).
119#[derive(Default)]
120pub struct EffectStateStorage {
121 effect_state: Mutex<FxHashMap<Box<[u8]>, EffectStateEntry>>,
122}
123
124impl EffectStateStorage {
125 /// Returns true if the per-key state holds `Applied { value_hash == target, result: Ok(()) }`.
126 ///
127 /// Intended for use by [`Effect::capture`] to elide content materialization when the apply
128 /// would dedup. Reading this from inside a turbo-tasks task is sound because
129 /// [`Effects::apply`] re-checks at apply time and fires the producing task's invalidator on
130 /// mismatch (via the [`ApplyError::Retry`] / [`EffectsError::Retry`] pathway).
131 pub fn matches_applied(&self, key: &[u8], target: u128) -> bool {
132 let entry = self.effect_state.lock().get(key).cloned();
133 let Some(entry) = entry else { return false };
134 matches!(
135 &*entry.lock(),
136 EffectLastApplied::Applied {
137 value_hash,
138 result: Ok(()),
139 } if *value_hash == target,
140 )
141 }
142
143 /// Look up or create the per-key state entry.
144 fn entry_for(&self, key: Box<[u8]>) -> EffectStateEntry {
145 self.effect_state
146 .lock()
147 .entry(key)
148 .or_insert_with(|| Arc::new(Mutex::new(EffectLastApplied::Unapplied)))
149 .clone()
150 }
151
152 /// Coordinate an apply for `(key, value_hash)` against the per-key state machine.
153 ///
154 /// Dedup hits (state already `Applied` with a matching hash) return the cached result without
155 /// running `body`. Otherwise `body` runs once under an `InProgress` guard and the result is
156 /// stored. A `None` `body` (capture elided content because storage matched, but it no longer
157 /// does) yields [`ApplyError::Retry`].
158 pub async fn run_apply<E, F, Fut>(
159 &self,
160 key: Box<[u8]>,
161 value_hash: u128,
162 body: Option<F>,
163 ) -> Result<(), ApplyError>
164 where
165 E: EffectError,
166 F: FnOnce() -> Fut + Send,
167 Fut: Future<Output = Result<(), E>> + Send,
168 {
169 let entry = self.entry_for(key);
170
171 // If `body` panics or the future is dropped before completion, the guard's drop impl
172 // resets the per-key state to `Unapplied` and notifies other waiters via the `Event` it
173 // recovers from the previous `InProgress`, so they retry rather than deadlock or observe
174 // a stale "panic" cache entry.
175 struct EventGuard<'a> {
176 entry: &'a EffectStateEntry,
177 }
178 impl Drop for EventGuard<'_> {
179 fn drop(&mut self) {
180 let prev_state = replace(&mut *self.entry.lock(), EffectLastApplied::Unapplied);
181 let EffectLastApplied::InProgress { write_event } = prev_state else {
182 unreachable!("EventGuard: prev_state must be InProgress");
183 };
184 write_event.notify(usize::MAX);
185 }
186 }
187
188 let begin_in_progress = |mut last_applied_guard: MutexGuard<'_, _>| {
189 *last_applied_guard = EffectLastApplied::InProgress {
190 write_event: Event::new(|| || "effect application in progress".to_string()),
191 };
192 EventGuard { entry: &entry }
193 };
194
195 let event_guard = loop {
196 let listener;
197 {
198 let last_applied_guard = entry.lock();
199 match &*last_applied_guard {
200 EffectLastApplied::Unapplied => {
201 break begin_in_progress(last_applied_guard);
202 }
203 EffectLastApplied::Applied {
204 value_hash: stored,
205 result,
206 } => {
207 if value_hash == *stored {
208 return result.clone().map_err(ApplyError::Failed);
209 } else {
210 break begin_in_progress(last_applied_guard);
211 }
212 }
213 EffectLastApplied::InProgress { write_event } => {
214 // Event::listen registers the listener immediately, so notifications
215 // fired after we drop last_applied_guard cannot be missed.
216 listener = write_event.listen();
217 }
218 }
219 };
220 listener.await;
221 };
222
223 // We hold the InProgress guard. Either run the body, or — if we have no content to
224 // apply — release the guard (resetting state to Unapplied + waking waiters) and Retry.
225 let Some(body) = body else {
226 drop(event_guard);
227 return Err(ApplyError::Retry);
228 };
229
230 // Erase the body's concrete error type to `Arc<dyn EffectError>` so the cached result
231 // type is uniform across all callers of the same key.
232 let effect_result: Result<(), Arc<dyn EffectError>> = body()
233 .await
234 .map_err(|err| Arc::new(err) as Arc<dyn EffectError>);
235
236 let prev_state = replace(
237 &mut *entry.lock(),
238 EffectLastApplied::Applied {
239 value_hash,
240 result: effect_result.clone(),
241 },
242 );
243 forget(event_guard);
244
245 let EffectLastApplied::InProgress { write_event } = prev_state else {
246 unreachable!("Effect applied: prev_state must be InProgress");
247 };
248 write_event.notify(usize::MAX);
249
250 effect_result.map_err(ApplyError::Failed)
251 }
252}
253
254/// Capture effects. Call this from within a [turbo-tasks operation][crate::OperationVc].
255///
256/// Collectibles are read from `ResolvedVc`s, so this function, and the return value of this
257/// function should be applied with [`Effects::apply`].
258///
259/// It's important to wrap calls to this function in an [operation with a strongly consistent
260/// read][crate::OperationVc::read_strongly_consistent] before applying the effects outside of the
261/// operation at the top-level (e.g. in a `run_once` closure) with [`Effects::apply`].
262///
263/// # Example
264///
265/// ```rust
266/// # #![feature(arbitrary_self_types_pointers)]
267/// #
268/// # use anyhow::Result;
269/// # use turbo_tasks::{
270/// # Effects, ReadRef, Vc, read_strongly_consistent_and_apply_effects, take_effects,
271/// # };
272/// #
273/// # async fn _wrapper() -> Result<()> {
274/// # type Example = ();
275/// # type Args = ();
276/// # let args = ();
277/// # #[turbo_tasks::function(operation)]
278/// # fn some_turbo_tasks_operation(_args: Args) {}
279/// #
280/// #[turbo_tasks::value(serialization = "skip")]
281/// struct OutputWithEffects {
282/// output: ReadRef<Example>,
283/// effects: Effects,
284/// }
285///
286/// // ensure the return value and the collectibles match by using a single operation for both
287/// #[turbo_tasks::function(operation)]
288/// async fn some_turbo_tasks_operation_with_effects(args: Args) -> Result<Vc<OutputWithEffects>> {
289/// let operation = some_turbo_tasks_operation(args);
290/// // we must first read the operation to populate the collectibles
291/// let output = operation.connect().await?;
292/// // read the effects from the collectibles
293/// let effects = take_effects(operation).await?;
294/// Ok(OutputWithEffects { output, effects }.cell())
295/// }
296///
297/// // read with strong consistency and apply the effects once at the top-level
298/// // (e.g. in a `run_once` closure)
299/// let _result_with_effects = read_strongly_consistent_and_apply_effects(
300/// some_turbo_tasks_operation_with_effects(args),
301/// |result| &result.effects,
302/// )
303/// .await?;
304/// # Ok(())
305/// # }
306/// ```
307pub async fn take_effects(source: impl CollectiblesSource) -> Result<Effects> {
308 debug_assert_not_in_top_level_task("take_effects");
309 let effects = source.take_collectibles::<Box<dyn Effect>>();
310
311 let captured: Vec<Box<dyn CapturedEffect>> = effects
312 .into_iter()
313 .map(async |effect_vc| effect_vc.into_trait_ref().await?.capture().await)
314 .try_join()
315 .await?;
316
317 // detect duplicate keys
318 let unique_keys = build_unique_keys(&captured);
319
320 let invalidator = get_invalidator()
321 .expect("take_effects must be called from within a turbo-tasks task context");
322
323 Ok(Effects::new(captured, unique_keys, invalidator))
324}
325
326#[derive(thiserror::Error, Debug, TraceRawVcs, NonLocalValue)]
327#[error("Conflicting effects for the same key (key length: {key_len} bytes)")]
328struct ConflictingEffectError {
329 key_len: usize,
330}
331
332const MAX_KEYS_TO_DISPLAY: usize = 10;
333/// Error returned by [`Effects::apply`]. Callers should retry on `Retry`; everything else is
334/// terminal.
335#[derive(thiserror::Error, Debug, Clone)]
336pub enum EffectsError {
337 /// A side effect failed during apply. Holds the first error encountered.
338 #[error(transparent)]
339 Apply(Arc<dyn EffectError>),
340
341 #[error("conflicting effects for the same key (key length: {0} bytes)")]
342 Conflict(usize),
343
344 #[error(
345 "effect state diverged before apply for {}{}; producing task invalidated, retry required",
346 keys.iter().take(MAX_KEYS_TO_DISPLAY).cloned().collect::<Vec<_>>().join(", "),
347 if keys.len() > MAX_KEYS_TO_DISPLAY { format!(", ... ({} more)", keys.len() - MAX_KEYS_TO_DISPLAY) } else { String::new() }
348 )]
349 Retry { keys: Vec<String> },
350}
351
352impl From<Arc<dyn EffectError>> for EffectsError {
353 fn from(err: Arc<dyn EffectError>) -> Self {
354 EffectsError::Apply(err)
355 }
356}
357
358/// Dedup'd indices into the captured Vec — one entry per unique key. Computed eagerly in
359/// [`take_effects`] purely from the captured effects (no [`EffectStateStorage`] interaction);
360/// the apply-side state machine in [`EffectStateStorage::run_apply`] handles per-key hash dedup.
361type UniqueKeys = Result<Vec<usize>, Arc<ConflictingEffectError>>;
362
363/// Slice of captured effects, individually Arc'd. Each effect is `Arc<dyn CapturedEffect>`
364/// so callers can cheaply clone a Send handle out across `.await` boundaries without holding
365/// the outer mutex.
366type CapturedSlice = Arc<[Arc<dyn CapturedEffect>]>;
367
368/// Captured effects from an operation. This struct can be used to return Effects from a turbo-tasks
369/// function and apply them later.
370///
371/// # Cell semantics
372///
373/// `Effects` uses `cell = "new"`: every producer re-execution allocates a fresh cell value and
374/// the prior cell is dropped. Cell-level dedup of `Effects` is given up; per-key dedup at apply
375/// time is provided by [`EffectStateStorage`]'s state machine (see
376/// [`EffectStateStorage::run_apply`]), which short-circuits when storage already holds
377/// `Applied { value_hash }` matching the new hash.
378///
379/// `Effects::apply` is idempotent and safe to call multiple times on the same value — the state
380/// machine in `run_apply` ensures each underlying side effect runs at most once per stored
381/// `(key, value_hash)` pair across all callers.
382#[turbo_tasks::value(shared, eq = "manual", serialization = "skip", cell = "new")]
383pub struct Effects {
384 /// Pre-resolved effects awaiting application. Lives for the lifetime of the cell — released
385 /// when the producer reruns and `cell = "new"` overwrites the cell, which is when any
386 /// upstream `ReadRef` strong-count cascades are naturally released.
387 #[turbo_tasks(debug_ignore, trace_ignore)]
388 captured: CapturedSlice,
389 /// Captured at `take_effects` time. `None` for `Effects::empty()` (nothing to retry).
390 #[turbo_tasks(debug_ignore, trace_ignore)]
391 invalidator: Option<Invalidator>,
392 /// Unique key info computed eagerly in `take_effects`. Holds one index into `captured` per
393 /// unique key, or a `ConflictingEffectError` if two captured effects share a key with
394 /// different hashes. No [`EffectStateStorage`] interaction here — that is deferred to
395 /// `apply()`.
396 #[turbo_tasks(debug_ignore, trace_ignore)]
397 unique_keys: Arc<UniqueKeys>,
398}
399
400/// `PartialEq`/`Eq` are compat shims so containing structs (which derive `PartialEq`/`Eq` via
401/// `turbo_tasks::value`) can still embed `Effects`. The actual cell-update strategy for `Effects`
402/// itself is `cell = "new"` — see the doc-comment above — so this `PartialEq` is not consulted
403/// for `Effects` cells. We always return `false` to match `cell = "new"` semantics for the
404/// wrapper structs (they should also refresh on every producer run).
405impl PartialEq for Effects {
406 fn eq(&self, _other: &Self) -> bool {
407 false
408 }
409}
410impl Eq for Effects {}
411
412impl Effects {
413 /// A test-only placeholder `Effects` value with no effects (and no producer to invalidate).
414 #[cfg(test)]
415 fn empty() -> Self {
416 Self {
417 captured: Arc::from(Vec::new()),
418 invalidator: None,
419 unique_keys: Arc::new(Ok(Vec::new())),
420 }
421 }
422
423 fn new(
424 captured: Vec<Box<dyn CapturedEffect>>,
425 unique_keys: UniqueKeys,
426 invalidator: Invalidator,
427 ) -> Self {
428 // Convert Box<dyn> into Arc<dyn> per slot. Each Arc is independently Send/Sync.
429 let captured: CapturedSlice = captured
430 .into_iter()
431 .map(Arc::<dyn CapturedEffect>::from)
432 .collect();
433 Self {
434 captured,
435 invalidator: Some(invalidator),
436 unique_keys: Arc::new(unique_keys),
437 }
438 }
439
440 /// Applies all effects that have been captured.
441 ///
442 /// Dispatch goes through each captured effect's [`CapturedEffect::apply`] (via
443 /// [`EffectStateStorage::run_apply`]) which handles the per-key state machine, dedup hits,
444 /// in-progress coordination, and panic recovery. The dispatch is idempotent — calling
445 /// `apply()` multiple times on the same `Effects` value runs each underlying side effect at
446 /// most once per stored `(key, value_hash)` pair.
447 ///
448 /// If any captured effect signals [`ApplyError::Retry`] (its content was elided at capture
449 /// time and storage state diverged between capture and apply), the producing task is
450 /// invalidated and [`EffectsError::Retry`] is returned after the remaining keys finish.
451 /// Side-effect failures (`ApplyError::Failed`) propagate as [`EffectsError::Apply`]; the
452 /// first such error wins.
453 ///
454 /// `apply` must only be used in a "top-level" task (e.g. [`run_once`][crate::run_once]),
455 /// after [`take_effects`] is called from an [operation read with strong
456 /// consistency][crate::OperationVc::read_strongly_consistent].
457 ///
458 /// See [`take_effects`] for example usage.
459 ///
460 /// **Do not call this directly.** External callers must go through
461 /// [`read_strongly_consistent_and_apply_effects`] or
462 /// [`resolve_strongly_consistent_and_take_and_apply_effects`], which own the read+apply+retry
463 /// loop required to recover from [`EffectsError::Retry`]. Exposed publicly only as
464 /// [`Effects::apply_for_testing`] (`#[doc(hidden)]`) so integration tests can drive the apply
465 /// state machine directly.
466 async fn apply(&self) -> Result<(), EffectsError> {
467 debug_assert_in_top_level_task(
468 "Effects::apply must be called from a top-level task to avoid unintended \
469 re-executions due to eventual consistency",
470 );
471 let unique = match self.unique_keys.as_ref() {
472 Ok(unique) => unique.as_slice(),
473 Err(err) => return Err(EffectsError::Conflict(err.key_len)),
474 };
475 if unique.is_empty() {
476 return Ok(());
477 }
478
479 let span = tracing::info_span!("apply effects", count = unique.len());
480 let captured = &self.captured;
481
482 async {
483 // Collect the keys of any effects that signaled `Retry` across the parallel apply so
484 // we invalidate at most once at the end of the batch and can report which outputs
485 // forced the retry. `Apply` errors still take precedence — they fail-fast through the
486 // `try_for_each_concurrent`.
487 let retry_keys = Mutex::new(Vec::<String>::new());
488 let result: Result<(), EffectsError> = futures::stream::iter(unique.iter())
489 .map(Ok::<_, EffectsError>)
490 .try_for_each_concurrent(APPLY_EFFECTS_CONCURRENCY_LIMIT, async |idx| {
491 // Run each apply on its own spawned task so that pending effects execute in
492 // parallel rather than serially on this future (see #94140).
493 let effect = captured[*idx].clone();
494 match spawn(async move { effect.apply().await }).await {
495 Ok(()) => Ok(()),
496 Err(ApplyError::Failed(err)) => Err(EffectsError::Apply(err)),
497 Err(ApplyError::Retry) => {
498 let key = captured[*idx].key();
499 retry_keys
500 .lock()
501 .push(String::from_utf8_lossy(&key).into_owned());
502 Ok(())
503 }
504 }
505 })
506 .await;
507
508 match result {
509 Err(e) => Err(e),
510 Ok(()) => {
511 let retry_keys = retry_keys.into_inner();
512 if retry_keys.is_empty() {
513 Ok(())
514 } else {
515 self.signal_retry(retry_keys)
516 }
517 }
518 }
519 }
520 .instrument(span)
521 .await
522 }
523
524 /// Test-only public alias for [`Effects::apply`]. Lets integration tests in other crates drive
525 /// the per-key apply state machine directly (e.g. asserting dedup counts or the raw
526 /// [`EffectsError::Retry`] signal). Production code must use
527 /// [`read_strongly_consistent_and_apply_effects`] instead, which owns the retry loop.
528 #[doc(hidden)]
529 pub async fn apply_for_testing(&self) -> Result<(), EffectsError> {
530 self.apply().await
531 }
532
533 /// Invalidate the producing task (if any) and return [`EffectsError::Retry`] carrying the
534 /// `keys` that signaled [`ApplyError::Retry`] (their capture elided content materialization but
535 /// storage state diverged before apply).
536 fn signal_retry(&self, keys: Vec<String>) -> Result<(), EffectsError> {
537 if let Some(invalidator) = self.invalidator {
538 with_turbo_tasks(|tt| invalidator.invalidate(&**tt));
539 }
540 Err(EffectsError::Retry { keys })
541 }
542}
543
544/// Strongly-consistent read of `op`, then apply its effects, retrying the whole read+apply on
545/// [`EffectsError::Retry`].
546///
547/// `get_effects` extracts the [`Effects`] from the read value (for a wrapper struct this is
548/// `|v| &v.effects`; for an `OperationVc<Effects>` it is `|e| e`).
549///
550/// On [`EffectsError::Retry`] the producing operation has already been invalidated by
551/// [`Effects::apply`], so the next
552/// [`read_strongly_consistent`][OperationVc::read_strongly_consistent] re-runs the producer and
553/// yields a fresh [`Effects`] whose `capture()` re-materializes content. Retries are bounded to
554/// avoid livelock when two producers perpetually stomp the same key; after the first retry a
555/// warning is logged on each subsequent attempt, and on exhaustion the last `Retry` surfaces as an
556/// error.
557///
558/// This is one of two public entry points for applying effects (see also
559/// [`read_strongly_consistent_and_apply_effects_with`]) — [`Effects::apply`] is private so the
560/// retry contract cannot be bypassed.
561pub async fn read_strongly_consistent_and_apply_effects<T, F>(
562 op: OperationVc<T>,
563 get_effects: F,
564) -> Result<ReadRef<T>>
565where
566 T: VcValueType,
567 F: Fn(&<<T as VcValueType>::Read as VcRead<T>>::Target) -> &Effects,
568{
569 let mut attempts = 0usize;
570 loop {
571 let value = op.read_strongly_consistent().await?;
572 // Deref the `ReadRef<T>` to the read target (`T` for non-transparent types).
573 let effects = get_effects(&*value);
574 match effects.apply().await {
575 Ok(()) => return Ok(value),
576 Err(e) => handle_apply_retry(e, &mut attempts)?,
577 }
578 }
579}
580
581/// AVOID CALLING THIS UNLESS DEEPLY REQUIRED
582///
583/// Like [`read_strongly_consistent_and_apply_effects`], but the [`Effects`] directly accessed by
584/// calling [`take_effects`] on the supplied operation.
585///
586/// Unlike [`read_strongly_consistent_and_apply_effects`], this may be called from *inside* a
587/// turbo-tasks task (it owns the `mark`/`unmark` around `apply`). The consequence is that the
588/// effects may be re-applied if that enclosing task is invalidated — acceptable for lazily-created
589/// resources.
590pub async fn resolve_strongly_consistent_and_take_and_apply_effects<T>(
591 op: OperationVc<T>,
592) -> Result<ResolvedVc<T>>
593where
594 T: VcValueType,
595{
596 let mut attempts = 0usize;
597 loop {
598 let value = op.resolve().strongly_consistent().await?;
599 // Run the callback while *not* marked top-level so it can `take_effects` / read Vcs.
600
601 let effects = take_effects(op).await?;
602 // `Effects::apply` asserts it runs at the top-level. Mark only around the apply, then
603 // unmark so any further work (including the next loop iteration's read) is unaffected.
604 mark_top_level_task();
605 let result = effects.apply().await;
606 unmark_top_level_task_may_leak_eventually_consistent_state();
607 match result {
608 Ok(()) => return Ok(value),
609 Err(e) => handle_apply_retry(e, &mut attempts)?,
610 }
611 }
612}
613
614/// Shared retry-decision for the two `read_strongly_consistent_and_apply_effects*` helpers.
615///
616/// Returns `Ok(())` to signal the caller should retry the read+apply loop (bounded by
617/// `MAX_RETRIES`). Returns `Err` for terminal outcomes: a non-`Retry` error, or `Retry` after the
618/// retry budget is exhausted.
619fn handle_apply_retry(err: EffectsError, attempts: &mut usize) -> Result<()> {
620 const MAX_RETRIES: usize = 4; // chosen by a fair dice roll
621 match err {
622 EffectsError::Retry { keys } if *attempts < MAX_RETRIES => {
623 *attempts += 1;
624 // Warn on every retry after the first.
625 if *attempts > 1 {
626 tracing::warn!(
627 attempts = *attempts,
628 ?keys,
629 "retrying effect application; this implies multiple routes are fighting to \
630 write one of these files",
631 );
632 }
633 Ok(())
634 }
635 EffectsError::Retry { keys } => anyhow::bail!(
636 "gave up applying effects after {MAX_RETRIES} retries; repeated effect-state \
637 divergence on: {keys:?}. This implies multiple routes are fighting to write one of \
638 these files."
639 ),
640 e => Err(e.into()),
641 }
642}
643
644/// Build the deduped per-key indices into the captured slice. Detects per-key value-hash
645/// conflicts. This is the eager half of effect deduplication — it inspects only the captured
646/// effects themselves (no [`EffectStateStorage`] interaction) and is therefore safe to call
647/// from inside a turbo-tasks task in [`take_effects`].
648fn build_unique_keys(captured: &[Box<dyn CapturedEffect>]) -> UniqueKeys {
649 let mut by_key: FxHashMap<Box<[u8]>, usize> = FxHashMap::default();
650 for (idx, effect) in captured.iter().enumerate() {
651 match by_key.entry(effect.key()) {
652 hash_map::Entry::Vacant(entry) => {
653 entry.insert(idx);
654 }
655 hash_map::Entry::Occupied(entry) => {
656 if captured[*entry.get()].value_hash() != effect.value_hash() {
657 return Err(Arc::new(ConflictingEffectError {
658 key_len: entry.key().len(),
659 }));
660 }
661 }
662 }
663 }
664
665 let mut keys: Vec<usize> = by_key.into_values().collect();
666 // Sort by idx so the order is deterministic — useful for stable tracing/logging.
667 keys.sort_unstable();
668 Ok(keys)
669}
670
671#[cfg(test)]
672mod tests {
673 use crate::{CollectiblesSource, Effects, take_effects};
674
675 #[test]
676 #[allow(dead_code)]
677 fn is_send() {
678 fn assert_send<T: Send>(_: T) {}
679 fn check_effects_apply() {
680 assert_send(Effects::empty().apply());
681 }
682 fn check_take_effects<T: CollectiblesSource + Send + Sync>(t: T) {
683 assert_send(take_effects(t));
684 }
685 }
686}