use serde_json::{json, Value};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::time::Duration;
use thiserror::Error;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::unix::{OwnedReadHalf, OwnedWriteHalf};
use tokio::net::UnixStream;
use tokio::process::{Child, Command};
use tokio::sync::Mutex;
use tokio::time::{sleep, timeout};
const PONG_DEADLINE: Duration = Duration::from_secs(5);
const CONNECT_DEADLINE: Duration = Duration::from_secs(10);
const READY_DEADLINE: Duration = Duration::from_secs(15);
#[derive(Debug, Error)]
pub enum WorkerError {
#[error("io: {0}")]
Io(#[from] std::io::Error),
#[error("json: {0}")]
Json(#[from] serde_json::Error),
#[error("pong not received within 5 s")]
PongTimeout,
#[error("worker closed the connection")]
Closed,
#[error("render error '{code}' (retryable={retryable})")]
Render { code: String, retryable: bool },
#[error("worker queue is full (503)")]
QueueOverflow,
#[error("worker is draining")]
Draining,
}
#[derive(Debug, Clone)]
pub struct RenderResult {
pub html: String,
pub headers: HashMap<String, String>,
pub cache_ttl: Option<u64>,
pub cache_tags: Vec<String>,
}
struct WorkerIo {
reader: BufReader<OwnedReadHalf>,
writer: OwnedWriteHalf,
}
pub struct WorkerClient {
io: Mutex<WorkerIo>,
pub socket_path: PathBuf,
child: Mutex<Child>,
}
impl WorkerClient {
pub async fn spawn(
socket_path: PathBuf,
manifest_path: &Path,
worker_entry: &Path,
config_path: Option<&Path>,
) -> Result<Self, WorkerError> {
let mut cmd = Command::new("bun");
cmd.arg(worker_entry)
.arg("--socket")
.arg(&socket_path)
.arg("--manifest")
.arg(manifest_path)
.stdout(Stdio::null())
.stderr(Stdio::inherit())
.kill_on_drop(true);
if let Some(cfg) = config_path {
cmd.arg("--config").arg(cfg);
}
let child = cmd.spawn()?;
let stream = connect_with_retry(&socket_path, CONNECT_DEADLINE).await?;
let (r, w) = stream.into_split();
let client = Self {
io: Mutex::new(WorkerIo {
reader: BufReader::new(r),
writer: w,
}),
socket_path,
child: Mutex::new(child),
};
let msg = client.recv_with_timeout(READY_DEADLINE).await?;
if msg.get("kind").and_then(|v| v.as_str()) != Some("ready") {
return Err(WorkerError::Io(std::io::Error::other(format!(
"expected ready, got {msg}"
))));
}
Ok(client)
}
pub async fn ping(&self) -> Result<(), WorkerError> {
let mut io = self.io.lock().await;
send(&mut io.writer, &json!({ "id": 0, "kind": "ping" })).await?;
timeout(PONG_DEADLINE, async {
loop {
let v = recv(&mut io.reader).await?;
if v.get("kind").and_then(|k| k.as_str()) == Some("pong") {
return Ok(());
}
}
})
.await
.map_err(|_| WorkerError::PongTimeout)?
}
pub async fn drain(&self) -> Result<(), WorkerError> {
let mut io = self.io.lock().await;
send(&mut io.writer, &json!({ "id": 0, "kind": "drain" })).await
}
pub async fn render(
&self,
id: u32,
route: &str,
req: Value,
deadline_ms: u64,
) -> Result<RenderResult, WorkerError> {
let msg = json!({
"id": id,
"kind": "render",
"route": route,
"req": req,
"deadline_ms": deadline_ms,
});
let mut io = self.io.lock().await;
send(&mut io.writer, &msg).await?;
loop {
let v = recv(&mut io.reader).await?;
if v.get("id").and_then(|i| i.as_u64()) != Some(u64::from(id)) {
continue;
}
return match v.get("kind").and_then(|k| k.as_str()) {
Some("ok") => Ok(RenderResult {
html: v["html"].as_str().unwrap_or("").to_string(),
headers: serde_json::from_value(v["headers"].clone()).unwrap_or_default(),
cache_ttl: v["cache"]["ttl"].as_u64(),
cache_tags: serde_json::from_value(v["cache"]["tags"].clone())
.unwrap_or_default(),
}),
Some("err") => {
let code = v["error"]["code"]
.as_str()
.unwrap_or("render_failed")
.to_string();
let retryable = v["error"]["retryable"].as_bool().unwrap_or(false);
Err(match code.as_str() {
"queue_overflow" => WorkerError::QueueOverflow,
"draining" => WorkerError::Draining,
_ => WorkerError::Render { code, retryable },
})
}
_ => Err(WorkerError::Io(std::io::Error::other(
"unexpected message kind",
))),
};
}
}
pub async fn kill(&self) -> std::io::Result<()> {
self.child.lock().await.kill().await
}
pub async fn wait(&self) -> std::io::Result<std::process::ExitStatus> {
self.child.lock().await.wait().await
}
async fn recv_with_timeout(&self, dur: Duration) -> Result<Value, WorkerError> {
let mut io = self.io.lock().await;
timeout(dur, recv(&mut io.reader))
.await
.map_err(|_| WorkerError::PongTimeout)?
}
}
async fn send(writer: &mut OwnedWriteHalf, msg: &Value) -> Result<(), WorkerError> {
let mut b = serde_json::to_vec(msg)?;
b.push(b'\n');
writer.write_all(&b).await?;
writer.flush().await?;
Ok(())
}
async fn recv(reader: &mut BufReader<OwnedReadHalf>) -> Result<Value, WorkerError> {
let mut line = String::new();
let n = reader.read_line(&mut line).await?;
if n == 0 {
return Err(WorkerError::Closed);
}
Ok(serde_json::from_str(&line)?)
}
async fn connect_with_retry(path: &Path, deadline: Duration) -> Result<UnixStream, WorkerError> {
let start = std::time::Instant::now();
loop {
match UnixStream::connect(path).await {
Ok(s) => return Ok(s),
Err(e) if start.elapsed() >= deadline => return Err(e.into()),
Err(_) => sleep(Duration::from_millis(50)).await,
}
}
}