Skip to main content

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