use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, Lines};
use tokio::process::{Child, Command};
use tokio::sync::{Mutex, oneshot};
use crate::protocol::{
CancelResult, ExecuteResult, InitializeResult, ListResult, ListSamplesParams,
ListSamplesResult, Notification, PROTOCOL_VERSION, Request, Response, RpcError, RunParams,
RunResult, ScoreParams, capabilities,
};
use crate::{Params, Trial};
type EventCb = Arc<dyn Fn(&Notification) + Send + Sync>;
type Pending =
Arc<std::sync::Mutex<HashMap<u64, oneshot::Sender<Result<serde_json::Value, RpcError>>>>>;
type BoxedWriter = Box<dyn AsyncWrite + Send + Unpin>;
type BoxedReader = Box<dyn AsyncRead + Send + Unpin>;
#[derive(Clone)]
pub struct HostHandle {
stdin: Arc<Mutex<BoxedWriter>>,
pending: Pending,
next_id: Arc<AtomicU64>,
supports_cancel: Arc<AtomicBool>,
}
impl HostHandle {
pub async fn initialize(&self, host_name: &str) -> Result<InitializeResult, RpcError> {
let value = self
.request(
"initialize",
serde_json::json!({ "protocol_version": PROTOCOL_VERSION, "host": host_name }),
false,
)
.await?;
let info: InitializeResult =
serde_json::from_value(value).map_err(|e| RpcError::new(e.to_string()))?;
if !crate::protocol::version_compatible(&info.protocol_version) {
return Err(RpcError::new(format!(
"incompatible protocol: study speaks {}, host speaks {} (major mismatch)",
info.protocol_version, PROTOCOL_VERSION
)));
}
let can_cancel = info.capabilities.iter().any(|c| c == capabilities::CANCEL);
self.supports_cancel.store(can_cancel, Ordering::Relaxed);
Ok(info)
}
pub async fn list(&self) -> Result<ListResult, RpcError> {
let value = self.request("list", serde_json::Value::Null, false).await?;
serde_json::from_value(value).map_err(|e| RpcError::new(e.to_string()))
}
pub async fn list_samples(
&self,
eval: &str,
cursor: &str,
) -> Result<ListSamplesResult, RpcError> {
let params = ListSamplesParams {
eval: eval.into(),
cursor: cursor.into(),
};
let value = self
.request("list_samples", serde_json::to_value(params).unwrap(), false)
.await?;
serde_json::from_value(value).map_err(|e| RpcError::new(e.to_string()))
}
pub async fn list_complete(&self) -> Result<ListResult, RpcError> {
let mut listing = self.list().await?;
for eval in &mut listing.evals {
let mut cursor = eval.next_cursor.take();
while let Some(c) = cursor {
let page = self.list_samples(&eval.name, &c).await?;
eval.samples.extend(page.samples);
cursor = page.next_cursor;
}
}
Ok(listing)
}
pub fn supports_cancel(&self) -> bool {
self.supports_cancel.load(Ordering::Relaxed)
}
pub async fn cancel(&self, run_id: u64) -> Result<bool, RpcError> {
if !self.supports_cancel.load(Ordering::Relaxed) {
return Ok(false);
}
let value = self
.request("cancel", serde_json::json!({ "id": run_id }), false)
.await?;
let result: CancelResult =
serde_json::from_value(value).map_err(|e| RpcError::new(e.to_string()))?;
Ok(result.cancelled)
}
pub async fn run(
&self,
eval: &str,
sample: &str,
target: &str,
params: &Params,
trial: Trial,
) -> Result<RunResult, RpcError> {
let params = run_params(eval, sample, target, params, trial);
let value = self
.request("run", serde_json::to_value(params).unwrap(), true)
.await?;
serde_json::from_value(value).map_err(|e| RpcError::new(e.to_string()))
}
pub async fn execute(
&self,
eval: &str,
sample: &str,
target: &str,
params: &Params,
trial: Trial,
) -> Result<ExecuteResult, RpcError> {
let params = run_params(eval, sample, target, params, trial);
let value = self
.request("execute", serde_json::to_value(params).unwrap(), true)
.await?;
let mut result: ExecuteResult =
serde_json::from_value(value).map_err(|e| RpcError::new(e.to_string()))?;
result.transcript.project_trajectory();
Ok(result)
}
pub async fn score(&self, captured: &ExecuteResult) -> Result<RunResult, RpcError> {
let params = ScoreParams {
eval: captured.eval.clone(),
sample: captured.sample.clone(),
target: captured.target.clone(),
params: captured.params.clone(),
trial: captured.trial,
trials: captured.trials,
seed: captured.seed,
transcript: captured.transcript.clone(),
};
let value = self
.request("score", serde_json::to_value(params).unwrap(), true)
.await?;
serde_json::from_value(value).map_err(|e| RpcError::new(e.to_string()))
}
async fn request(
&self,
method: &str,
params: serde_json::Value,
cancelable: bool,
) -> Result<serde_json::Value, RpcError> {
let id = self.next_id.fetch_add(1, Ordering::SeqCst) + 1;
let (tx, rx) = oneshot::channel();
self.pending
.lock()
.expect("pending mutex poisoned")
.insert(id, tx);
let mut guard = RequestGuard {
id,
pending: self.pending.clone(),
cancel: None,
completed: false,
};
let request = Request {
id,
method: method.into(),
params,
};
let mut line = serde_json::to_vec(&request).map_err(|e| RpcError::new(e.to_string()))?;
line.push(b'\n');
{
let mut stdin = self.stdin.lock().await;
stdin
.write_all(&line)
.await
.map_err(|e| RpcError::new(e.to_string()))?;
stdin
.flush()
.await
.map_err(|e| RpcError::new(e.to_string()))?;
}
if cancelable && self.supports_cancel.load(Ordering::Relaxed) {
guard.cancel = Some((self.stdin.clone(), self.next_id.clone()));
}
let out = match rx.await {
Ok(result) => result,
Err(_) => Err(RpcError::new("study closed the connection")),
};
guard.completed = true;
out
}
}
struct RequestGuard {
id: u64,
pending: Pending,
cancel: Option<(Arc<Mutex<BoxedWriter>>, Arc<AtomicU64>)>,
completed: bool,
}
impl Drop for RequestGuard {
fn drop(&mut self) {
self.pending
.lock()
.expect("pending mutex poisoned")
.remove(&self.id);
if self.completed {
return;
}
if let Some((stdin, next_id)) = self.cancel.take() {
let run_id = self.id;
if let Ok(rt) = tokio::runtime::Handle::try_current() {
rt.spawn(async move {
let _ = send_cancel(&stdin, &next_id, run_id).await;
});
}
}
}
}
fn run_params(eval: &str, sample: &str, target: &str, params: &Params, trial: Trial) -> RunParams {
RunParams {
eval: eval.into(),
sample: sample.into(),
target: target.into(),
params: params.clone(),
trial: trial.index,
trials: trial.count,
seed: trial.seed,
}
}
async fn send_cancel(
stdin: &Arc<Mutex<BoxedWriter>>,
next_id: &Arc<AtomicU64>,
run_id: u64,
) -> std::io::Result<()> {
let id = next_id.fetch_add(1, Ordering::SeqCst) + 1;
let request = Request {
id,
method: "cancel".into(),
params: serde_json::json!({ "id": run_id }),
};
let mut line = serde_json::to_vec(&request).unwrap_or_default();
line.push(b'\n');
let mut stdin = stdin.lock().await;
stdin.write_all(&line).await?;
stdin.flush().await
}
pub struct Host {
child: Option<Child>,
handle: HostHandle,
reader: Option<tokio::task::JoinHandle<()>>,
on_event: Arc<std::sync::Mutex<EventCb>>,
}
impl Host {
pub async fn spawn(mut command: Command) -> std::io::Result<Self> {
command
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit());
let mut child = command.spawn()?;
let stdin = child.stdin.take().expect("piped stdin");
let stdout = child.stdout.take().expect("piped stdout");
Ok(Self::with_io(
Some(child),
Box::new(stdout),
Box::new(stdin),
))
}
pub fn connect<R, W>(reader: R, writer: W) -> Self
where
R: AsyncRead + Send + Unpin + 'static,
W: AsyncWrite + Send + Unpin + 'static,
{
Self::with_io(None, Box::new(reader), Box::new(writer))
}
fn with_io(child: Option<Child>, reader: BoxedReader, writer: BoxedWriter) -> Self {
let pending: Pending = Arc::new(std::sync::Mutex::new(HashMap::new()));
let on_event: Arc<std::sync::Mutex<EventCb>> =
Arc::new(std::sync::Mutex::new(Arc::new(|_: &Notification| {})));
let reader = tokio::spawn(reader_loop(
BufReader::new(reader).lines(),
pending.clone(),
on_event.clone(),
));
Self {
child,
handle: HostHandle {
stdin: Arc::new(Mutex::new(writer)),
pending,
next_id: Arc::new(AtomicU64::new(0)),
supports_cancel: Arc::new(AtomicBool::new(false)),
},
reader: Some(reader),
on_event,
}
}
pub fn on_event(self, f: impl Fn(&Notification) + Send + Sync + 'static) -> Self {
*self.on_event.lock().expect("on_event mutex poisoned") = Arc::new(f);
self
}
pub fn handle(&self) -> HostHandle {
self.handle.clone()
}
pub async fn initialize(&self, host_name: &str) -> Result<InitializeResult, RpcError> {
self.handle.initialize(host_name).await
}
pub async fn list(&self) -> Result<ListResult, RpcError> {
self.handle.list().await
}
pub async fn list_complete(&self) -> Result<ListResult, RpcError> {
self.handle.list_complete().await
}
pub async fn run(
&self,
eval: &str,
sample: &str,
target: &str,
params: &Params,
trial: Trial,
) -> Result<RunResult, RpcError> {
self.handle.run(eval, sample, target, params, trial).await
}
pub async fn execute(
&self,
eval: &str,
sample: &str,
target: &str,
params: &Params,
trial: Trial,
) -> Result<ExecuteResult, RpcError> {
self.handle
.execute(eval, sample, target, params, trial)
.await
}
pub async fn score(&self, captured: &ExecuteResult) -> Result<RunResult, RpcError> {
self.handle.score(captured).await
}
pub async fn cancel(&self, run_id: u64) -> Result<bool, RpcError> {
self.handle.cancel(run_id).await
}
pub async fn shutdown(mut self) -> std::io::Result<()> {
drop(self.handle);
if let Some(reader) = self.reader.take() {
let _ = reader.await;
}
match self.child.take() {
Some(mut child) => child.wait().await.map(|_| ()),
None => Ok(()),
}
}
}
enum Inbound {
Response(Response),
Notification(Notification),
Request(Request),
Junk,
}
fn classify(line: &str) -> Inbound {
let has_method = serde_json::from_str::<serde_json::Value>(line)
.ok()
.and_then(|v| v.get("method").map(|m| !m.is_null()))
.unwrap_or(false);
if has_method {
if let Ok(req) = serde_json::from_str::<Request>(line) {
return Inbound::Request(req);
}
if let Ok(note) = serde_json::from_str::<Notification>(line) {
return Inbound::Notification(note);
}
return Inbound::Junk;
}
match serde_json::from_str::<Response>(line) {
Ok(resp) => Inbound::Response(resp),
Err(_) => Inbound::Junk,
}
}
async fn reader_loop(
mut lines: Lines<BufReader<BoxedReader>>,
pending: Pending,
on_event: Arc<std::sync::Mutex<EventCb>>,
) {
while let Ok(Some(line)) = lines.next_line().await {
if line.trim().is_empty() {
continue;
}
match classify(&line) {
Inbound::Response(response) => {
let result = match (response.result, response.error) {
(Some(result), _) => Ok(result),
(None, Some(err)) => Err(err),
(None, None) => Err(RpcError::new("empty response")),
};
if let Some(tx) = pending
.lock()
.expect("pending mutex poisoned")
.remove(&response.id)
{
let _ = tx.send(result);
}
}
Inbound::Notification(notification) => {
let cb = on_event.lock().expect("on_event mutex poisoned").clone();
cb(¬ification);
}
Inbound::Request(req) => {
let cb = on_event.lock().expect("on_event mutex poisoned").clone();
cb(&Notification {
method: "log".into(),
params: serde_json::json!({
"message": format!("ignoring unsupported host request: {}", req.method)
}),
});
}
Inbound::Junk => {}
}
}
let mut pending = pending.lock().expect("pending mutex poisoned");
for (_, tx) in pending.drain() {
let _ = tx.send(Err(RpcError::new("study closed the connection")));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classifies_response_notification_and_reverse_request() {
assert!(matches!(
classify(r#"{"id":3,"result":{"ok":true}}"#),
Inbound::Response(r) if r.id == 3
));
assert!(matches!(
classify(r#"{"method":"event","params":{"kind":"started"}}"#),
Inbound::Notification(n) if n.method == "event"
));
assert!(matches!(
classify(r#"{"id":1,"method":"broker_model","params":{}}"#),
Inbound::Request(r) if r.method == "broker_model" && r.id == 1
));
}
#[tokio::test]
async fn reverse_request_does_not_complete_a_pending_host_request() {
let pending: Pending = Arc::new(std::sync::Mutex::new(HashMap::new()));
let (tx, rx) = oneshot::channel();
pending.lock().unwrap().insert(1, tx);
match classify(r#"{"id":1,"method":"ping","params":{}}"#) {
Inbound::Request(_) => {} other => panic!(
"reverse request misclassified as {}",
match other {
Inbound::Response(_) => "response",
Inbound::Notification(_) => "notification",
Inbound::Junk => "junk",
Inbound::Request(_) => unreachable!(),
}
),
}
assert!(pending.lock().unwrap().contains_key(&1));
drop(pending);
assert!(rx.await.is_err(), "waiter must not have been completed");
}
}