use std::collections::HashMap;
use std::ffi::OsString;
use std::future::Future;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use std::time::Duration;
use tokio::sync::{oneshot, watch};
use tokio::time::Instant;
#[cfg(feature = "tracing")]
use tracing::instrument::{Instrument as _, WithSubscriber as _};
use crate::internal::core::Core;
use crate::internal::listing;
use crate::{ChannelWait, Command, CommandResult, Error};
#[derive(Default)]
pub(crate) struct ChannelWaits {
channels: Mutex<HashMap<String, Channel>>,
}
struct Channel {
kept: bool,
waiting: usize,
resident: bool,
failure: Option<Error>,
releases: watch::Sender<u64>,
}
impl Default for Channel {
fn default() -> Self {
Self {
kept: false,
waiting: 0,
resident: false,
failure: None,
releases: watch::channel(0).0,
}
}
}
impl Channel {
fn is_idle(&self) -> bool {
!self.kept && !self.resident && self.waiting == 0 && self.failure.is_none()
}
}
impl ChannelWaits {
fn channels(&self) -> MutexGuard<'_, HashMap<String, Channel>> {
self.channels.lock().unwrap_or_else(PoisonError::into_inner)
}
fn join(&self, channel: &str) -> Join<'_> {
let mut channels = self.channels();
let state = channels.entry(channel.to_owned()).or_default();
if state.kept {
state.kept = false;
let idle = state.is_idle();
if idle {
channels.remove(channel);
}
return Join::Kept;
}
state.waiting += 1;
let opening = !state.resident;
if opening {
state.resident = true;
state.failure = None;
}
Join::Waiting(Waiter {
waits: self,
channel: channel.to_owned(),
seen: *state.releases.borrow(),
released: state.releases.subscribe(),
opening,
settled: false,
})
}
fn finish(&self, channel: &str, outcome: Result<(), Error>) {
let mut channels = self.channels();
let Some(state) = channels.get_mut(channel) else {
return;
};
state.resident = false;
match outcome {
Ok(()) => {
state.releases.send_modify(|count| *count += 1);
state.kept = state.waiting == 0;
}
Err(error) => {
state.failure = Some(error);
state.releases.send_modify(|_| ());
}
}
if state.is_idle() {
channels.remove(channel);
}
}
}
enum Join<'a> {
Kept,
Waiting(Waiter<'a>),
}
struct Waiter<'a> {
waits: &'a ChannelWaits,
channel: String,
seen: u64,
released: watch::Receiver<u64>,
opening: bool,
settled: bool,
}
enum Settled {
Signalled,
Failed(Error),
Gone,
TimedOut,
}
impl Waiter<'_> {
fn settle(&mut self) -> Settled {
self.settled = true;
let mut channels = self.waits.channels();
let Some(state) = channels.get_mut(&self.channel) else {
return Settled::TimedOut;
};
state.waiting -= 1;
let settled = if *state.releases.borrow() > self.seen {
Settled::Signalled
} else if state.resident {
Settled::TimedOut
} else {
state.failure.take().map_or(Settled::Gone, Settled::Failed)
};
if state.is_idle() {
channels.remove(&self.channel);
}
settled
}
}
impl Drop for Waiter<'_> {
fn drop(&mut self) {
if self.settled {
return;
}
let mut channels = self.waits.channels();
let Some(state) = channels.get_mut(&self.channel) else {
return;
};
state.waiting -= 1;
state.kept = *state.releases.borrow() > self.seen && state.waiting == 0;
if state.is_idle() {
channels.remove(&self.channel);
}
}
}
pub(crate) async fn wait(
core: &Arc<Core>,
channel: &str,
budget: Duration,
) -> Result<ChannelWait, Error> {
#[cfg(feature = "control-mode")]
refuse_if_routed(core)?;
let deadline = Instant::now().checked_add(budget);
loop {
let mut waiter = match core.channel_waits().join(channel) {
Join::Kept => return Ok(ChannelWait::Signalled),
Join::Waiting(waiter) => waiter,
};
if waiter.opening {
park(core, channel);
}
tokio::select! {
result = waiter.released.changed() => {
let _ = result;
}
() = elapsed(deadline) => {}
}
match waiter.settle() {
Settled::Signalled => return Ok(ChannelWait::Signalled),
Settled::Failed(error) => return Err(error),
Settled::Gone if !out_of_time(deadline) => {}
Settled::Gone | Settled::TimedOut => return Ok(ChannelWait::TimedOut),
}
}
}
fn park(core: &Arc<Core>, channel: &str) {
let core = Arc::clone(core);
let channel = channel.to_owned();
spawn(async move {
let (_, dispatch) = core.dispatch_without_deadline(channel_command(None, &channel));
let outcome = mutated(dispatch.await);
core.channel_waits().finish(&channel, outcome);
});
}
pub(crate) async fn lock(core: &Arc<Core>, channel: &str, budget: Duration) -> Result<(), Error> {
#[cfg(feature = "control-mode")]
refuse_if_routed(core)?;
let command = channel_command(Some("-L"), channel);
let summary = command.summary();
let (request_id, dispatch) = core.dispatch_without_deadline(command);
let (granted, taken) = oneshot::channel();
let holder = Arc::clone(core);
let name = channel.to_owned();
spawn(async move {
let outcome = mutated(dispatch.await);
if let Err(outcome) = granted.send(outcome) {
if outcome.is_ok() {
let _ = unlock(&holder, &name).await;
}
}
});
let mut grant = Grant {
taken,
core: Arc::clone(core),
channel: channel.to_owned(),
settled: false,
};
let deadline = Instant::now().checked_add(budget);
tokio::select! {
outcome = &mut grant.taken => match outcome {
Ok(outcome) => outcome,
Err(_) => Err(Error::supervisor_lost(request_id.get(), summary)),
},
() = elapsed(deadline) => grant
.close()
.unwrap_or_else(|| Err(Error::timeout(request_id.get(), summary, budget))),
}
}
struct Grant {
taken: oneshot::Receiver<Result<(), Error>>,
core: Arc<Core>,
channel: String,
settled: bool,
}
impl Grant {
fn close(&mut self) -> Option<Result<(), Error>> {
self.settled = true;
self.taken.close();
self.taken.try_recv().ok()
}
}
impl Drop for Grant {
fn drop(&mut self) {
if self.settled || !matches!(self.close(), Some(Ok(()))) {
return;
}
if tokio::runtime::Handle::try_current().is_err() {
return;
}
let core = Arc::clone(&self.core);
let channel = std::mem::take(&mut self.channel);
spawn(async move {
let _ = unlock(&core, &channel).await;
});
}
}
pub(crate) async fn unlock(core: &Arc<Core>, channel: &str) -> Result<(), Error> {
listing::mutate(core, "wait-for", channel_command(Some("-U"), channel)).await
}
pub(crate) async fn signal(core: &Arc<Core>, channel: &str) -> Result<(), Error> {
listing::mutate(core, "wait-for", channel_command(Some("-S"), channel)).await
}
#[cfg(feature = "control-mode")]
fn refuse_if_routed(core: &Arc<Core>) -> Result<(), Error> {
if core.routes_over_control_mode() {
return Err(Error::control_mode_blocking());
}
Ok(())
}
fn channel_command(flag: Option<&'static str>, channel: &str) -> Command {
let command = Command::new("wait-for");
let command = match flag {
Some(flag) => command.arg(flag),
None => command,
};
command.arg("--").arg(OsString::from(channel))
}
fn mutated(result: Result<CommandResult, Error>) -> Result<(), Error> {
let result = result?;
if result.success() {
Ok(())
} else {
Err(listing::mutation_failure("wait-for", &result, None))
}
}
async fn elapsed(deadline: Option<Instant>) {
match deadline {
Some(deadline) => tokio::time::sleep_until(deadline).await,
None => std::future::pending().await,
}
}
fn out_of_time(deadline: Option<Instant>) -> bool {
deadline.is_some_and(|deadline| Instant::now() >= deadline)
}
fn spawn(task: impl Future<Output = ()> + Send + 'static) {
#[cfg(feature = "tracing")]
tokio::spawn(task.in_current_span().with_current_subscriber());
#[cfg(not(feature = "tracing"))]
tokio::spawn(task);
}
#[cfg(test)]
mod tests {
use super::{ChannelWaits, Join, Settled};
#[test]
fn a_release_is_kept_only_when_no_caller_is_left_to_hear_it() {
let waits = ChannelWaits::default();
let Join::Waiting(gave_up) = waits.join("build") else {
panic!("the channel starts with nothing kept");
};
drop(gave_up);
waits.finish("build", Ok(()));
assert!(matches!(waits.join("build"), Join::Kept));
let Join::Waiting(mut waiting) = waits.join("build") else {
panic!("a kept release is one-shot");
};
waits.finish("build", Ok(()));
assert!(matches!(waiting.settle(), Settled::Signalled));
assert!(matches!(waits.join("build"), Join::Waiting(_)));
let Join::Waiting(raced) = waits.join("build") else {
panic!("the release went to the caller that was waiting");
};
waits.finish("build", Ok(()));
drop(raced);
assert!(matches!(waits.join("build"), Join::Kept));
}
}