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},
};
type Key = (String, Vec<u8>);
#[derive(Default)]
pub(crate) struct Opens {
live: Mutex<BTreeMap<Key, Weak<Live>>>,
#[cfg(test)]
test: tests::Hooks,
}
impl Opens {
pub(crate) fn open<B: crate::Blob>(
self: &Arc<Self>,
partition: &str,
name: &[u8],
open: impl Future<Output = Result<(B, u64, BlobVersion), Error>> + Send,
) -> Result<Opened<'_, B>, Error> {
let mut opens = self.enter();
let (inner, len, version) = self.complete(open)?;
let key = (partition.to_owned(), name.to_vec());
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),
})
}
pub(crate) fn remove<R>(
&self,
partition: &str,
name: Option<&[u8]>,
remove: impl Future<Output = Result<R, Error>> + Send,
) -> Result<R, Error> {
self.replace(partition, name, || self.complete(remove))
}
pub(crate) fn replace<R>(
&self,
partition: &str,
name: Option<&[u8]>,
replace: impl FnOnce() -> Result<R, Error>,
) -> Result<R, Error> {
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)
}
fn enter(&self) -> MutexGuard<'_, BTreeMap<Key, Weak<Live>>> {
#[cfg(test)]
self.test.registry_entry(self.live.is_locked());
self.live.lock()
}
fn complete<R>(&self, work: impl Future<Output = Result<R, Error>>) -> Result<R, Error> {
let result = work
.now_or_never()
.expect("memory namespace work completes in one poll")?;
#[cfg(test)]
self.test.namespace_handoff();
Ok(result)
}
}
pub(crate) struct Opened<'a, B> {
_namespace: MutexGuard<'a, BTreeMap<Key, Weak<Live>>>,
result: (Blob<B>, u64, BlobVersion),
}
impl<B> Opened<'_, B> {
pub(crate) fn finish(self) -> (Blob<B>, u64, BlobVersion) {
self.result
}
}
struct Live {
key: Key,
opens: Arc<Opens>,
}
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);
}
}
}
pub struct Blob<B> {
inner: B,
_live: Arc<Live>,
}
impl<B: crate::Blob> crate::Blob for Blob<B> {
async fn read_at_buf(
&self,
offset: u64,
len: usize,
bufs: impl Into<IoBufsMut> + Send,
options: ReadOptions,
) -> Result<IoBufsMut, Error> {
self.inner.read_at_buf(offset, len, bufs, options).await
}
async fn read_at(
&self,
offset: u64,
len: usize,
options: ReadOptions,
) -> Result<IoBufsMut, Error> {
self.inner.read_at(offset, len, options).await
}
async fn write_at(
&self,
offset: u64,
bufs: impl Into<IoBufs> + 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<usize>, Receiver<()>);
#[derive(Default)]
pub(super) struct Hooks {
open_observation: Mutex<Option<OpenObservation>>,
namespace_handoff: Mutex<Option<(Sender<()>, Receiver<()>)>>,
registry_observation: Mutex<Option<Sender<bool>>>,
}
impl Opens {
pub(crate) fn pause_namespace(&self, entered: Sender<()>, released: Receiver<()>) {
*self.test.namespace_handoff.lock() = Some((entered, released));
}
pub(crate) fn watch_registry(&self, entered: Sender<bool>) {
*self.test.registry_observation.lock() = Some(entered);
}
}
impl Hooks {
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();
}
}
pub(super) fn registry_entry(&self, locked: bool) {
if let Some(entered) = self.registry_observation.lock().take() {
entered.send(locked).unwrap();
}
}
pub(super) fn observe_open(&self, identity: Option<&Weak<Live>>) {
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();
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();
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"));
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"));
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));
});
}
}