Skip to main content

onetaskgraph_plugin_api/
held.rs

1//! A write held to the version of the task it writes: `--if-version`.
2//!
3//! The engine holds a write of one task to the version its caller read, and refuses the write
4//! when the task is at any other. Where that check is made is the source's to say, through
5//! [`TaskSource::holds_versions`](crate::TaskSource::holds_versions):
6//!
7//! - A source that serializes its own writes — a folder of Markdown, under the lock beside the
8//!   task's file; an in-memory source, under the mutex over its work — makes the check and the
9//!   write one step, so no competing write can land between them. It answers `true`, and reads
10//!   the version the call is held to with [`HeldVersion::expected`] inside each task write,
11//!   refusing a moved one with [`HeldVersion::moved`].
12//! - A source whose backend offers no conditional write answers `false`, and the engine reads
13//!   the task through it immediately before the write instead. The time between that read and
14//!   the mutation is the backend's residual race, which each such plugin's documentation states.
15//!
16//! The hold is task-local, so a source that never asks writes exactly as it did, and one that
17//! asks outside a held call is answered that nothing is held.
18
19use std::future::Future;
20use std::sync::{Arc, Mutex};
21
22use crate::{NativeId, SourceError};
23
24tokio::task_local! {
25    static HELD: Arc<HeldVersion>;
26}
27
28/// One task's write held to the version its caller read.
29#[derive(Debug)]
30pub struct HeldVersion {
31    id: NativeId,
32    version: String,
33    /// The version the source found instead, once it has refused the write.
34    moved: Mutex<Option<String>>,
35}
36
37impl HeldVersion {
38    /// A write of the task `id` held to `version`.
39    #[must_use]
40    pub fn new(id: NativeId, version: String) -> Self {
41        Self {
42            id,
43            version,
44            moved: Mutex::new(None),
45        }
46    }
47
48    /// Run `call` with this hold answering every [`HeldVersion::expected`] made inside it.
49    pub async fn scope<F: Future>(held: Arc<Self>, call: F) -> F::Output {
50        HELD.scope(held, call).await
51    }
52
53    /// The version the write in progress holds the task `id` to — `None` when no write in
54    /// progress is held, or the one that is holds another task.
55    #[must_use]
56    pub fn expected(id: &NativeId) -> Option<String> {
57        HELD.try_with(|held| (&held.id == id).then(|| held.version.clone()))
58            .ok()
59            .flatten()
60    }
61
62    /// Refuse the write in progress of the task `id`, which the source found at `found` rather
63    /// than at the version it is held to: the refusal the source fails its call with, writing
64    /// nothing, recorded so the engine reports it as the version having moved.
65    #[must_use]
66    pub fn moved(id: &NativeId, found: Option<String>) -> SourceError {
67        let found = found.unwrap_or_default();
68        let _ = HELD.try_with(|held| {
69            if &held.id == id {
70                *held
71                    .moved
72                    .lock()
73                    .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(found.clone());
74            }
75        });
76        SourceError::Refused {
77            message: format!(
78                "task {id} is at version {found:?}, not the version the write is held to, so \
79                 nothing was written"
80            ),
81        }
82    }
83
84    /// The version the source found the task at when it refused the write, if it did.
85    #[must_use]
86    pub fn found(&self) -> Option<String> {
87        self.moved
88            .lock()
89            .unwrap_or_else(std::sync::PoisonError::into_inner)
90            .clone()
91    }
92
93    /// The task this hold is for.
94    #[must_use]
95    pub const fn id(&self) -> &NativeId {
96        &self.id
97    }
98
99    /// The version the write is held to.
100    #[must_use]
101    pub fn version(&self) -> &str {
102        &self.version
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[tokio::test]
111    async fn a_hold_answers_only_inside_its_scope_and_only_for_its_task() {
112        let id = NativeId::from("t1");
113        assert_eq!(HeldVersion::expected(&id), None);
114        let held = Arc::new(HeldVersion::new(id.clone(), "v1".to_owned()));
115        let inside = HeldVersion::scope(Arc::clone(&held), async {
116            let other = HeldVersion::expected(&NativeId::from("t2"));
117            let refused = HeldVersion::moved(&id, Some("v2".to_owned()));
118            (HeldVersion::expected(&id), other, refused)
119        })
120        .await;
121        assert_eq!(inside.0.as_deref(), Some("v1"));
122        assert_eq!(inside.1, None);
123        assert!(matches!(inside.2, SourceError::Refused { .. }));
124        assert_eq!(held.found().as_deref(), Some("v2"));
125    }
126}