1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
//! [`SyncRegion`] — the safe concurrent default: a `Region` behind an `RwLock`.
use ;
use crate::;
/// A thread-safe wrapper around [`Region<T>`] — the trusted concurrent baseline.
///
/// This is a coarse-grained `std::sync::RwLock<Region<T>>` with an ergonomic
/// guard-based API: multiple readers (`read`) or one writer (`write`) at a time.
/// It is the *always-shippable* concurrent answer: correct under any interleaving
/// because every mutation serialises through the lock. Note: "correct under any
/// interleaving" refers to **safety/serialization** — memory safety and API
/// contract invariants — not to bounded writer latency. `std::sync::RwLock` does
/// not guarantee portable fairness or starvation-freedom; on some platforms a new
/// reader may be delayed behind an awaiting writer depending on OS scheduling.
/// Finer-grained or lock-free alternatives are out of scope for this crate.
///
/// The wrapper stays `#![forbid(unsafe_code)]`: all interior mutability comes
/// from `std`'s `RwLock`. Use [`read`](Self::read) / [`write`](Self::write) for
/// multi-operation transactions (the borrows tie to the guard), or the
/// one-shot convenience methods ([`insert`](Self::insert),
/// [`remove`](Self::remove), …) which take `&self` and lock internally.
/// `reserve` and `capacity` have no one-shot form — reach them through a
/// held guard, e.g. `sr.write().reserve(n)` / `sr.read().capacity()`.
///
/// ## Poisoning policy
///
/// A panic while the **write** guard is held poisons the `RwLock` — `std` never
/// poisons on a read-guard panic (e.g. a panicking `T::clone` inside
/// [`get_cloned`](Self::get_cloned) releases the read lock cleanly, with no
/// poison). The **container structure** (Region/slotmap invariants) stays
/// intact regardless of the panic — this crate guarantees no memory
/// corruption. However, **interior side effects are the responsibility of `T`**:
/// if `T::clone` modifies internal state (e.g. `Cell`, atomics, internal `Mutex`)
/// before panicking, that modification persists. The `RwLock` itself does not
/// poison on read-guard panic, so the `SyncRegion` remains usable.
///
/// **Poison recovery guarantees container integrity only, not operation completion.**
/// The recovered `Region` has no memory corruption, but an interrupted operation
/// may have left partial effects visible: a panicking `T::Drop` during `clear()`
/// leaves the region partially cleared -- container-valid and reusable, but the
/// exact set of surviving values is an unspecified implementation detail of the
/// underlying `slotmap` version's unwind cleanup, not a stable contract (see
/// `Region::clear`'s own documentation), and a panicked multi-op `write()`
/// transaction leaves whatever partial effects it already applied. Callers whose
/// `T` carries cross-value invariants, or whose multi-op transactions need
/// all-or-nothing semantics, must implement their own signaling — this crate
/// provides none beyond what's documented here.
///
/// **Poison is cleared on recovery.** [`read`](Self::read) and
/// [`write`](Self::write) (and therefore every one-shot convenience method,
/// which locks internally) clear the lock's poison flag immediately after
/// recovering from it. This is a deliberate policy consistent with "poison
/// recovery guarantees container integrity only" above: since this crate
/// already always trusts the container after ANY recovery, permanently
/// forcing every subsequent access down `std::sync::RwLock`'s slower
/// poisoned-recovery path would cost real performance for no additional
/// safety — the container was already proven sound on the FIRST recovery.
/// One consequence: `SyncRegion` never exposes an `is_poisoned()` check,
/// because poisoned state is never observable for longer than the single
/// access that first recovers from it (except through [`Debug`], which
/// reports it without clearing). Applications that need a durable,
/// observable "a writer panicked here" signal for their own cross-value
/// invariants must implement it themselves (e.g. an `AtomicBool` alongside
/// the `SyncRegion`) — this crate's own poison flag is not a substitute,
/// by design.
///
/// ## Reentrancy
///
/// [`get_cloned`](Self::get_cloned) runs `T::clone`, and [`clear`](Self::clear) runs each
/// `T::Drop`, while the internal lock is held. If `T`'s `Clone` or `Drop` implementation
/// re-enters the same `SyncRegion` (directly or transitively), the thread deadlocks or
/// panics per `std::sync::RwLock`'s documented same-thread reacquisition behavior.
/// Even non-reentrant but slow `Clone`/`Drop` delays every other user: `clear` holds
/// the write lock across its entire linear sweep, while `get_cloned` holds the read lock
/// across the clone (readers are unaffected by the latter, but writers block). Never
/// call a one-shot convenience method (or a nested `read`/`write`) while the calling
/// thread already holds a read/write guard from the same `SyncRegion` — the one-shots
/// lock internally and the nested acquisition deadlocks (`std`'s `RwLock` is not reentrant;
/// even read-after-read can block behind a queued writer, since the platform's priority
/// policy is unspecified).
///
/// ## Contended reads
///
/// Under multi-threaded read contention, the one-shot convenience methods
/// ([`get_cloned`](Self::get_cloned), [`contains`](Self::contains),
/// [`len`](Self::len), [`is_empty`](Self::is_empty)) anti-scale: each call pays a
/// shared-cache-line lock acquisition that dominates the nanosecond-scale lookup.
/// Historical measurement (harness: `examples/contended_reads.rs`, regime: 8
/// readers, one-shot vs. batched reads on a noisy dev host) showed a ~4×
/// aggregate throughput loss at 8 readers and ~30× speedup from batching. A
/// later, more rigorous gate (`docs/perf/R828_STRUCTURAL_LEVERS_GATE.md` §2)
/// measured the same question with a different harness (`r828_batch_guard_probe.rs`)
/// and found **9.15×** — explicitly recorded as an open discrepancy, not silently
/// reconciled. The numbers above are retained for historical context; do not
/// treat them as a stable measurement of batching's benefit without consulting
/// the R828 gate's analysis.
///
/// ## Async runtimes
///
/// `SyncRegion` uses blocking `std::sync::RwLock`, which is not async-aware.
/// In an async context (e.g. `tokio`), this has concrete hazards:
///
/// - **Holding a guard across `.await` blocks the executor worker** for the
/// entire await duration, not just the critical section. The worker cannot
/// schedule other tasks while blocked.
///
/// - **One-shot methods (`get_cloned`, `insert`, `remove`, …) synchronously
/// block the OS thread** — they are not "async-safe just because they're
/// fast." Contention can still stall an executor worker.
///
/// - **`tokio::time::timeout` does NOT cancel a blocking lock acquisition.**
/// The timeout fires after the operation completes (or never fires if the
/// lock is held forever), but the blocking wait itself cannot be
/// interrupted.
///
/// - **`spawn_blocking` does NOT make an already-started operation
/// cancellation-safe.** Once a blocking call is in flight on a worker thread,
/// dropping the `JoinHandle` does not abort it.
///
/// For async-friendly ownership, use an async lock type such as
/// `tokio::sync::RwLock` or `async_rwlock` instead of this wrapper.
/// Guard-batching (see the contended reads section above) is valid only within
/// a synchronous section — it does not make blocking safe in async code.