use std::collections::VecDeque;
use argonauts_core::control::{ControlReply, ControlRequest};
use argonauts_core::engine::{EngineCommand, EngineExit};
use argonauts_core::events::run::{RunOutcome, WaitReason};
use tokio::sync::{mpsc, oneshot};
use super::server::{EngineReply, RUN_HOST_EXITING, command, error};
pub(crate) struct Entry {
pub request: ControlRequest,
reply: oneshot::Sender<Delivery>,
}
enum Delivery {
Taken(EngineReply),
Answered(Box<ControlReply>),
}
pub(crate) struct Receipt(oneshot::Receiver<Delivery>);
impl Entry {
pub(crate) fn new(request: ControlRequest) -> (Self, Receipt) {
let (reply, receipt) = oneshot::channel();
(Self { request, reply }, Receipt(receipt))
}
pub(crate) fn answer(self, reply: ControlReply) {
let _ = self.reply.send(Delivery::Answered(Box::new(reply)));
}
fn taken(self, reply: EngineReply) {
let _ = self.reply.send(Delivery::Taken(reply));
}
}
impl Receipt {
pub(crate) async fn reply(self) -> ControlReply {
match self.0.await {
Ok(Delivery::Taken(reply)) => reply.wait().await,
Ok(Delivery::Answered(reply)) => *reply,
Err(_) => error(RUN_HOST_EXITING),
}
}
}
pub(crate) struct Queue {
incoming: mpsc::UnboundedReceiver<Entry>,
backlog: VecDeque<Entry>,
}
impl Queue {
pub(crate) fn new(incoming: mpsc::UnboundedReceiver<Entry>) -> Self {
Self {
incoming,
backlog: VecDeque::new(),
}
}
async fn next(&mut self) -> Option<Entry> {
match self.backlog.pop_front() {
Some(entry) => Some(entry),
None => self.incoming.recv().await,
}
}
fn try_next(&mut self) -> Option<Entry> {
self.backlog
.pop_front()
.or_else(|| self.incoming.try_recv().ok())
}
fn put_back(&mut self, entry: Entry) {
self.backlog.push_front(entry);
}
pub(crate) fn is_empty(&self) -> bool {
self.backlog.is_empty() && self.incoming.is_empty()
}
pub(crate) fn take_all(&mut self) -> Vec<Entry> {
let mut entries: Vec<Entry> = self.backlog.drain(..).collect();
while let Ok(entry) = self.incoming.try_recv() {
entries.push(entry);
}
entries
}
pub(crate) fn restore(&mut self, entries: Vec<Entry>) {
for entry in entries.into_iter().rev() {
self.put_back(entry);
}
}
pub(crate) fn close(&mut self) -> Vec<Entry> {
self.incoming.close();
self.take_all()
}
}
pub(crate) type InFlight = (Entry, EngineReply);
pub(crate) struct Forwarded {
taken: usize,
unsettled: Option<InFlight>,
}
impl Forwarded {
pub(crate) fn settle(self, queue: &mut Queue, exit: Option<&EngineExit>) -> usize {
let Some((entry, reply)) = self.unsettled else {
return self.taken;
};
if reply.answered() || exit.is_some_and(|exit| done_by(&entry.request, exit)) {
entry.taken(reply);
return self.taken + 1;
}
queue.put_back(entry);
self.taken
}
}
fn done_by(request: &ControlRequest, exit: &EngineExit) -> bool {
matches!(
(request, exit),
(
ControlRequest::Stop,
EngineExit::Finished(RunOutcome::Stopped)
) | (
ControlRequest::Pause { .. },
EngineExit::Waiting(WaitReason::Paused)
)
)
}
pub(crate) fn prime(queue: &mut Queue, inbox: &mpsc::Sender<EngineCommand>) -> Option<InFlight> {
while let Some(entry) = queue.try_next() {
let (command, reply) = match command(entry.request.clone()) {
Ok(made) => made,
Err(refused) => {
entry.answer(error(refused));
continue;
}
};
if inbox.try_send(command).is_err() {
queue.put_back(entry);
return None;
}
return Some((entry, reply));
}
None
}
pub(crate) async fn forward(
queue: &mut Queue,
inbox: mpsc::Sender<EngineCommand>,
primed: Option<InFlight>,
) -> Forwarded {
let mut taken = 0;
let mut in_flight = primed;
loop {
let Ok(permit) = inbox.reserve().await else {
break;
};
if let Some((entry, reply)) = in_flight.take() {
taken += 1;
entry.taken(reply);
}
let next = tokio::select! {
entry = queue.next() => entry,
() = inbox.closed() => None,
};
let Some(entry) = next else {
break;
};
match command(entry.request.clone()) {
Ok((command, reply)) => {
permit.send(command);
in_flight = Some((entry, reply));
}
Err(refused) => entry.answer(error(refused)),
}
}
Forwarded {
taken,
unsettled: in_flight,
}
}
pub(crate) fn deferrable(request: &ControlRequest) -> bool {
matches!(
request,
ControlRequest::Answer { .. }
| ControlRequest::Guidance { .. }
| ControlRequest::Mention { .. }
| ControlRequest::Reassign { .. }
| ControlRequest::Stop
| ControlRequest::StopSession { .. }
| ControlRequest::ApprovalMode { .. }
| ControlRequest::TakeOverEnded { .. }
| ControlRequest::SpotCheck { .. }
| ControlRequest::Goal { .. }
)
}
pub(crate) fn deferred_reply(request: &ControlRequest) -> ControlReply {
match request {
ControlRequest::Guidance { .. } | ControlRequest::Mention { .. } => {
error("the run is waiting; the message is delivered when it resumes")
}
_ => ControlReply::Ok,
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use argonauts_core::settings::approvals::ApprovalMode;
use super::*;
use crate::host::server::tests::TOKEN;
fn approval_mode() -> ControlRequest {
ControlRequest::ApprovalMode {
mode: ApprovalMode::Auto,
token: TOKEN.to_owned(),
}
}
#[tokio::test]
async fn a_command_the_engine_never_took_goes_to_the_next_engine() {
let (sender, incoming) = mpsc::unbounded_channel();
let mut queue = Queue::new(incoming);
let (entry, receipt) = Entry::new(approval_mode());
sender.send(entry).unwrap();
let mut replied = tokio::spawn(receipt.reply());
let (inbox, unread) = mpsc::channel(1);
let (forwarded, ()) = tokio::join!(forward(&mut queue, inbox, None), async {
tokio::task::yield_now().await;
drop(unread);
});
assert_eq!(forwarded.settle(&mut queue, None), 0);
assert!(!queue.is_empty(), "the command is still the host's");
assert!(
tokio::time::timeout(Duration::from_millis(50), &mut replied)
.await
.is_err(),
"nothing is acknowledged before an engine takes it"
);
let (inbox, mut read) = mpsc::channel(1);
let (release, released) = oneshot::channel::<()>();
let engine = tokio::spawn(async move {
let command = read.recv().await;
let _ = released.await;
command
});
let (forwarded, ()) = tokio::join!(forward(&mut queue, inbox, None), async {
let reply = (&mut replied).await.unwrap();
assert_eq!(reply, ControlReply::Ok, "acknowledged once taken");
release.send(()).unwrap();
});
assert_eq!(forwarded.settle(&mut queue, None), 1);
assert!(queue.is_empty());
assert!(matches!(
engine.await.unwrap(),
Some(EngineCommand::ApprovalMode { .. })
));
}
#[tokio::test]
async fn a_primed_command_is_followed_like_any_other() {
let (sender, incoming) = mpsc::unbounded_channel();
let mut queue = Queue::new(incoming);
let subscribe = ControlRequest::Subscribe {
from_seq: 0,
from_snapshot: false,
};
sender.send(Entry::new(subscribe).0).unwrap();
let (entry, receipt) = Entry::new(approval_mode());
sender.send(entry).unwrap();
let mut replied = tokio::spawn(receipt.reply());
let (inbox, unread) = mpsc::channel(1);
let primed = prime(&mut queue, &inbox);
assert!(
queue.is_empty(),
"past the one no engine takes, it waits in the inbox"
);
drop(unread);
let forwarded = forward(&mut queue, inbox, primed).await;
assert_eq!(forwarded.settle(&mut queue, None), 0);
assert!(!queue.is_empty(), "the next engine gets it");
assert!(
tokio::time::timeout(Duration::from_millis(50), &mut replied)
.await
.is_err(),
"nothing is acknowledged before an engine takes it"
);
let (inbox, mut read) = mpsc::channel(1);
let primed = prime(&mut queue, &inbox);
assert!(
matches!(read.try_recv(), Ok(EngineCommand::ApprovalMode { .. })),
"it's there before the engine first looks"
);
let (forwarded, ()) = tokio::join!(forward(&mut queue, inbox, primed), async {
let reply = (&mut replied).await.unwrap();
assert_eq!(reply, ControlReply::Ok, "acknowledged once taken");
drop(read);
});
assert_eq!(forwarded.settle(&mut queue, None), 1);
assert!(queue.is_empty());
}
async fn taken_then_returned(request: ControlRequest, exit: EngineExit) -> (usize, Queue) {
let (sender, incoming) = mpsc::unbounded_channel();
let mut queue = Queue::new(incoming);
sender.send(Entry::new(request).0).unwrap();
let (inbox, mut read) = mpsc::channel(1);
let (forwarded, read_one) = tokio::join!(forward(&mut queue, inbox, None), async move {
let command = read.recv().await;
drop(read);
command.is_some()
});
assert!(read_one, "the engine took the command");
let taken = forwarded.settle(&mut queue, Some(&exit));
(taken, queue)
}
#[tokio::test]
async fn a_command_the_exit_already_did_is_never_sent_again() {
let stop = taken_then_returned(
ControlRequest::Stop,
EngineExit::Finished(RunOutcome::Stopped),
);
let (taken, queue) = stop.await;
assert_eq!(taken, 1);
assert!(
queue.is_empty(),
"the next engine doesn't stop the run again"
);
let pause = taken_then_returned(
ControlRequest::Pause {
now: true,
outside_agent: None,
},
EngineExit::Waiting(WaitReason::Paused),
);
let (taken, queue) = pause.await;
assert_eq!(taken, 1);
assert!(
queue.is_empty(),
"the next engine doesn't pause the run again"
);
}
#[tokio::test]
async fn a_command_the_exit_doesnt_explain_goes_to_the_next_engine() {
let (taken, queue) =
taken_then_returned(approval_mode(), EngineExit::Waiting(WaitReason::Approval)).await;
assert_eq!(taken, 0);
assert!(!queue.is_empty(), "a second copy rather than a lost one");
let (taken, queue) = taken_then_returned(
ControlRequest::Stop,
EngineExit::Waiting(WaitReason::Approval),
)
.await;
assert_eq!(taken, 0);
assert!(!queue.is_empty(), "the run didn't stop, so the stop waits");
}
#[tokio::test]
async fn closing_returns_what_is_left_in_order() {
let (sender, incoming) = mpsc::unbounded_channel();
let mut queue = Queue::new(incoming);
for request in [approval_mode(), ControlRequest::Stop] {
sender.send(Entry::new(request).0).unwrap();
}
let first = queue.take_all();
sender
.send(Entry::new(ControlRequest::CancelPause).0)
.unwrap();
queue.restore(first);
let left: Vec<_> = queue
.close()
.into_iter()
.map(|entry| entry.request)
.collect();
assert_eq!(
left,
[
approval_mode(),
ControlRequest::Stop,
ControlRequest::CancelPause
]
);
assert!(sender.send(Entry::new(ControlRequest::Stop).0).is_err());
}
}