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}