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 read-only capability for one resolved endpoint snapshot.
42///
43/// The snapshot remains valid after its registration is released or its
44/// address is reused. Its storage and reclamation mechanism are intentionally
45/// opaque; consumers can access the endpoint through [`core::ops::Deref`] or
46/// [`AsRef`] but cannot reconstruct registration authority from it.
47///
48/// Resolved endpoints are read-only capabilities. The wrapper does not grant
49/// mutable access, construction, destructuring, or registration authority.
50/// As with any shared Rust reference, an endpoint may still expose deliberate
51/// interior-mutability operations through `&self`.
52///
53/// ```compile_fail
54/// use bombay_address::AddressSpace;
55///
56/// let space = AddressSpace::new();
57/// let _lease = space.claim("worker", String::from("endpoint")).unwrap();
58/// let mut endpoint = space.resolve(&"worker").unwrap();
59/// endpoint.push_str("-mutated");
60/// ```
61///
62/// Its private field prevents consumers from constructing or destructuring it:
63///
64/// ```compile_fail
65/// use bombay_address::Resolved;
66///
67/// let endpoint = Resolved(String::from("forged"));
68/// let Resolved(inner) = endpoint;
69/// ```
70///
71/// The endpoint cannot be moved out through the shared dereference:
72///
73/// ```compile_fail
74/// use bombay_address::AddressSpace;
75///
76/// let space = AddressSpace::new();
77/// let _lease = space.claim("worker", String::from("endpoint")).unwrap();
78/// let endpoint = space.resolve(&"worker").unwrap();
79/// let inner: String = *endpoint;
80/// ```
81///
82/// Resolution does not confer release authority:
83///
84/// ```compile_fail
85/// use bombay_address::AddressSpace;
86///
87/// let space = AddressSpace::new();
88/// let _lease = space.claim("worker", String::from("endpoint")).unwrap();
89/// let endpoint = space.resolve(&"worker").unwrap();
90/// endpoint.release();
91/// ```
92///
93/// `Send` and `Sync` are inherited from `E`; the wrapper does not manufacture
94/// either property for an endpoint that lacks it:
95///
96/// ```compile_fail
97/// use bombay_address::Resolved;
98/// use std::rc::Rc;
99///
100/// fn assert_send<T: Send>() {}
101/// assert_send::<Resolved<Rc<()>>>();
102/// ```
103///
104/// ```compile_fail
105/// use bombay_address::Resolved;
106/// use std::cell::Cell;
107///
108/// fn assert_sync<T: Sync>() {}
109/// assert_sync::<Resolved<Cell<()>>>();
110/// ```
111pub struct Resolved<E>(E);
112
113impl<E: Clone> Clone for Resolved<E> {
114    fn clone(&self) -> Self {
115        Self(self.0.clone())
116    }
117}
118
119impl<E> core::ops::Deref for Resolved<E> {
120    type Target = E;
121
122    fn deref(&self) -> &Self::Target {
123        &self.0
124    }
125}
126
127impl<E> AsRef<E> for Resolved<E> {
128    fn as_ref(&self) -> &E {
129        self
130    }
131}
132
133impl<E: fmt::Debug> fmt::Debug for Resolved<E> {
134    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
135        formatter.debug_tuple("Resolved").field(&self.0).finish()
136    }
137}
138
139impl<E: PartialEq> PartialEq for Resolved<E> {
140    fn eq(&self, other: &Self) -> bool {
141        self.as_ref() == other.as_ref()
142    }
143}
144
145impl<E: PartialEq> PartialEq<E> for Resolved<E> {
146    fn eq(&self, other: &E) -> bool {
147        self.as_ref() == other
148    }
149}
150
151impl<E: Eq> Eq for Resolved<E> {}
152
153/// A failed attempt to claim an address that already has a live owner.
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub struct AddressInUse<A>(pub A);
156
157/// The reason an address claim failed.
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub enum ClaimError<A> {
160    /// An owner is already registered at the returned address.
161    AddressInUse(A),
162    /// This address space has issued every available registration identity.
163    ///
164    /// Exhaustion is permanent: identities are never wrapped or reused.
165    RegistrationIdsExhausted(A),
166}
167
168struct RegistrationScopeMarker {
169    _private: u8,
170}
171
172/// Opaque identity of one address-space scope within the current process.
173///
174/// Clones of an [`AddressSpace`] have the same scope. Independently created
175/// spaces have different scopes. This value is process-local: it is not a
176/// durable identifier, an authentication credential, or registration
177/// authority.
178#[derive(Clone)]
179pub struct RegistrationScopeId(Arc<RegistrationScopeMarker>);
180
181impl PartialEq for RegistrationScopeId {
182    fn eq(&self, other: &Self) -> bool {
183        Arc::ptr_eq(&self.0, &other.0)
184    }
185}
186
187impl Eq for RegistrationScopeId {}
188
189impl core::hash::Hash for RegistrationScopeId {
190    fn hash<H: Hasher>(&self, state: &mut H) {
191        Arc::as_ptr(&self.0).hash(state);
192    }
193}
194
195impl fmt::Debug for RegistrationScopeId {
196    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
197        formatter
198            .debug_tuple("RegistrationScopeId")
199            .field(&Arc::as_ptr(&self.0))
200            .finish()
201    }
202}
203
204/// Opaque identity of one exact registration within the current process.
205///
206/// The identity is independent of the address and endpoint types. It grants no
207/// ownership, resolution, or release authority and is meaningful only for
208/// process-local equality and correlation.
209#[derive(Clone, PartialEq, Eq, Hash)]
210pub struct RegistrationId {
211    scope: RegistrationScopeId,
212    generation: NonZeroU64,
213}
214
215impl RegistrationId {
216    /// Return the address-space scope in which this registration was created.
217    #[must_use]
218    pub fn scope_id(&self) -> &RegistrationScopeId {
219        &self.scope
220    }
221}
222
223impl fmt::Debug for RegistrationId {
224    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
225        formatter
226            .debug_struct("RegistrationId")
227            .field("scope", &self.scope)
228            .field("generation", &self.generation)
229            .finish()
230    }
231}
232
233struct Inner<A, E> {
234    scope: RegistrationScopeId,
235    entries: RwLock<OpenTable<A, E>>,
236}
237
238impl<A, E> Inner<A, E> {
239    #[cfg(loom)]
240    fn read_entries(&self) -> loom::sync::RwLockReadGuard<'_, OpenTable<A, E>> {
241        // loom's RwLock::read() always returns Ok — the lock-acquisition
242        // loop never fails. Unwrap is honest: loom never poisons.
243        self.entries.read().unwrap()
244    }
245
246    #[cfg(loom)]
247    fn write_entries(&self) -> loom::sync::RwLockWriteGuard<'_, OpenTable<A, E>> {
248        self.entries.write().unwrap()
249    }
250
251    #[cfg(not(loom))]
252    fn read_entries(&self) -> parking_lot::RwLockReadGuard<'_, OpenTable<A, E>> {
253        self.entries.read()
254    }
255
256    #[cfg(not(loom))]
257    fn write_entries(&self) -> parking_lot::RwLockWriteGuard<'_, OpenTable<A, E>> {
258        self.entries.write()
259    }
260}
261
262/// A shared mapping from addresses to typed endpoints.
263pub struct AddressSpace<A, E> {
264    inner: Arc<Inner<A, E>>,
265}
266
267impl<A, E> Clone for AddressSpace<A, E> {
268    fn clone(&self) -> Self {
269        Self {
270            inner: self.inner.clone(),
271        }
272    }
273}
274
275impl<A, E> Default for AddressSpace<A, E> {
276    fn default() -> Self {
277        Self::new()
278    }
279}
280
281impl<A, E> AddressSpace<A, E> {
282    /// Construct an empty address space.
283    #[must_use]
284    pub fn new() -> Self {
285        Self {
286            inner: Arc::new(Inner {
287                scope: RegistrationScopeId(Arc::new(RegistrationScopeMarker { _private: 0 })),
288                entries: RwLock::new(OpenTable::new()),
289            }),
290        }
291    }
292
293    /// Return this address space's opaque, process-local registration scope.
294    #[must_use]
295    pub fn registration_scope_id(&self) -> RegistrationScopeId {
296        self.inner.scope.clone()
297    }
298
299    /// Return the number of live registrations.
300    #[must_use]
301    pub fn len(&self) -> usize {
302        self.inner.read_entries().len()
303    }
304
305    /// Return whether the address space is empty.
306    #[must_use]
307    pub fn is_empty(&self) -> bool {
308        self.len() == 0
309    }
310}
311
312impl<A: Eq + Hash, E> AddressSpace<A, E> {
313    /// Reclaim excess backing-table capacity (rehash the map into a
314    /// smaller allocation where the implementation can do so). This may
315    /// block concurrent address-space operations while it runs.
316    ///
317    /// Preserves every live registration, endpoint, and generation. Safe
318    /// on a new or already-empty space; idempotent; may be called while
319    /// registrations are live. Ordinary `Lease::release` never shrinks
320    /// automatically.
321    pub fn shrink_to_fit(&self) {
322        self.inner.write_entries().shrink_to_fit();
323    }
324}
325
326impl<A, E> AddressSpace<A, E>
327where
328    A: Eq + Hash + Clone,
329{
330    /// Exclusively claim `address` for `endpoint`.
331    ///
332    /// # Errors
333    /// Returns [`AddressInUse`] when an owner is already registered.
334    ///
335    /// # Panics
336    /// Panics after this address space has issued all `u64::MAX` registration
337    /// identities. Use [`AddressSpace::try_claim`] to handle this physically
338    /// unreachable boundary without panicking. Identities never wrap or reuse.
339    ///
340    /// # Key contracts
341    ///
342    /// The address key is `Clone`d before the write guard is taken, so
343    /// caller `Clone` code never runs under the lock. The clone must
344    /// preserve `Eq`/`Hash` identity (see [module-level
345    /// docs](index.html#address-key-contracts)). The original key is
346    /// given to the returned [`Lease`]; the clone is stored in the table.
347    pub fn claim(&self, address: A, endpoint: E) -> Result<Lease<A, E>, AddressInUse<A>> {
348        match self.try_claim(address, endpoint) {
349            Ok(lease) => Ok(lease),
350            Err(ClaimError::AddressInUse(address)) => Err(AddressInUse(address)),
351            Err(ClaimError::RegistrationIdsExhausted(_)) => {
352                panic!("registration identities exhausted")
353            }
354        }
355    }
356
357    /// Exclusively claim `address`, including explicit identity-exhaustion
358    /// handling.
359    ///
360    /// This is equivalent to [`AddressSpace::claim`] during ordinary
361    /// operation. Unlike `claim`, it returns
362    /// [`ClaimError::RegistrationIdsExhausted`] after the final identity has
363    /// been issued. Exhaustion is permanent.
364    ///
365    /// # Errors
366    /// Returns [`ClaimError::AddressInUse`] when an owner is already
367    /// registered. Returns [`ClaimError::RegistrationIdsExhausted`] after the
368    /// address space has issued all `u64::MAX` registration identities.
369    pub fn try_claim(&self, address: A, endpoint: E) -> Result<Lease<A, E>, ClaimError<A>> {
370        // The key copy for the table happens before the write guard is
371        // taken: caller `A: Clone` code must not run under the lock.
372        let key = address.clone();
373        let mut entries = self.inner.write_entries();
374        if entries.get(&address).is_some() {
375            return Err(ClaimError::AddressInUse(address));
376        }
377        let Some(generation) = entries.take_generation() else {
378            return Err(ClaimError::RegistrationIdsExhausted(address));
379        };
380        entries.insert(key, generation.get(), endpoint);
381        Ok(Lease {
382            inner: self.inner.clone(),
383            address,
384            generation,
385            released: false,
386        })
387    }
388}
389
390impl<A, E> AddressSpace<A, E>
391where
392    A: Eq + Hash,
393    E: Clone,
394{
395    /// Resolve an opaque snapshot of the endpoint currently registered at
396    /// `address`.
397    ///
398    /// The returned handle is an exact snapshot of the resolved registration:
399    /// it remains valid after the lease is released or the address is reused.
400    /// Capturing the registered endpoint handle while holding the read guard
401    /// runs no caller code. The endpoint's [`Clone`] implementation then
402    /// defines the snapshot outside the table lock, so custom clone and drop
403    /// code may safely re-enter the address space.
404    #[must_use]
405    pub fn resolve(&self, address: &A) -> Option<Resolved<E>> {
406        let guard = self.inner.read_entries();
407        let endpoint = guard.get(address).map(|entry| Arc::clone(&entry.endpoint));
408        drop(guard);
409        endpoint.as_deref().cloned().map(Resolved)
410    }
411}
412
413/// Exclusive ownership of one exact address registration generation.
414pub struct Lease<A, E>
415where
416    A: Eq + Hash,
417{
418    inner: Arc<Inner<A, E>>,
419    address: A,
420    generation: NonZeroU64,
421    released: bool,
422}
423
424impl<A, E> Lease<A, E>
425where
426    A: Eq + Hash,
427{
428    /// Inspect the owned address.
429    #[must_use]
430    pub fn address(&self) -> &A {
431        &self.address
432    }
433
434    /// Return the opaque, process-local identity of this exact registration.
435    ///
436    /// The returned value grants no ownership or release authority and does
437    /// not keep the registration or endpoint alive.
438    #[must_use]
439    pub fn registration_id(&self) -> RegistrationId {
440        RegistrationId {
441            scope: self.inner.scope.clone(),
442            generation: self.generation,
443        }
444    }
445
446    /// Release this registration immediately.
447    ///
448    /// # Key contracts
449    ///
450    /// The address key is re-hashed during the table removal. The key's
451    /// `Hash` and `Eq` must not panic; a panicking hash or equality
452    /// comparison during release may permanently leave the registration
453    /// in place (the lease is consumed, but the table entry is never
454    /// removed).
455    pub fn release(mut self) {
456        self.release_inner();
457    }
458
459    fn release_inner(&mut self) {
460        if self.released {
461            return;
462        }
463        self.released = true;
464        // The removed address and endpoint handle are dropped after the
465        // write guard is released, so re-entrant `Drop` implementations
466        // cannot deadlock against the lock.
467        let removed = {
468            let mut entries = self.inner.write_entries();
469            entries.remove_if(&self.address, self.generation.get())
470        };
471        drop(removed);
472    }
473}
474
475impl<A, E> Drop for Lease<A, E>
476where
477    A: Eq + Hash,
478{
479    fn drop(&mut self) {
480        self.release_inner();
481    }
482}
483
484#[cfg(test)]
485mod tests {
486    use super::{AddressSpace, ClaimError, Resolved};
487    use std::collections::HashSet;
488
489    fn assert_send_sync<T: Send + Sync>() {}
490
491    #[test]
492    fn resolved_inherits_positive_auto_traits_from_endpoint() {
493        assert_send_sync::<Resolved<String>>();
494    }
495
496    #[test]
497    fn resolved_preserves_explicit_endpoint_interior_mutability() {
498        use std::sync::Arc;
499        use std::sync::atomic::{AtomicUsize, Ordering};
500
501        let state = Arc::new(AtomicUsize::new(1));
502        let space = AddressSpace::new();
503        let _lease = space.claim("worker", Arc::clone(&state)).unwrap();
504        let endpoint = space.resolve(&"worker").unwrap();
505
506        endpoint.store(2, Ordering::SeqCst);
507        assert_eq!(state.load(Ordering::SeqCst), 2);
508    }
509
510    #[test]
511    fn lease_owns_and_releases_one_registration() {
512        let space = AddressSpace::new();
513        let lease = space.claim(7, "first").unwrap();
514        assert_eq!(space.resolve(&7).as_deref().copied(), Some("first"));
515        assert!(matches!(
516            space.try_claim(7, "second"),
517            Err(ClaimError::AddressInUse(7))
518        ));
519        drop(lease);
520        assert_eq!(space.resolve(&7), None);
521        let replacement = space.claim(7, "second").unwrap();
522        assert_eq!(space.resolve(&7).as_deref().copied(), Some("second"));
523        drop(replacement);
524    }
525
526    #[test]
527    fn registration_identity_distinguishes_replacements() {
528        let space = AddressSpace::new();
529        let first = space.claim("worker", 1).unwrap();
530        let first_id = first.registration_id();
531        first.release();
532
533        let second = space.claim("worker", 2).unwrap();
534        let second_id = second.registration_id();
535        assert_ne!(first_id, second_id);
536        assert_eq!(first_id.scope_id(), second_id.scope_id());
537
538        let ids = HashSet::from([first_id, second_id]);
539        assert_eq!(ids.len(), 2);
540    }
541
542    #[test]
543    fn registration_identity_is_scoped_to_the_address_space() {
544        let first = AddressSpace::new();
545        let first_peer = first.clone();
546        let second = AddressSpace::new();
547
548        assert_eq!(
549            first.registration_scope_id(),
550            first_peer.registration_scope_id()
551        );
552        assert_ne!(
553            first.registration_scope_id(),
554            second.registration_scope_id()
555        );
556
557        let first_id = first.claim((1_u8, 2_u8), ()).unwrap().registration_id();
558        let second_id = second.claim((1_u8, 2_u8), ()).unwrap().registration_id();
559        assert_ne!(first_id, second_id);
560    }
561
562    #[test]
563    fn retaining_an_identity_grants_no_registration_lifetime() {
564        let space = AddressSpace::new();
565        let lease = space
566            .claim(String::from("worker"), String::from("mailbox"))
567            .unwrap();
568        let identity = lease.registration_id();
569        drop(lease);
570
571        assert_eq!(space.resolve(&String::from("worker")), None);
572        let replacement = space
573            .claim(String::from("worker"), String::from("replacement"))
574            .unwrap();
575        assert_ne!(identity, replacement.registration_id());
576    }
577
578    #[test]
579    fn resolved_snapshot_survives_release_and_address_reuse() {
580        let space = AddressSpace::new();
581        let first = space.claim(7, String::from("first")).unwrap();
582        let snapshot = space.resolve(&7).unwrap();
583
584        first.release();
585        let second = space.claim(7, String::from("second")).unwrap();
586
587        assert_eq!(snapshot.as_str(), "first");
588        assert_eq!(space.resolve(&7).unwrap().as_str(), "second");
589        drop(second);
590    }
591
592    #[test]
593    fn exhausted_space_rejects_claims_without_reusing_an_identity() {
594        let space = AddressSpace::new();
595        space.inner.write_entries().set_next_generation(u64::MAX);
596
597        let last = space.claim(1_u64, "last").unwrap();
598        let last_id = last.registration_id();
599        last.release();
600
601        assert!(matches!(
602            space.try_claim(2, "never inserted"),
603            Err(ClaimError::RegistrationIdsExhausted(2))
604        ));
605        assert!(space.is_empty());
606        assert_eq!(last_id.scope_id(), &space.registration_scope_id());
607    }
608}