Skip to main content

cove_runtime/
shared.rs

1//! `Shared<T>`: mutable state that more than one task may reach.
2//!
3//! The Language Card names this type in the sentence that keeps a vector out
4//! of a task: "finish it as an array or wrap mutable state in `Shared` or
5//! another synchronized type." ADR 0008 makes it the one value that crosses a
6//! task boundary by sharing rather than by copying, which is the reason the
7//! type exists.
8//!
9//! ```cove
10//! let metrics = Shared(Metrics(requests: 0, failures: 0))
11//! metrics.lock(fn(var value) { value.record(failed) })
12//! ```
13//!
14//! There is no `get` and no `set`. Every access is a scoped [`SharedCell::lock`],
15//! so a read-modify-write cannot be written as two operations that race: the
16//! read, the modification, and the write are one call with the lock held for
17//! all three.
18//!
19//! What the cell holds is a [`Transfer`], not a [`Value`]: the wrapped value
20//! must be task-safe, a task-safe value is exactly one a `Transfer` can carry,
21//! and a `Transfer` is the only form two threads can both address. Each `lock`
22//! therefore converts the cell's contents into the locking task's own
23//! [`Value`] and converts back what the closure leaves — the same copy the
24//! task-safety rule already demands at every other boundary.
25//!
26//! # Cycles
27//!
28//! A cell may hold another cell, including itself, and ADR 0011 says plainly
29//! that nothing reclaims such an `Arc` cycle: cells outlive every task that
30//! holds one, so collecting a cycle among them needs a collector that stops
31//! every thread, which the per-task collector rules out by design. The ADR's
32//! amendment picks a narrower, cheaper policy: `lock` rejects the one shape
33//! of cycle it can see for free — the cell ending up holding a handle to
34//! itself — and leaves everything wider as a documented, accepted leak. See
35//! `Transfer::reaches` and `direct_cycle`.
36
37use std::fmt;
38use std::sync::atomic::{AtomicU64, Ordering};
39use std::sync::{Arc, Mutex};
40
41use cove_diag::Span;
42
43use crate::error::RuntimeError;
44use crate::task::{NotTaskSafe, Transfer, TASK_SAFETY_RULE};
45use crate::value::Value;
46
47/// The next tag [`THREAD_TAG`] hands out.
48///
49/// Zero means "no task holds this cell", so tags start at one.
50static NEXT_THREAD_TAG: AtomicU64 = AtomicU64::new(1);
51
52thread_local! {
53    /// A number identifying this thread among the threads that take locks.
54    ///
55    /// A [`std::thread::ThreadId`] cannot be turned into an integer on stable
56    /// Rust, and the holder has to be readable without taking a lock of its
57    /// own, so the runtime hands out its own tags.
58    static THREAD_TAG: u64 = NEXT_THREAD_TAG.fetch_add(1, Ordering::Relaxed);
59}
60
61/// The storage a `Shared<T>` value addresses.
62///
63/// Cloning the [`Arc`] is what "crosses by sharing" means: every task that
64/// received the `Shared` addresses this one cell, and the [`Mutex`] is what
65/// makes their accesses take turns.
66pub struct SharedCell {
67    value: Mutex<Transfer>,
68    /// The tag of the thread currently inside [`SharedCell::lock`], or zero.
69    ///
70    /// Only that thread can write its own tag here, so reading it back is a
71    /// sound test for "this task already holds this cell" — which is a
72    /// deadlock the runtime reports instead of waiting for.
73    holder: AtomicU64,
74}
75
76/// Marks a cell as held for as long as the value is alive, so every path out
77/// of a `lock` — including a closure that raised an error — releases it.
78struct Held<'a> {
79    holder: &'a AtomicU64,
80}
81
82impl<'a> Held<'a> {
83    fn new(holder: &'a AtomicU64, tag: u64) -> Held<'a> {
84        holder.store(tag, Ordering::Release);
85        Held { holder }
86    }
87}
88
89impl Drop for Held<'_> {
90    fn drop(&mut self) {
91        self.holder.store(0, Ordering::Release);
92    }
93}
94
95impl SharedCell {
96    /// Wraps an already converted value.
97    pub fn new(value: Transfer) -> Arc<SharedCell> {
98        Arc::new(SharedCell {
99            value: Mutex::new(value),
100            holder: AtomicU64::new(0),
101        })
102    }
103
104    /// `Shared(value)`: wraps `value`, or reports why it may not be wrapped.
105    ///
106    /// The payload must be task-safe for the same reason a spawned closure's
107    /// captures must be. A `Shared<Vector<T>>` would let a vector be reached
108    /// from two tasks, which is exactly what the sentence naming `Shared`
109    /// forbids.
110    pub fn wrap(value: &Value, span: Span) -> Result<Arc<SharedCell>, RuntimeError> {
111        match Transfer::of(value) {
112            Ok(transfer) => Ok(SharedCell::new(transfer)),
113            Err(found) => Err(cannot_wrap(&found, span)),
114        }
115    }
116
117    /// Runs `body` with the wrapped value, holding the lock for the whole
118    /// call, and stores back whatever `body` leaves in it.
119    ///
120    /// `body` receives the value and returns its own result together with the
121    /// value to store — which is the same value when the closure took it as a
122    /// `var` alias and mutated it in place. A `body` that raises an error
123    /// stores nothing, so a half-finished modification is never left behind
124    /// for another task to find.
125    pub fn lock<R>(
126        &self,
127        span: Span,
128        body: impl FnOnce(Value) -> Result<(R, Value), RuntimeError>,
129    ) -> Result<R, RuntimeError> {
130        let tag = THREAD_TAG.with(|tag| *tag);
131        if self.holder.load(Ordering::Acquire) == tag {
132            return Err(reentrant_lock(span));
133        }
134        // A panic inside a `lock` ends the run that raised it, so a poisoned
135        // cell is not a state anything recovers from. Taking the value back
136        // keeps one task's broken invariant from becoming a second,
137        // unrelated failure in another.
138        let mut guard = self
139            .value
140            .lock()
141            .unwrap_or_else(|poisoned| poisoned.into_inner());
142        // Declared after the guard, so it is dropped before it: the cell stops
143        // being held before another task can acquire it.
144        let _held = Held::new(&self.holder, tag);
145        let (result, updated) = body(guard.clone().into_value())?;
146        let transfer = Transfer::of(&updated).map_err(|found| cannot_store(&found, span))?;
147        // ADR 0011's amendment: a cell holding a handle to itself is an `Arc`
148        // cycle no collector reclaims, and this is the one shape of that
149        // cycle cheap to catch — `transfer` is the value already walked in
150        // full to check task-safety, so asking it whether it names this same
151        // cell costs one more pass over a tree already built, not a new
152        // walk of the heap. A cycle through a second cell is not caught
153        // here; see the ADR for why that stays an accepted, documented leak.
154        if transfer.reaches(self as *const SharedCell) {
155            return Err(direct_cycle(span));
156        }
157        *guard = transfer;
158        Ok(result)
159    }
160}
161
162/// A cell is a handle, and its contents are reachable only under the lock, so
163/// it shows as the handle it is rather than as what it currently holds.
164impl fmt::Debug for SharedCell {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        f.write_str("Shared")
167    }
168}
169
170/// `Shared(value)` where `value` may not cross a task boundary.
171fn cannot_wrap(found: &NotTaskSafe, span: Span) -> RuntimeError {
172    RuntimeError::new(format!(
173        "`Shared` cannot wrap {}, which cannot cross a task boundary",
174        found.subject()
175    ))
176    .at(span)
177    .with_rule(TASK_SAFETY_RULE)
178    .with_help(found.help("wrapping it"))
179}
180
181/// A `lock` whose closure left something behind that may not cross a task
182/// boundary.
183fn cannot_store(found: &NotTaskSafe, span: Span) -> RuntimeError {
184    RuntimeError::new(format!(
185        "`lock` cannot store {}, which cannot cross a task boundary",
186        found.subject()
187    ))
188    .at(span)
189    .with_rule(TASK_SAFETY_RULE)
190    .with_help(found.help("storing it"))
191}
192
193/// A `lock` taken by a task that already holds the same cell.
194///
195/// Waiting would be waiting for itself, so the runtime says so instead of
196/// hanging.
197fn reentrant_lock(span: Span) -> RuntimeError {
198    RuntimeError::new("this task already holds this `Shared`, so `lock` would wait for itself")
199        .at(span)
200        .with_rule(
201            "`lock` holds the value for the whole of the closure it is given, so a `lock` on the same `Shared` inside it can never be granted.",
202        )
203        .with_help("do the whole read-modify-write in one `lock`")
204}
205
206/// The Language Card sentence [`direct_cycle`] quotes.
207///
208/// ADR 0011's amendment names this the one shape of `Shared` cycle worth
209/// rejecting: cheap to detect, because it falls out of the walk `lock`
210/// already does to check task-safety, and cheap to explain, because it is
211/// exactly "do not store a handle to this cell back into itself."
212const SHARED_ACYCLIC_RULE: &str = "`Shared` ownership must stay acyclic. A cell may not come to hold a handle to itself; `lock` rejects a closure that would leave the cell reachable from its own new value. A cycle through two or more cells is not detected and leaks.";
213
214/// A `lock` whose closure left behind a value that reaches the very cell
215/// being locked — `n.lock(fn(var value) { value = Node(cell: Some(n)) })`,
216/// in ADR 0011's own example.
217///
218/// Nothing reclaims an `Arc` cycle among `Shared` cells, so this is refused
219/// at the point of assignment rather than left to leak silently. A cycle
220/// closed through a *second* cell is not caught here — that would mean
221/// locking another cell mid-walk to see what it already holds, which risks
222/// exactly the deadlock `lock` elsewhere guards against — and remains an
223/// accepted, documented leak.
224fn direct_cycle(span: Span) -> RuntimeError {
225    RuntimeError::new(
226        "this `lock` would leave the cell holding a handle to itself, and no collector reclaims that cycle",
227    )
228    .at(span)
229    .with_rule(SHARED_ACYCLIC_RULE)
230    .with_help("keep the reference outside the cell, or restructure so the value never stores a handle back to its own `Shared`")
231}