Skip to main content

sefer_region/
sync_region.rs

1//! [`SyncRegion`] — the safe concurrent default: a `Region` behind an `RwLock`.
2
3use std::sync::{PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard};
4
5use crate::{Handle, Region};
6
7/// A thread-safe wrapper around [`Region<T>`] — the trusted concurrent baseline.
8///
9/// This is a coarse-grained `std::sync::RwLock<Region<T>>` with an ergonomic
10/// guard-based API: multiple readers (`read`) or one writer (`write`) at a time.
11/// It is the *always-shippable* concurrent answer: correct under any interleaving
12/// because every mutation serialises through the lock. Lock-free tiers
13/// (Phase 3b) are a later opt-in upgrade for read-mostly hot paths; until those
14/// land and clear loom/TSan, this is the default for shared mutable regions.
15///
16/// The wrapper stays `#![forbid(unsafe_code)]`: all interior mutability comes
17/// from `std`'s `RwLock`. Use [`read`](Self::read) / [`write`](Self::write) for
18/// multi-operation transactions (the borrows tie to the guard), or the
19/// one-shot convenience methods ([`insert`](Self::insert),
20/// [`remove`](Self::remove), …) which take `&self` and lock internally.
21///
22/// ## Poisoning policy
23///
24/// A panic while a guard is held poisons the `RwLock`. A poisoned `Region` is
25/// still structurally valid — no broken memory invariants: `slotmap` keeps the
26/// dense store generational and consistent regardless of a panicked op, so we
27/// **recover from poison** rather than propagate it. Every accessor uses
28/// `RwLockReadGuard`/`RwLockWriteGuard` recovery (`PoisonError::into_inner`),
29/// handing back the intact inner `Region` and letting callers continue. This
30/// keeps a panic in one thread from bricking the region for all others.
31pub struct SyncRegion<T> {
32    inner: RwLock<Region<T>>,
33}
34
35impl<T> SyncRegion<T> {
36    /// Creates an empty region that allocates nothing until first use.
37    #[must_use]
38    pub fn new() -> Self {
39        Self {
40            inner: RwLock::new(Region::new()),
41        }
42    }
43
44    /// Creates an empty region with space pre-reserved for `capacity` entries.
45    #[must_use]
46    pub fn with_capacity(capacity: usize) -> Self {
47        Self {
48            inner: RwLock::new(Region::with_capacity(capacity)),
49        }
50    }
51
52    /// Locks for shared read, returning a guard that hands out `&Region<T>`.
53    ///
54    /// Multiple readers may hold the guard concurrently. Recovers from poison
55    /// (see the [poisoning policy](Self#poisoning-policy)).
56    pub fn read(&self) -> RwLockReadGuard<'_, Region<T>> {
57        self.inner.read().unwrap_or_else(PoisonError::into_inner)
58    }
59
60    /// Locks for exclusive write, returning a guard that hands out `&mut Region<T>`.
61    ///
62    /// Blocks all other readers and writers until dropped. Recovers from poison
63    /// (see the [poisoning policy](Self#poisoning-policy)).
64    pub fn write(&self) -> RwLockWriteGuard<'_, Region<T>> {
65        self.inner.write().unwrap_or_else(PoisonError::into_inner)
66    }
67
68    /// Inserts `value`, returning a fresh handle that resolves to it (I1).
69    ///
70    /// One-shot convenience that locks for write internally. For a transaction
71    /// that does several ops under one lock, use [`write`](Self::write) instead.
72    pub fn insert(&self, value: T) -> Handle<T> {
73        self.write().insert(value)
74    }
75
76    /// Removes and returns the value for `handle`, or `None` if stale/removed.
77    ///
78    /// One-shot convenience that locks for write internally.
79    pub fn remove(&self, handle: Handle<T>) -> Option<T> {
80        self.write().remove(handle)
81    }
82
83    /// Whether `handle` currently resolves to a live value.
84    ///
85    /// One-shot convenience that locks for read internally.
86    #[must_use]
87    pub fn contains(&self, handle: Handle<T>) -> bool {
88        self.read().contains(handle)
89    }
90
91    /// Number of live values (I4).
92    ///
93    /// One-shot convenience that locks for read internally. Note that under
94    /// concurrency the count is a momentary snapshot, not a stable property.
95    #[must_use]
96    pub fn len(&self) -> usize {
97        self.read().len()
98    }
99
100    /// Whether the region holds no live values (I4).
101    #[must_use]
102    pub fn is_empty(&self) -> bool {
103        self.read().is_empty()
104    }
105
106    /// Removes every value, invalidating all outstanding handles.
107    ///
108    /// One-shot convenience that locks for write internally.
109    pub fn clear(&self) {
110        self.write().clear();
111    }
112
113    /// Clones the value for `handle` out without holding a guard, or `None` if
114    /// stale/removed. One-shot convenience that locks for read internally.
115    ///
116    /// Prefer this over [`read`](Self::read) when you only need a by-value copy
117    /// and don't want to hold the guard across other work.
118    pub fn get_cloned(&self, handle: Handle<T>) -> Option<T>
119    where
120        T: Clone,
121    {
122        self.read().get(handle).cloned()
123    }
124}
125
126impl<T> Default for SyncRegion<T> {
127    fn default() -> Self {
128        Self::new()
129    }
130}