use async_lock::Mutex;
use futures::{FutureExt, future::Shared};
use oneshot::{Receiver, Sender};
struct ReleaseChannel {
sender: Sender<()>,
receiver: Shared<Receiver<()>>,
}
#[derive(Default)]
pub struct Line {
channel: Mutex<Option<ReleaseChannel>>,
}
impl Line {
pub fn new() -> Line {
Line {
channel: Default::default(),
}
}
pub async fn try_acquire_token<'l>(&'l self) -> AcquireOutcome<'l> {
let mut channel = self.channel.lock().await;
if let Some(channel) = channel.as_ref() {
return AcquireOutcome::Failure(channel.receiver.clone());
}
let (sender, receiver) = oneshot::channel();
*channel = Some(ReleaseChannel {
sender,
receiver: receiver.shared(),
});
AcquireOutcome::Success(Token { line: self })
}
pub(self) fn release(&self) {
match self.channel.lock_blocking().take() {
Some(channel) => match channel.sender.send(()) {
Ok(_) => (),
Err(_) => log::error!("trying to release using a closed channel"),
},
None => log::error!("trying to release before acquiring"),
};
}
}
#[must_use = "if the token is unused the line will immediately release again"]
pub enum AcquireOutcome<'ao> {
Success(Token<'ao>),
Failure(Shared<Receiver<()>>),
}
impl<'ao> AcquireOutcome<'ao> {
pub fn or_token(self, token: Option<Token<'ao>>) -> Self {
match self {
AcquireOutcome::Success(_) => self,
AcquireOutcome::Failure(_) => match token {
Some(token) => AcquireOutcome::Success(token),
None => self,
},
}
}
}
#[must_use = "if unused the line will immediately release again"]
pub struct Token<'t> {
line: &'t Line,
}
impl Drop for Token<'_> {
fn drop(&mut self) {
self.line.release();
}
}
#[cfg(test)]
mod tests {
use tokio::time::Duration;
use super::*;
async fn get_token(line: &Line) -> Token<'_> {
match line.try_acquire_token().await {
AcquireOutcome::Success(token) => token,
AcquireOutcome::Failure(_) => panic!("expected a token from try_acquire_token()"),
}
}
#[tokio::test(flavor = "current_thread")]
async fn acquire_token() {
let line = Line::new();
let _token = get_token(&line).await;
match line.try_acquire_token().await {
AcquireOutcome::Success(_) => {
panic!("should not be able to acquire the line while the token is in scope")
}
AcquireOutcome::Failure(_) => (),
}
}
#[tokio::test(flavor = "current_thread")]
async fn token_out_of_scope() {
let line = Line::new();
{
let _token = get_token(&line).await;
match line.try_acquire_token().await {
AcquireOutcome::Success(_) => {
panic!("should not be able to acquire the line while the token is in scope")
}
AcquireOutcome::Failure(_) => (),
}
}
match line.try_acquire_token().await {
AcquireOutcome::Success(_) => (),
AcquireOutcome::Failure(_) => {
panic!("expected a token now that the previous token has been dropped")
}
}
}
#[tokio::test(flavor = "current_thread")]
async fn or_token() {
let line = Line::new();
let token = get_token(&line).await;
match line.try_acquire_token().await.or_token(Some(token)) {
AcquireOutcome::Success(_) => (),
AcquireOutcome::Failure(_) => panic!("we should have kept our token"),
}
}
#[tokio::test(flavor = "current_thread")]
async fn line_release_on_drop() {
let line = Line::new();
let mut success = false;
async fn acquire_sleep_and_drop(line: &Line) {
let _token = get_token(&line).await;
tokio::time::sleep(Duration::from_millis(10)).await;
}
async fn wait_for_line(line: &Line, success: &mut bool) {
let shared = match line.try_acquire_token().await {
AcquireOutcome::Success(_) => {
panic!("should not be able to acquire the line while the token is in scope")
}
AcquireOutcome::Failure(shared) => shared,
};
shared.await.unwrap();
*success = true;
}
tokio::join! {
biased;
acquire_sleep_and_drop(&line),
wait_for_line(&line, &mut success),
};
assert!(success)
}
}