#![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg(feature = "log")]
use log::{debug, error, trace};
use std::error::Error;
use std::fmt::{Display, Formatter};
#[cfg(loom)]
use loom::sync::{Arc, Condvar, Mutex, MutexGuard};
#[cfg(not(loom))]
use std::sync::{Arc, Condvar, Mutex, MutexGuard};
#[cfg(any(not(loom), feature = "tokio"))]
use std::time::Duration;
struct Shared {
count: Mutex<usize>,
cv: Condvar,
#[cfg(feature = "tokio")]
notify: tokio::sync::Notify,
}
impl Shared {
fn lock_count(&self) -> MutexGuard<'_, usize> {
self.count.lock().unwrap_or_else(|e| e.into_inner())
}
}
pub struct Rendezvous {
shared: Arc<Shared>,
detached: bool,
}
pub struct RendezvousGuard(Arc<Shared>);
impl Rendezvous {
pub fn new() -> Self {
Self {
shared: Arc::new(Shared {
count: Mutex::new(0),
cv: Condvar::new(),
#[cfg(feature = "tokio")]
notify: tokio::sync::Notify::new(),
}),
detached: false,
}
}
pub fn fork_guard(&self) -> RendezvousGuard {
#[cfg(feature = "log")]
{
trace!("Forking rendezvous guard");
}
*self.shared.lock_count() += 1;
RendezvousGuard(self.shared.clone())
}
pub fn rendezvous(self) {
self.wait();
}
#[cfg(feature = "tokio")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
pub async fn rendezvous_async(self) {
self.wait_async().await;
}
#[cfg(not(loom))]
pub fn rendezvous_timeout(
mut self,
timeout: Duration,
) -> Result<(), (Self, RendezvousTimeoutError)> {
if self.wait_timeout(timeout) {
Ok(())
} else {
#[cfg(feature = "log")]
{
debug!("A timeout occurred during a rendezvous");
}
self.detached = true;
Err((self, RendezvousTimeoutError::Timeout))
}
}
#[cfg(feature = "tokio")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
pub async fn rendezvous_timeout_async(
mut self,
timeout: Duration,
) -> Result<(), (Self, RendezvousTimeoutError)> {
match tokio::time::timeout(timeout, self.wait_async()).await {
Ok(()) => Ok(()),
Err(_elapsed) => {
#[cfg(feature = "log")]
{
debug!("A timeout occurred during a rendezvous");
}
self.detached = true;
Err((self, RendezvousTimeoutError::Timeout))
}
}
}
fn wait(&self) {
let mut count = self.shared.lock_count();
while *count > 0 {
count = self
.shared
.cv
.wait(count)
.unwrap_or_else(|e| e.into_inner());
}
}
#[cfg(not(loom))]
fn wait_timeout(&self, timeout: Duration) -> bool {
let count = self.shared.lock_count();
let (count, result) = self
.shared
.cv
.wait_timeout_while(count, timeout, |c| *c > 0)
.unwrap_or_else(|e| e.into_inner());
let _ = result;
*count == 0
}
#[cfg(feature = "tokio")]
async fn wait_async(&self) {
loop {
let notified = self.shared.notify.notified();
if *self.shared.lock_count() == 0 {
return;
}
notified.await;
}
}
}
impl Default for Rendezvous {
fn default() -> Self {
Rendezvous::new()
}
}
impl RendezvousGuard {
pub fn fork(&self) -> RendezvousGuard {
#[cfg(feature = "log")]
{
trace!("Forking nested rendezvous guard");
}
*self.0.lock_count() += 1;
RendezvousGuard(self.0.clone())
}
pub fn completed(self) {}
}
impl Clone for RendezvousGuard {
fn clone(&self) -> Self {
self.fork()
}
}
impl Drop for RendezvousGuard {
fn drop(&mut self) {
let mut count = self.0.lock_count();
*count -= 1;
if *count == 0 {
self.0.cv.notify_all();
#[cfg(feature = "tokio")]
self.0.notify.notify_waiters();
}
}
}
impl Drop for Rendezvous {
fn drop(&mut self) {
if self.detached {
return;
}
#[cfg(all(debug_assertions, feature = "log"))]
if *self.shared.lock_count() > 0 {
error!("Implementation error: Rendezvous method not invoked")
}
self.wait();
}
}
#[derive(Debug, Eq, PartialEq)]
pub enum RendezvousTimeoutError {
Timeout,
}
impl Display for RendezvousTimeoutError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
RendezvousTimeoutError::Timeout => write!(f, "Timeout"),
}
}
}
impl Error for RendezvousTimeoutError {}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Instant;
#[test]
fn stress_sync_concurrent_fork_drop() {
for _ in 0..200 {
let rendezvous = Rendezvous::new();
let mut handles = Vec::new();
for _ in 0..64 {
let guard = rendezvous.fork_guard();
handles.push(thread::spawn(move || drop(guard)));
}
rendezvous.rendezvous();
for h in handles {
h.join().unwrap();
}
}
}
#[test]
fn stress_sync_nested_fork() {
for _ in 0..100 {
let rendezvous = Rendezvous::new();
let root = rendezvous.fork_guard();
let mut handles = Vec::new();
for _ in 0..16 {
let g = root.clone();
handles.push(thread::spawn(move || {
let sub = g.fork();
drop(g);
drop(sub);
}));
}
drop(root);
rendezvous.rendezvous();
for h in handles {
h.join().unwrap();
}
}
}
#[cfg(feature = "tokio")]
#[test]
fn stress_async_concurrent_drop() {
tokio_test::block_on(async {
for _ in 0..100 {
let rendezvous = Rendezvous::new();
let mut handles = Vec::new();
for _ in 0..32 {
let guard = rendezvous.fork_guard();
handles.push(thread::spawn(move || drop(guard)));
}
rendezvous.rendezvous_async().await;
for h in handles {
h.join().unwrap();
}
}
});
}
#[test]
fn rendezvous_can_pass_away() {
let rendezvous = Rendezvous::new();
rendezvous.rendezvous();
}
#[test]
fn rendezvous_can_be_dropped_right_away() {
let rendezvous = Rendezvous::new();
drop(rendezvous);
}
#[test]
fn test_timeout() {
let rendezvous = Rendezvous::new();
let guard = rendezvous.fork_guard();
let result = rendezvous.rendezvous_timeout(Duration::from_millis(100));
assert!(matches!(result, Err((_, RendezvousTimeoutError::Timeout))));
drop(guard);
}
#[test]
fn test_background_forks() {
let rendezvous = Rendezvous::new();
let guard = rendezvous.fork_guard();
thread::spawn(move || {
let _guard = guard;
thread::sleep(Duration::from_millis(400))
});
rendezvous.rendezvous();
}
#[test]
fn fork_after_timeout_does_not_panic() {
let rendezvous = Rendezvous::new();
let first = rendezvous.fork_guard();
let rendezvous = match rendezvous.rendezvous_timeout(Duration::from_millis(10)) {
Ok(()) => panic!("guard still held, should have timed out"),
Err((rendezvous, err)) => {
assert_eq!(err, RendezvousTimeoutError::Timeout);
rendezvous
}
};
let second = rendezvous.fork_guard();
thread::spawn(move || {
thread::sleep(Duration::from_millis(50));
drop(first);
drop(second);
});
assert!(rendezvous
.rendezvous_timeout(Duration::from_secs(1))
.is_ok());
}
#[test]
fn drop_after_timeout_is_bounded() {
let rendezvous = Rendezvous::new();
let guard = rendezvous.fork_guard();
let handle = thread::spawn(move || {
let _guard = guard;
thread::sleep(Duration::from_secs(5))
});
let rendezvous = match rendezvous.rendezvous_timeout(Duration::from_millis(10)) {
Ok(()) => panic!("guard still held, should have timed out"),
Err((rendezvous, _)) => rendezvous,
};
let start = Instant::now();
drop(rendezvous);
assert!(
start.elapsed() < Duration::from_secs(1),
"drop after timeout blocked"
);
handle.join().unwrap();
}
#[cfg(feature = "tokio")]
#[test]
fn async_rendezvous_completes() {
tokio_test::block_on(async {
let rendezvous = Rendezvous::new();
let guard = rendezvous.fork_guard();
thread::spawn(move || {
let _guard = guard;
thread::sleep(Duration::from_millis(100))
});
rendezvous.rendezvous_async().await;
});
}
#[cfg(feature = "tokio")]
#[test]
fn async_timeout_returns_rendezvous() {
tokio_test::block_on(async {
let rendezvous = Rendezvous::new();
let guard = rendezvous.fork_guard();
let rendezvous = match rendezvous
.rendezvous_timeout_async(Duration::from_millis(10))
.await
{
Ok(()) => panic!("guard still held, should have timed out"),
Err((rendezvous, err)) => {
assert_eq!(err, RendezvousTimeoutError::Timeout);
rendezvous
}
};
drop(guard);
assert!(rendezvous
.rendezvous_timeout_async(Duration::from_secs(1))
.await
.is_ok());
});
}
}