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}