Skip to main content

bombay_address/
lib.rs

1//! A typed concurrent address space with generation-safe ownership.
2//!
3//! # Address key contracts
4//!
5//! Address keys (`A: Eq + Hash + Clone`) must satisfy these invariants on
6//! every call that passes through the space:
7//!
8//! - **Deterministic.** `Hash` and `Eq` must produce the same output for
9//!   the same input every time they are called.
10//! - **Consistent.** `Hash` and `Eq` must agree: if `a == b`, then
11//!   `a.hash(…)` and `b.hash(…)` must write the same bytes to the
12//!   hasher (the `Hash` trait requirement).
13//! - **No panic.** `Hash` and `Eq` must never panic. A panicking `Hash`
14//!   or `Eq` during `claim`, `resolve`, or `release` may leave the
15//!   address space in an inconsistent state.
16//! - **No re-entrancy.** `Hash` and `Eq` must not call any method of the
17//!   *same* `AddressSpace` from which they were invoked. The hash-table
18//!   probe runs under the table's write or read guard, and
19//!   `parking_lot` locks are not re-entrant: a re-entrant `Hash` or
20//!   `Eq` will deadlock. (Endpoint `Clone` and `Drop` are intentionally
21//!   run outside the lock and can safely re-enter the space.)
22//! - **Identity-preserving `Clone`.** `A: Clone` must preserve `Eq` and
23//!   `Hash` identity: for every address value `a`,
24//!   `a.clone() == a` and `a.clone()` hashes to the same value as `a`.
25//!   The reference documentation calls this the *logical Clone contract*
26//!   for key types.
27
28mod table;
29
30use core::hash::Hash;
31#[cfg(loom)]
32use loom::sync::{Arc, RwLock};
33#[cfg(not(loom))]
34use parking_lot::RwLock;
35#[cfg(not(loom))]
36use std::sync::Arc;
37use std::{fmt, hash::Hasher, num::NonZeroU64};
38
39use table::OpenTable;
40
41/// A failed attempt to claim an address that already has a live owner.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct AddressInUse<A>(pub A);
44
45/// The reason an address claim failed.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum ClaimError<A> {
48    /// An owner is already registered at the returned address.
49    AddressInUse(A),
50    /// This address space has issued every available registration identity.
51    ///
52    /// Exhaustion is permanent: identities are never wrapped or reused.
53    RegistrationIdsExhausted(A),
54}
55
56struct RegistrationScopeMarker {
57    _private: u8,
58}
59
60/// Opaque identity of one address-space scope within the current process.
61///
62/// Clones of an [`AddressSpace`] have the same scope. Independently created
63/// spaces have different scopes. This value is process-local: it is not a
64/// durable identifier, an authentication credential, or registration
65/// authority.
66#[derive(Clone)]
67pub struct RegistrationScopeId(Arc<RegistrationScopeMarker>);
68
69impl PartialEq for RegistrationScopeId {
70    fn eq(&self, other: &Self) -> bool {
71        Arc::ptr_eq(&self.0, &other.0)
72    }
73}
74
75impl Eq for RegistrationScopeId {}
76
77impl core::hash::Hash for RegistrationScopeId {
78    fn hash<H: Hasher>(&self, state: &mut H) {
79        Arc::as_ptr(&self.0).hash(state);
80    }
81}
82
83impl fmt::Debug for RegistrationScopeId {
84    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
85        formatter
86            .debug_tuple("RegistrationScopeId")
87            .field(&Arc::as_ptr(&self.0))
88            .finish()
89    }
90}
91
92/// Opaque identity of one exact registration within the current process.
93///
94/// The identity is independent of the address and endpoint types. It grants no
95/// ownership, resolution, or release authority and is meaningful only for
96/// process-local equality and correlation.
97#[derive(Clone, PartialEq, Eq, Hash)]
98pub struct RegistrationId {
99    scope: RegistrationScopeId,
100    generation: NonZeroU64,
101}
102
103impl RegistrationId {
104    /// Return the address-space scope in which this registration was created.
105    #[must_use]
106    pub fn scope_id(&self) -> &RegistrationScopeId {
107        &self.scope
108    }
109}
110
111impl fmt::Debug for RegistrationId {
112    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
113        formatter
114            .debug_struct("RegistrationId")
115            .field("scope", &self.scope)
116            .field("generation", &self.generation)
117            .finish()
118    }
119}
120
121struct Inner<A, E> {
122    scope: RegistrationScopeId,
123    entries: RwLock<OpenTable<A, E>>,
124}
125
126impl<A, E> Inner<A, E> {
127    #[cfg(loom)]
128    fn read_entries(&self) -> loom::sync::RwLockReadGuard<'_, OpenTable<A, E>> {
129        // loom's RwLock::read() always returns Ok — the lock-acquisition
130        // loop never fails. Unwrap is honest: loom never poisons.
131        self.entries.read().unwrap()
132    }
133
134    #[cfg(loom)]
135    fn write_entries(&self) -> loom::sync::RwLockWriteGuard<'_, OpenTable<A, E>> {
136        self.entries.write().unwrap()
137    }
138
139    #[cfg(not(loom))]
140    fn read_entries(&self) -> parking_lot::RwLockReadGuard<'_, OpenTable<A, E>> {
141        self.entries.read()
142    }
143
144    #[cfg(not(loom))]
145    fn write_entries(&self) -> parking_lot::RwLockWriteGuard<'_, OpenTable<A, E>> {
146        self.entries.write()
147    }
148}
149
150/// A shared mapping from addresses to typed endpoints.
151pub struct AddressSpace<A, E> {
152    inner: Arc<Inner<A, E>>,
153}
154
155impl<A, E> Clone for AddressSpace<A, E> {
156    fn clone(&self) -> Self {
157        Self {
158            inner: self.inner.clone(),
159        }
160    }
161}
162
163impl<A, E> Default for AddressSpace<A, E> {
164    fn default() -> Self {
165        Self::new()
166    }
167}
168
169impl<A, E> AddressSpace<A, E> {
170    /// Construct an empty address space.
171    #[must_use]
172    pub fn new() -> Self {
173        Self {
174            inner: Arc::new(Inner {
175                scope: RegistrationScopeId(Arc::new(RegistrationScopeMarker { _private: 0 })),
176                entries: RwLock::new(OpenTable::new()),
177            }),
178        }
179    }
180
181    /// Return this address space's opaque, process-local registration scope.
182    #[must_use]
183    pub fn registration_scope_id(&self) -> RegistrationScopeId {
184        self.inner.scope.clone()
185    }
186
187    /// Return the number of live registrations.
188    #[must_use]
189    pub fn len(&self) -> usize {
190        self.inner.read_entries().len()
191    }
192
193    /// Return whether the address space is empty.
194    #[must_use]
195    pub fn is_empty(&self) -> bool {
196        self.len() == 0
197    }
198}
199
200impl<A: Eq + Hash, E> AddressSpace<A, E> {
201    /// Reclaim excess backing-table capacity (rehash the map into a
202    /// smaller allocation where the implementation can do so). This may
203    /// block concurrent address-space operations while it runs.
204    ///
205    /// Preserves every live registration, endpoint, and generation. Safe
206    /// on a new or already-empty space; idempotent; may be called while
207    /// registrations are live. Ordinary `Lease::release` never shrinks
208    /// automatically.
209    pub fn shrink_to_fit(&self) {
210        self.inner.write_entries().shrink_to_fit();
211    }
212}
213
214impl<A, E> AddressSpace<A, E>
215where
216    A: Eq + Hash + Clone,
217{
218    /// Exclusively claim `address` for `endpoint`.
219    ///
220    /// # Errors
221    /// Returns [`AddressInUse`] when an owner is already registered.
222    ///
223    /// # Panics
224    /// Panics after this address space has issued all `u64::MAX` registration
225    /// identities. Use [`AddressSpace::try_claim`] to handle this physically
226    /// unreachable boundary without panicking. Identities never wrap or reuse.
227    ///
228    /// # Key contracts
229    ///
230    /// The address key is `Clone`d before the write guard is taken, so
231    /// caller `Clone` code never runs under the lock. The clone must
232    /// preserve `Eq`/`Hash` identity (see [module-level
233    /// docs](index.html#address-key-contracts)). The original key is
234    /// given to the returned [`Lease`]; the clone is stored in the table.
235    pub fn claim(&self, address: A, endpoint: E) -> Result<Lease<A, E>, AddressInUse<A>> {
236        match self.try_claim(address, endpoint) {
237            Ok(lease) => Ok(lease),
238            Err(ClaimError::AddressInUse(address)) => Err(AddressInUse(address)),
239            Err(ClaimError::RegistrationIdsExhausted(_)) => {
240                panic!("registration identities exhausted")
241            }
242        }
243    }
244
245    /// Exclusively claim `address`, including explicit identity-exhaustion
246    /// handling.
247    ///
248    /// This is equivalent to [`AddressSpace::claim`] during ordinary
249    /// operation. Unlike `claim`, it returns
250    /// [`ClaimError::RegistrationIdsExhausted`] after the final identity has
251    /// been issued. Exhaustion is permanent.
252    ///
253    /// # Errors
254    /// Returns [`ClaimError::AddressInUse`] when an owner is already
255    /// registered. Returns [`ClaimError::RegistrationIdsExhausted`] after the
256    /// address space has issued all `u64::MAX` registration identities.
257    pub fn try_claim(&self, address: A, endpoint: E) -> Result<Lease<A, E>, ClaimError<A>> {
258        // The key copy for the table happens before the write guard is
259        // taken: caller `A: Clone` code must not run under the lock.
260        let key = address.clone();
261        let mut entries = self.inner.write_entries();
262        if entries.get(&address).is_some() {
263            return Err(ClaimError::AddressInUse(address));
264        }
265        let Some(generation) = entries.take_generation() else {
266            return Err(ClaimError::RegistrationIdsExhausted(address));
267        };
268        entries.insert(key, generation.get(), endpoint);
269        Ok(Lease {
270            inner: self.inner.clone(),
271            address,
272            generation,
273            released: false,
274        })
275    }
276}
277
278impl<A, E> AddressSpace<A, E>
279where
280    A: Eq + Hash,
281    E: Clone,
282{
283    /// Resolve a snapshot of the endpoint currently registered at `address`.
284    ///
285    /// The endpoint's `Clone` runs after the read guard is dropped: taking
286    /// the shared handle under the guard is refcount arithmetic only, so a
287    /// re-entrant `Clone` that claims or releases in the address space
288    /// cannot deadlock against the lock.
289    #[must_use]
290    pub fn resolve(&self, address: &A) -> Option<E> {
291        let endpoint = {
292            let guard = self.inner.read_entries();
293            guard.get(address).map(|entry| Arc::clone(&entry.endpoint))
294        };
295        endpoint.as_deref().cloned()
296    }
297}
298
299/// Exclusive ownership of one exact address registration generation.
300pub struct Lease<A, E>
301where
302    A: Eq + Hash,
303{
304    inner: Arc<Inner<A, E>>,
305    address: A,
306    generation: NonZeroU64,
307    released: bool,
308}
309
310impl<A, E> Lease<A, E>
311where
312    A: Eq + Hash,
313{
314    /// Inspect the owned address.
315    #[must_use]
316    pub fn address(&self) -> &A {
317        &self.address
318    }
319
320    /// Return the opaque, process-local identity of this exact registration.
321    ///
322    /// The returned value grants no ownership or release authority and does
323    /// not keep the registration or endpoint alive.
324    #[must_use]
325    pub fn registration_id(&self) -> RegistrationId {
326        RegistrationId {
327            scope: self.inner.scope.clone(),
328            generation: self.generation,
329        }
330    }
331
332    /// Release this registration immediately.
333    ///
334    /// # Key contracts
335    ///
336    /// The address key is re-hashed during the table removal. The key's
337    /// `Hash` and `Eq` must not panic; a panicking hash or equality
338    /// comparison during release may permanently leave the registration
339    /// in place (the lease is consumed, but the table entry is never
340    /// removed).
341    pub fn release(mut self) {
342        self.release_inner();
343    }
344
345    fn release_inner(&mut self) {
346        if self.released {
347            return;
348        }
349        self.released = true;
350        // The removed address and endpoint handle are dropped after the
351        // write guard is released, so re-entrant `Drop` implementations
352        // cannot deadlock against the lock.
353        let removed = {
354            let mut entries = self.inner.write_entries();
355            entries.remove_if(&self.address, self.generation.get())
356        };
357        drop(removed);
358    }
359}
360
361impl<A, E> Drop for Lease<A, E>
362where
363    A: Eq + Hash,
364{
365    fn drop(&mut self) {
366        self.release_inner();
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use super::{AddressSpace, ClaimError};
373    use std::collections::HashSet;
374
375    #[test]
376    fn lease_owns_and_releases_one_registration() {
377        let space = AddressSpace::new();
378        let lease = space.claim(7, "first").unwrap();
379        assert_eq!(space.resolve(&7), Some("first"));
380        assert!(matches!(
381            space.try_claim(7, "second"),
382            Err(ClaimError::AddressInUse(7))
383        ));
384        drop(lease);
385        assert_eq!(space.resolve(&7), None);
386        let replacement = space.claim(7, "second").unwrap();
387        assert_eq!(space.resolve(&7), Some("second"));
388        drop(replacement);
389    }
390
391    #[test]
392    fn registration_identity_distinguishes_replacements() {
393        let space = AddressSpace::new();
394        let first = space.claim("worker", 1).unwrap();
395        let first_id = first.registration_id();
396        first.release();
397
398        let second = space.claim("worker", 2).unwrap();
399        let second_id = second.registration_id();
400        assert_ne!(first_id, second_id);
401        assert_eq!(first_id.scope_id(), second_id.scope_id());
402
403        let ids = HashSet::from([first_id, second_id]);
404        assert_eq!(ids.len(), 2);
405    }
406
407    #[test]
408    fn registration_identity_is_scoped_to_the_address_space() {
409        let first = AddressSpace::new();
410        let first_peer = first.clone();
411        let second = AddressSpace::new();
412
413        assert_eq!(
414            first.registration_scope_id(),
415            first_peer.registration_scope_id()
416        );
417        assert_ne!(
418            first.registration_scope_id(),
419            second.registration_scope_id()
420        );
421
422        let first_id = first.claim((1_u8, 2_u8), ()).unwrap().registration_id();
423        let second_id = second.claim((1_u8, 2_u8), ()).unwrap().registration_id();
424        assert_ne!(first_id, second_id);
425    }
426
427    #[test]
428    fn retaining_an_identity_grants_no_registration_lifetime() {
429        let space = AddressSpace::new();
430        let lease = space
431            .claim(String::from("worker"), String::from("mailbox"))
432            .unwrap();
433        let identity = lease.registration_id();
434        drop(lease);
435
436        assert_eq!(space.resolve(&String::from("worker")), None);
437        let replacement = space
438            .claim(String::from("worker"), String::from("replacement"))
439            .unwrap();
440        assert_ne!(identity, replacement.registration_id());
441    }
442
443    #[test]
444    fn exhausted_space_rejects_claims_without_reusing_an_identity() {
445        let space = AddressSpace::new();
446        space.inner.write_entries().set_next_generation(u64::MAX);
447
448        let last = space.claim(1_u64, "last").unwrap();
449        let last_id = last.registration_id();
450        last.release();
451
452        assert!(matches!(
453            space.try_claim(2, "never inserted"),
454            Err(ClaimError::RegistrationIdsExhausted(2))
455        ));
456        assert!(space.is_empty());
457        assert_eq!(last_id.scope_id(), &space.registration_scope_id());
458    }
459}