//! Exclusive logical opens for the memory backend. use crate::{BlobVersion, Error, Handle, IoBufs, IoBufsMut, ReadOptions, WriteOptions}; use commonware_formatting::hex; use commonware_utils::sync::{Mutex, MutexGuard}; use futures::{Future, FutureExt as _}; use std::{ collections::BTreeMap, ptr, sync::{Arc, Weak}, }; /// Identifies a blob by its partition and name. type Key = (String, Vec); /// Couples namespace transactions to logical user-handle lifetimes. #[derive(Default)] pub(crate) struct Opens { live: Mutex>>, #[cfg(test)] test: tests::Hooks, } impl Opens { /// Registers an exclusive open atomically with opening the underlying blob. /// /// The open future must complete in one poll. The returned guard retains the /// namespace lock until [`Opened::finish`] returns the blob. pub(crate) fn open( self: &Arc, partition: &str, name: &[u8], open: impl Future> + Send, ) -> Result, Error> { let mut opens = self.enter(); let (inner, len, version) = self.complete(open)?; let key = (partition.to_owned(), name.to_vec()); // Observing liveness must not acquire an owner whose destructor locks this registry. let live = opens .get(&key) .is_some_and(|identity| identity.strong_count() != 0); #[cfg(test)] self.test.observe_open(opens.get(&key)); if live { return Err(Error::BlobAlreadyOpen(partition.to_owned(), hex(name))); } let live = Arc::new(Live { key: key.clone(), opens: self.clone(), }); opens.insert(key, Arc::downgrade(&live)); Ok(Opened { _namespace: opens, result: (Blob { inner, _live: live }, len, version), }) } /// Retires registrations atomically with a blob or partition removal. /// /// The removal future must complete in one poll. Failed removals leave registrations intact. pub(crate) fn remove( &self, partition: &str, name: Option<&[u8]>, remove: impl Future> + Send, ) -> Result { self.replace(partition, name, || self.complete(remove)) } /// Retires registrations atomically with removal or installation of a raw memory image. pub(crate) fn replace( &self, partition: &str, name: Option<&[u8]>, replace: impl FnOnce() -> Result, ) -> Result { let mut opens = self.enter(); let replaced = replace()?; opens.retain(|(stored_partition, stored_name), _| { stored_partition != partition || name.is_some_and(|name| stored_name != name) }); Ok(replaced) } /// Lock the registry. fn enter(&self) -> MutexGuard<'_, BTreeMap>> { #[cfg(test)] self.test.registry_entry(self.live.is_locked()); self.live.lock() } /// Complete namespace work that must finish in one poll. fn complete(&self, work: impl Future>) -> Result { let result = work .now_or_never() .expect("memory namespace work completes in one poll")?; #[cfg(test)] self.test.namespace_handoff(); Ok(result) } } /// Holds the namespace lock until the opened blob is returned. pub(crate) struct Opened<'a, B> { // Release the registry before dropping the user lease, including during unwinding. _namespace: MutexGuard<'a, BTreeMap>>, result: (Blob, u64, BlobVersion), } impl Opened<'_, B> { pub(crate) fn finish(self) -> (Blob, u64, BlobVersion) { self.result } } /// Marks a blob as open until it is dropped or removed. struct Live { key: Key, opens: Arc, } impl Drop for Live { fn drop(&mut self) { let mut opens = self.opens.live.lock(); if opens .get(&self.key) .is_some_and(|live| ptr::eq(live.as_ptr(), self)) { opens.remove(&self.key); } } } /// A blob handle whose open stays exclusive until it is dropped or removed. pub struct Blob { inner: B, _live: Arc, } impl crate::Blob for Blob { async fn read_at_buf( &self, offset: u64, len: usize, bufs: impl Into + Send, options: ReadOptions, ) -> Result { self.inner.read_at_buf(offset, len, bufs, options).await } async fn read_at( &self, offset: u64, len: usize, options: ReadOptions, ) -> Result { self.inner.read_at(offset, len, options).await } async fn write_at( &self, offset: u64, bufs: impl Into + Send, options: WriteOptions, ) -> Result<(), Error> { self.inner.write_at(offset, bufs, options).await } async fn resize(&self, len: u64) -> Result<(), Error> { self.inner.resize(len).await } async fn sync(&self) -> Result<(), Error> { self.inner.sync().await } async fn start_sync(&self) -> Handle<()> { self.inner.start_sync().await } } #[cfg(test)] pub(crate) mod tests { use super::*; use crate::{Blob as _, Runner as _, Storage as _, deterministic::Runner, mocks::Storage}; use std::{ sync::mpsc::{self, Receiver, Sender}, thread, }; type OpenObservation = (Sender, Receiver<()>); /// One-shot observations and pauses for a single logical-open registry. #[derive(Default)] pub(super) struct Hooks { open_observation: Mutex>, namespace_handoff: Mutex, Receiver<()>)>>, registry_observation: Mutex>>, } impl Opens { /// Pause the next namespace handoff after reporting arrival through `entered`. /// Resume when `released` receives a message or its sender drops. pub(crate) fn pause_namespace(&self, entered: Sender<()>, released: Receiver<()>) { *self.test.namespace_handoff.lock() = Some((entered, released)); } /// Report the lock state when the next namespace operation attempts entry. pub(crate) fn watch_registry(&self, entered: Sender) { *self.test.registry_observation.lock() = Some(entered); } } impl Hooks { /// Report the namespace handoff and wait for release. pub(super) fn namespace_handoff(&self) { let hook = self.namespace_handoff.lock().take(); if let Some((entered, released)) = hook { entered.send(()).unwrap(); let _ = released.recv(); } } /// Report the lock state before attempting registry entry. pub(super) fn registry_entry(&self, locked: bool) { if let Some(entered) = self.registry_observation.lock().take() { entered.send(locked).unwrap(); } } /// Report the current owner count and wait for release. /// A missing registration has zero owners. pub(super) fn observe_open(&self, identity: Option<&Weak>) { let hook = self.open_observation.lock().take(); if let Some((entered, released)) = hook { entered .send(identity.map_or(0, Weak::strong_count)) .unwrap(); let _ = released.recv(); } } } #[test] fn test_open_racing_last_blob_drop() { Runner::default().start(|_| async move { let context = Storage::new(); let (blob, _) = context.open("partition", b"blob").await.unwrap(); blob.write_at(0, b"saved", WriteOptions::default()) .await .unwrap(); blob.sync().await.unwrap(); let identity = Arc::downgrade(&blob._live); let opens = blob._live.opens.clone(); let (release_drop, dropping) = mpsc::channel(); let dropper = thread::spawn(move || { dropping.recv().unwrap(); drop(blob); }); let (entered, entering) = mpsc::channel(); let (release, released) = mpsc::channel(); *opens.test.open_observation.lock() = Some((entered, released)); let coordinator = thread::spawn(move || { let owners = entering.recv().unwrap(); release_drop.send(()).unwrap(); // The strong count changes when the external owner drops. Successful opens below // verify that the registry progresses while the drop is scheduled. while identity.strong_count() == owners { thread::yield_now(); } release.send(()).unwrap(); }); assert!(matches!( context.open("partition", b"blob").await, Err(Error::BlobAlreadyOpen(partition, name)) if partition == "partition" && name == "626c6f62" )); coordinator.join().unwrap(); dropper.join().unwrap(); drop(context.open("partition", b"independent").await.unwrap()); let (blob, size) = context.open("partition", b"blob").await.unwrap(); assert_eq!(size, 5); assert_eq!( blob.read_at(0, 5, ReadOptions::default()) .await .unwrap() .coalesce(), b"saved", ); }); } #[test] fn test_replace_retires_only_matching_registrations() { Runner::default().start(|_| async move { let inner = crate::storage::memory::Storage::new(); let opens = Arc::new(Opens::default()); let open = |partition: &str, name: &[u8]| { let versions = crate::DEFAULT_BLOB_VERSION..=crate::DEFAULT_BLOB_VERSION; opens .open( partition, name, inner.open_versioned(partition, name, versions), ) .map(Opened::finish) }; let refused = |partition: &str, name: &[u8]| { matches!(open(partition, name), Err(Error::BlobAlreadyOpen(..))) }; let ax = open("a", b"x").unwrap(); let ay = open("a", b"y").unwrap(); let bx = open("b", b"x").unwrap(); // A failed replacement leaves every registration intact. assert!(matches!( opens.replace("a", None, || Err::<(), _>(Error::Closed)), Err(Error::Closed) )); assert!(refused("a", b"x") && refused("a", b"y") && refused("b", b"x")); // Replacing a name retires only that name. The old handle cannot release the new one. opens.replace("a", Some(b"x"), || Ok(())).unwrap(); assert!(refused("a", b"y") && refused("b", b"x")); let current = open("a", b"x").unwrap(); drop(ax); assert!(refused("a", b"x")); // Replacing a partition retires its live names but not another partition's. opens.replace("a", None, || Ok(())).unwrap(); drop(open("a", b"x").unwrap()); drop(open("a", b"y").unwrap()); assert!(refused("b", b"x")); drop((current, ay, bx)); }); } }