use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use super::refcount::CollectionQuiesce;
pub struct LifecycleDrainGuard {
registry: Arc<CollectionQuiesce>,
database_id: u64,
tenant_id: u64,
collection: String,
active: bool,
}
impl LifecycleDrainGuard {
pub fn disarm(mut self) {
self.active = false;
}
}
impl Drop for LifecycleDrainGuard {
fn drop(&mut self) {
if self.active {
self.registry
.clear_drain(self.database_id, self.tenant_id, &self.collection);
}
}
}
impl CollectionQuiesce {
pub fn begin_drain(&self, database_id: u64, tenant_id: u64, collection: &str) {
let mut inner = self.inner_mut();
let entry = inner
.states
.entry((database_id, tenant_id, collection.to_string()))
.or_default();
entry.drain_holders = entry.drain_holders.saturating_add(1);
}
pub fn clear_drain(&self, database_id: u64, tenant_id: u64, collection: &str) {
let mut inner = self.inner_mut();
let key = (database_id, tenant_id, collection.to_string());
let remove = if let Some(state) = inner.states.get_mut(&key) {
state.drain_holders = state.drain_holders.saturating_sub(1);
state.drain_holders == 0 && state.open_scans == 0
} else {
false
};
if remove {
inner.states.remove(&key);
}
drop(inner);
self.notify.notify_waiters();
}
pub fn forget(&self, database_id: u64, tenant_id: u64, collection: &str) {
let mut inner = self.inner_mut();
let key = (database_id, tenant_id, collection.to_string());
let remove = if let Some(state) = inner.states.get_mut(&key) {
state.drain_holders = state.drain_holders.saturating_sub(1);
state.drain_holders == 0
} else {
false
};
if remove {
inner.states.remove(&key);
}
drop(inner);
self.notify.notify_waiters();
}
pub fn try_acquire_lifecycle(
self: &Arc<Self>,
database_id: u64,
tenant_id: u64,
collection: &str,
) -> Option<LifecycleDrainGuard> {
let mut inner = self.inner_mut();
let entry = inner
.states
.entry((database_id, tenant_id, collection.to_string()))
.or_default();
if entry.drain_holders > 0 {
return None;
}
entry.drain_holders = 1;
Some(LifecycleDrainGuard {
registry: Arc::clone(self),
database_id,
tenant_id,
collection: collection.to_string(),
active: true,
})
}
pub async fn acquire_lifecycle(
self: &Arc<Self>,
database_id: u64,
tenant_id: u64,
collection: &str,
) -> LifecycleDrainGuard {
loop {
let notified = self.notify.notified();
tokio::pin!(notified);
notified.as_mut().enable();
{
let mut inner = self.inner_mut();
let entry = inner
.states
.entry((database_id, tenant_id, collection.to_string()))
.or_default();
if entry.drain_holders == 0 {
entry.drain_holders = 1;
return LifecycleDrainGuard {
registry: Arc::clone(self),
database_id,
tenant_id,
collection: collection.to_string(),
active: true,
};
}
}
notified.await;
}
}
pub fn wait_until_drained(
self: &Arc<Self>,
database_id: u64,
tenant_id: u64,
collection: &str,
) -> WaitDrain {
WaitDrain {
registry: Arc::clone(self),
database_id,
tenant_id,
collection: collection.to_string(),
notified: None,
}
}
fn inner_mut(&self) -> std::sync::MutexGuard<'_, super::refcount::Inner> {
self.inner.lock().expect("CollectionQuiesce mutex poisoned")
}
}
pub struct WaitDrain {
registry: Arc<CollectionQuiesce>,
database_id: u64,
tenant_id: u64,
collection: String,
notified: Option<Pin<Box<tokio::sync::futures::Notified<'static>>>>,
}
unsafe impl Send for WaitDrain {}
impl Future for WaitDrain {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
loop {
if self
.registry
.open_scans(self.database_id, self.tenant_id, &self.collection)
== 0
{
return Poll::Ready(());
}
if self.notified.is_none() {
let notify: &tokio::sync::Notify = &self.registry.notify;
let notified: tokio::sync::futures::Notified<'_> = notify.notified();
let notified: tokio::sync::futures::Notified<'static> =
unsafe { std::mem::transmute(notified) };
self.notified = Some(Box::pin(notified));
}
let fut = self.notified.as_mut().expect("just set");
match fut.as_mut().poll(cx) {
Poll::Ready(()) => {
self.notified = None;
continue;
}
Poll::Pending => {
if self
.registry
.open_scans(self.database_id, self.tenant_id, &self.collection)
== 0
{
return Poll::Ready(());
}
return Poll::Pending;
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const DB: u64 = 0;
#[tokio::test]
async fn drain_resolves_immediately_when_no_open_scans() {
let q = CollectionQuiesce::new();
q.begin_drain(DB, 1, "c");
q.wait_until_drained(DB, 1, "c").await;
}
#[tokio::test]
async fn drain_waits_for_last_scan_to_release() {
let q = CollectionQuiesce::new();
let g1 = q.try_start_scan(DB, 1, "c").unwrap();
let g2 = q.try_start_scan(DB, 1, "c").unwrap();
q.begin_drain(DB, 1, "c");
let q_clone = Arc::clone(&q);
let drain_task = tokio::spawn(async move {
q_clone.wait_until_drained(DB, 1, "c").await;
});
tokio::task::yield_now().await;
assert!(
!drain_task.is_finished(),
"drain must not resolve while scans open"
);
drop(g1);
tokio::task::yield_now().await;
assert!(
!drain_task.is_finished(),
"drain must not resolve with 1 scan still open"
);
drop(g2);
drain_task.await.unwrap();
}
#[tokio::test]
async fn single_lifecycle_holder_clears_on_forget() {
let q = CollectionQuiesce::new();
q.begin_drain(DB, 1, "c");
assert!(q.is_draining(DB, 1, "c"));
q.forget(DB, 1, "c");
assert!(!q.is_draining(DB, 1, "c"));
}
#[tokio::test]
async fn lifecycle_acquisition_is_exclusive() {
let q = CollectionQuiesce::new();
let first = q.acquire_lifecycle(DB, 1, "c").await;
let q_clone = Arc::clone(&q);
let second = tokio::spawn(async move { q_clone.acquire_lifecycle(DB, 1, "c").await });
tokio::task::yield_now().await;
assert!(!second.is_finished());
drop(first);
let second = second.await.unwrap();
drop(second);
assert!(!q.is_draining(DB, 1, "c"));
}
#[tokio::test]
async fn is_draining_until_every_lifecycle_holder_forgets() {
let q = CollectionQuiesce::new();
q.begin_drain(DB, 1, "c");
q.begin_drain(DB, 1, "c");
q.forget(DB, 1, "c");
assert!(q.is_draining(DB, 1, "c"));
q.forget(DB, 1, "c");
assert!(!q.is_draining(DB, 1, "c"));
}
#[tokio::test]
async fn forget_clears_state() {
let q = CollectionQuiesce::new();
q.begin_drain(DB, 1, "c");
q.wait_until_drained(DB, 1, "c").await;
q.forget(DB, 1, "c");
assert!(!q.is_draining(DB, 1, "c"));
assert!(q.try_start_scan(DB, 1, "c").is_ok());
}
}