use std::collections::VecDeque;
use tokio::sync::{mpsc, watch};
use super::types::action;
use super::*;
const CHANNEL_RETENTION: usize = 8 * 1024 * 1024;
const EXITED_RETENTION: Duration = Duration::from_secs(5 * 60);
const ATTACH_CHUNK: usize = 64 * 1024;
const ATTACH_QUEUE: usize = 64;
const MAX_START_RESERVE_ROUNDS: usize = 8;
#[derive(Debug, Clone, Default)]
pub struct ExecutionSpec {
pub id: Option<String>,
pub cmd: Vec<String>,
pub env: HashMap<String, String>,
pub working_dir: String,
pub user: String,
pub tty: bool,
pub tty_size: Option<(u16, u16)>,
pub timeout_seconds: u32,
pub stdin: bool,
}
#[derive(Debug, Clone)]
pub struct ExecutionSnapshot {
pub id: String,
pub sandbox_id: SandboxId,
pub tty: bool,
pub started_at: DateTime<Utc>,
pub exited_at: Option<DateTime<Utc>>,
pub exit_status: Option<ExitStatus>,
pub error: Option<String>,
pub stdout_len: u64,
pub stderr_len: u64,
pub stdin: StdinState,
}
impl ExecutionSnapshot {
#[must_use]
pub const fn is_running(&self) -> bool {
self.exited_at.is_none()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExecutionChannel {
Stdout,
Stderr,
}
#[derive(Debug, Clone)]
pub struct ExecutionOutput {
pub channel: ExecutionChannel,
pub offset: u64,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct StdinState {
pub bytes_written: u64,
pub closed: bool,
}
#[derive(Default)]
struct ChannelBuffer {
base: u64,
data: VecDeque<u8>,
}
impl ChannelBuffer {
fn end(&self) -> u64 {
self.base + self.data.len() as u64
}
fn append(&mut self, bytes: &[u8]) {
self.data.extend(bytes);
if self.data.len() > CHANNEL_RETENTION {
let drop = self.data.len() - CHANNEL_RETENTION;
self.data.drain(..drop);
self.base += drop as u64;
}
}
fn read_from(&self, offset: u64, max: usize) -> (u64, Vec<u8>) {
let start = offset.max(self.base);
if start >= self.end() {
return (start, Vec::new());
}
let skip = usize::try_from(start - self.base).expect("offset within retained buffer");
let take = (self.data.len() - skip).min(max);
let (front, back) = self.data.as_slices();
let mut out = Vec::with_capacity(take);
if skip < front.len() {
let n = (front.len() - skip).min(take);
out.extend_from_slice(&front[skip..skip + n]);
out.extend_from_slice(&back[..take - n]);
} else {
let off = skip - front.len();
out.extend_from_slice(&back[off..off + take]);
}
(start, out)
}
}
#[derive(Default)]
struct ExecState {
stdout: ChannelBuffer,
stderr: ChannelBuffer,
stdin_written: u64,
stdin_closed: bool,
exited_at: Option<DateTime<Utc>>,
exit_status: Option<ExitStatus>,
error: Option<String>,
}
pub(super) struct Execution {
id: String,
sandbox_id: SandboxId,
tty: bool,
started_at: DateTime<Utc>,
cmd: Vec<String>,
state: Mutex<ExecState>,
version: watch::Sender<u64>,
input_tx: mpsc::Sender<ExecInputMsg>,
stdin_gate: tokio::sync::Mutex<()>,
}
impl Execution {
fn new(
id: String,
sandbox_id: SandboxId,
spec: &ExecutionSpec,
input_tx: mpsc::Sender<ExecInputMsg>,
) -> Self {
Self {
id,
sandbox_id,
tty: spec.tty,
started_at: Utc::now(),
cmd: spec.cmd.clone(),
state: Mutex::new(ExecState {
stdin_closed: !spec.stdin,
..ExecState::default()
}),
version: watch::channel(0).0,
input_tx,
stdin_gate: tokio::sync::Mutex::new(()),
}
}
fn snapshot(&self) -> ExecutionSnapshot {
let st = self.state.lock().unwrap();
ExecutionSnapshot {
id: self.id.clone(),
sandbox_id: self.sandbox_id.clone(),
tty: self.tty,
started_at: self.started_at,
exited_at: st.exited_at,
exit_status: st.exit_status,
error: st.error.clone(),
stdout_len: st.stdout.end(),
stderr_len: st.stderr.end(),
stdin: StdinState {
bytes_written: st.stdin_written,
closed: st.stdin_closed,
},
}
}
fn bump(&self) {
self.version.send_modify(|v| *v += 1);
}
fn mark_exited(&self, outcome: &std::result::Result<ExitStatus, String>) {
let mut st = self.state.lock().unwrap();
if st.exited_at.is_some() {
return;
}
st.exited_at = Some(Utc::now());
match outcome {
Ok(status) => st.exit_status = Some(*status),
Err(e) => st.error = Some(e.clone()),
}
drop(st);
self.bump();
}
fn has_exited(&self) -> bool {
self.state.lock().unwrap().exited_at.is_some()
}
fn exited_error(&self) -> VmmError {
VmmError::WrongState {
id: format!("execution '{}'", self.id),
expected: "running".into(),
actual: "exited".into(),
}
}
fn attach(
self: &Arc<Self>,
stdout_offset: u64,
stderr_offset: u64,
) -> (ExecutionSnapshot, mpsc::Receiver<ExecutionOutput>) {
let snapshot = self.snapshot();
let (tx, rx) = mpsc::channel(ATTACH_QUEUE);
tokio::spawn(pump_attach(
Arc::clone(self),
stdout_offset,
stderr_offset,
tx,
));
(snapshot, rx)
}
async fn write_stdin(&self, offset: u64, data: &[u8], eof: bool) -> Result<StdinState> {
if eof && self.tty {
return Err(VmmError::Config(
"cannot close stdin of a TTY execution; send Ctrl-D (0x04) instead".into(),
));
}
let _gate = self.stdin_gate.lock().await;
let fresh = {
let st = self.state.lock().unwrap();
if st.exited_at.is_some() {
return Err(self.exited_error());
}
if offset > st.stdin_written {
return Err(VmmError::StdinGap {
accepted: st.stdin_written,
offset,
});
}
let overlap = st.stdin_written - offset;
if overlap >= data.len() as u64 {
Vec::new()
} else {
if st.stdin_closed {
return Err(VmmError::Config("stdin is already closed".into()));
}
#[allow(
clippy::cast_possible_truncation,
reason = "overlap < data.len() which is usize"
)]
let skip = overlap as usize;
data[skip..].to_vec()
}
};
if !fresh.is_empty() {
let accepted = fresh.len() as u64;
self.input_tx
.send(ExecInputMsg::Stdin(fresh))
.await
.map_err(|_| self.exited_error())?;
self.state.lock().unwrap().stdin_written += accepted;
self.bump();
}
if eof && !self.state.lock().unwrap().stdin_closed {
let _ = self.input_tx.send(ExecInputMsg::Eof).await;
self.state.lock().unwrap().stdin_closed = true;
self.bump();
}
Ok(self.snapshot().stdin)
}
async fn signal(&self, signal: i32) -> Result<()> {
if !(1..=64).contains(&signal) {
return Err(VmmError::Config(format!("invalid signal {signal}")));
}
if self.has_exited() {
return Err(self.exited_error());
}
self.input_tx
.send(ExecInputMsg::Signal(signal))
.await
.map_err(|_| self.exited_error())
}
async fn resize(&self, width: u16, height: u16) -> Result<()> {
if !self.tty {
return Err(VmmError::Config("execution has no TTY to resize".into()));
}
if self.has_exited() {
return Err(self.exited_error());
}
self.input_tx
.send(ExecInputMsg::Resize { width, height })
.await
.map_err(|_| self.exited_error())
}
async fn wait(&self, timeout: Duration) -> ExecutionSnapshot {
let mut version = self.version.subscribe();
let deadline = tokio::time::Instant::now() + timeout;
loop {
version.borrow_and_update();
if self.has_exited() || timeout.is_zero() {
return self.snapshot();
}
if tokio::time::timeout_at(deadline, version.changed())
.await
.is_err()
{
return self.snapshot();
}
}
}
}
type ExecKey = (SandboxId, String);
#[derive(Default)]
pub(super) struct ExecutionRegistry {
inner: Mutex<RegistryInner>,
}
#[derive(Default)]
struct RegistryInner {
live: HashMap<ExecKey, Arc<Execution>>,
pending: HashMap<ExecKey, PendingStart>,
}
struct PendingStart {
cmd: Vec<String>,
done: watch::Sender<bool>,
}
enum Reserve {
Existing(ExecutionSnapshot),
Slot(SlotGuard),
AwaitPending(watch::Receiver<bool>),
}
struct SlotGuard {
registry: Arc<ExecutionRegistry>,
key: ExecKey,
committed: bool,
}
impl SlotGuard {
fn commit(mut self, exec: &Arc<Execution>) {
let mut inner = self.registry.inner.lock().unwrap();
let pending = inner.pending.remove(&self.key);
inner.live.insert(self.key.clone(), Arc::clone(exec));
drop(inner);
if let Some(pending) = pending {
let _ = pending.done.send(true);
}
self.committed = true;
}
}
impl Drop for SlotGuard {
fn drop(&mut self) {
if !self.committed {
let pending = self
.registry
.inner
.lock()
.unwrap()
.pending
.remove(&self.key);
if let Some(pending) = pending {
let _ = pending.done.send(true);
}
}
}
}
impl ExecutionRegistry {
fn reserve(self: &Arc<Self>, key: ExecKey, cmd: &[String]) -> Result<Reserve> {
let mut inner = self.inner.lock().unwrap();
if let Some(existing) = inner.live.get(&key) {
if existing.cmd == cmd {
return Ok(Reserve::Existing(existing.snapshot()));
}
return Err(VmmError::AlreadyExists(format!(
"execution '{}' (id reused for a different command)",
key.1
)));
}
if let Some(pending) = inner.pending.get(&key) {
if pending.cmd == cmd {
return Ok(Reserve::AwaitPending(pending.done.subscribe()));
}
return Err(VmmError::AlreadyExists(format!(
"execution '{}' (id reused for a different command)",
key.1
)));
}
inner.pending.insert(
key.clone(),
PendingStart {
cmd: cmd.to_vec(),
done: watch::channel(false).0,
},
);
Ok(Reserve::Slot(SlotGuard {
registry: Arc::clone(self),
key,
committed: false,
}))
}
fn get(&self, sandbox_id: &str, execution_id: &str) -> Result<Arc<Execution>> {
self.inner
.lock()
.unwrap()
.live
.get(&(sandbox_id.to_owned(), execution_id.to_owned()))
.cloned()
.ok_or_else(|| {
VmmError::NotFound(format!(
"execution '{execution_id}' in sandbox '{sandbox_id}'"
))
})
}
fn remove_generation(&self, exec: &Arc<Execution>) {
let key = (exec.sandbox_id.clone(), exec.id.clone());
let mut inner = self.inner.lock().unwrap();
if inner.live.get(&key).is_some_and(|e| Arc::ptr_eq(e, exec)) {
inner.live.remove(&key);
}
}
fn interrupt_sandbox(&self, sandbox_id: &str) {
let executions: Vec<Arc<Execution>> = {
let inner = self.inner.lock().unwrap();
inner
.live
.iter()
.filter(|((sid, _), _)| sid == sandbox_id)
.map(|(_, exec)| Arc::clone(exec))
.collect()
};
for exec in executions {
exec.mark_exited(&Err("sandbox stopped".to_owned()));
}
}
}
pub(super) fn spawn_teardown_purge(
registry: Arc<ExecutionRegistry>,
mut events: broadcast::Receiver<SandboxEvent>,
) {
tokio::spawn(async move {
loop {
match events.recv().await {
Ok(ev) if ev.is_terminal() => {
registry.interrupt_sandbox(&ev.sandbox_id);
}
Ok(_) | Err(broadcast::error::RecvError::Lagged(_)) => {}
Err(broadcast::error::RecvError::Closed) => break,
}
}
});
}
fn abort_workload(
id: &SandboxId,
error: &str,
instances: &InstanceMap,
events_tx: &broadcast::Sender<SandboxEvent>,
) {
let arc = instances.read().unwrap().get(id).cloned();
if let Some(arc) = arc {
let mut inst = arc.lock().unwrap();
inst.last_exited_at = Some(Utc::now());
if inst.state == SandboxState::Running {
inst.state = SandboxState::Ready;
}
}
let _ = events_tx.send(SandboxEvent::new(id, action::IDLE).with_attr("error", error));
}
async fn run_session(
exec: Arc<Execution>,
mut output_rx: mpsc::Receiver<Result<OutputChunk>>,
instances: InstanceMap,
events_tx: broadcast::Sender<SandboxEvent>,
registry: Arc<ExecutionRegistry>,
) {
let outcome: std::result::Result<ExitStatus, String> = loop {
match output_rx.recv().await {
Some(Ok(OutputChunk::Stdout(data))) => {
exec.state.lock().unwrap().stdout.append(&data);
exec.bump();
}
Some(Ok(OutputChunk::Stderr(data))) => {
exec.state.lock().unwrap().stderr.append(&data);
exec.bump();
}
Some(Ok(OutputChunk::Exit(status))) => break Ok(status),
Some(Err(e)) => break Err(e.to_string()),
None => break Err("execution session closed before exit".to_owned()),
}
};
match &outcome {
Ok(status) => {
super::workload::finish_workload(&exec.sandbox_id, *status, &instances, &events_tx);
}
Err(e) => {
warn!(sandbox_id = %exec.sandbox_id, execution_id = %exec.id, error = %e,
"execution session broke before exit");
abort_workload(&exec.sandbox_id, e, &instances, &events_tx);
}
}
exec.mark_exited(&outcome);
tokio::time::sleep(EXITED_RETENTION).await;
registry.remove_generation(&exec);
}
impl SandboxManager {
pub async fn start_execution(
&self,
sandbox_id: &SandboxId,
spec: ExecutionSpec,
) -> Result<ExecutionSnapshot> {
let id = match &spec.id {
Some(id) if !id.is_empty() => {
super::validate_id("execution id", id)?;
id.clone()
}
_ => Uuid::new_v4().to_string(),
};
if spec.cmd.is_empty() {
return Err(VmmError::Config(
"execution command must not be empty".into(),
));
}
let mut slot = None;
for _ in 0..MAX_START_RESERVE_ROUNDS {
match self
.executions
.reserve((sandbox_id.clone(), id.clone()), &spec.cmd)?
{
Reserve::Existing(snapshot) => return Ok(snapshot),
Reserve::Slot(guard) => {
slot = Some(guard);
break;
}
Reserve::AwaitPending(mut done) => {
let _ = done.changed().await;
}
}
}
let Some(slot) = slot else {
return Err(VmmError::AlreadyExists(format!(
"execution '{id}' (concurrent starts did not settle)"
)));
};
let uds_path = self.require_ready_vsock(sandbox_id)?;
let start = StartCommand {
cmd: spec.cmd.clone(),
env: spec.env.clone(),
working_dir: spec.working_dir.clone(),
user: spec.user.clone(),
tty: spec.tty,
tty_width: spec.tty_size.map_or(80, |(w, _)| w),
tty_height: spec.tty_size.map_or(24, |(_, h)| h),
timeout_seconds: spec.timeout_seconds,
};
super::workload::claim_running(sandbox_id, &self.instances)?;
let (input_tx, output_rx) = match vsock::exec(&uds_path, start).await {
Ok(pair) => pair,
Err(e) => {
super::workload::release_running(sandbox_id, &self.instances);
return Err(e);
}
};
if !spec.stdin && !spec.tty {
let _ = input_tx.send(ExecInputMsg::Eof).await;
}
let exec = Arc::new(Execution::new(id, sandbox_id.clone(), &spec, input_tx));
slot.commit(&exec);
let _ = self
.events_tx
.send(SandboxEvent::new(sandbox_id, action::RUNNING));
tokio::spawn(run_session(
Arc::clone(&exec),
output_rx,
Arc::clone(&self.instances),
self.events_tx.clone(),
Arc::clone(&self.executions),
));
Ok(exec.snapshot())
}
pub fn attach_execution(
&self,
sandbox_id: &str,
execution_id: &str,
stdout_offset: u64,
stderr_offset: u64,
) -> Result<(ExecutionSnapshot, mpsc::Receiver<ExecutionOutput>)> {
let exec = self.executions.get(sandbox_id, execution_id)?;
Ok(exec.attach(stdout_offset, stderr_offset))
}
pub async fn write_stdin(
&self,
sandbox_id: &str,
execution_id: &str,
offset: u64,
data: &[u8],
eof: bool,
) -> Result<StdinState> {
self.executions
.get(sandbox_id, execution_id)?
.write_stdin(offset, data, eof)
.await
}
pub fn stdin_status(&self, sandbox_id: &str, execution_id: &str) -> Result<StdinState> {
Ok(self
.executions
.get(sandbox_id, execution_id)?
.snapshot()
.stdin)
}
pub async fn signal_execution(
&self,
sandbox_id: &str,
execution_id: &str,
signal: i32,
) -> Result<()> {
self.executions
.get(sandbox_id, execution_id)?
.signal(signal)
.await
}
pub async fn resize_execution(
&self,
sandbox_id: &str,
execution_id: &str,
width: u16,
height: u16,
) -> Result<()> {
self.executions
.get(sandbox_id, execution_id)?
.resize(width, height)
.await
}
pub async fn wait_execution(
&self,
sandbox_id: &str,
execution_id: &str,
timeout: Duration,
) -> Result<ExecutionSnapshot> {
Ok(self
.executions
.get(sandbox_id, execution_id)?
.wait(timeout)
.await)
}
}
async fn pump_attach(
exec: Arc<Execution>,
mut stdout_cursor: u64,
mut stderr_cursor: u64,
tx: mpsc::Sender<ExecutionOutput>,
) {
let mut version = exec.version.subscribe();
loop {
version.borrow_and_update();
loop {
let (chunk, exited) = {
let st = exec.state.lock().unwrap();
let (start, data) = st.stdout.read_from(stdout_cursor, ATTACH_CHUNK);
if data.is_empty() {
let (start, data) = st.stderr.read_from(stderr_cursor, ATTACH_CHUNK);
if data.is_empty() {
(None, st.exited_at.is_some())
} else {
stderr_cursor = start + data.len() as u64;
(
Some(ExecutionOutput {
channel: ExecutionChannel::Stderr,
offset: start,
data,
}),
false,
)
}
} else {
stdout_cursor = start + data.len() as u64;
(
Some(ExecutionOutput {
channel: ExecutionChannel::Stdout,
offset: start,
data,
}),
false,
)
}
};
match chunk {
Some(chunk) => {
if tx.send(chunk).await.is_err() {
return; }
}
None if exited => return, None => break, }
}
let _ = version.changed().await;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn stdin_spec() -> ExecutionSpec {
ExecutionSpec {
cmd: vec!["cat".into()],
stdin: true,
..ExecutionSpec::default()
}
}
fn test_execution(spec: &ExecutionSpec) -> (Arc<Execution>, mpsc::Receiver<ExecInputMsg>) {
let (tx, rx) = mpsc::channel(8);
let exec = Arc::new(Execution::new(
"exec-1".into(),
"sandbox-1".into(),
spec,
tx,
));
(exec, rx)
}
#[test]
fn channel_buffer_addresses_bytes_absolutely() {
let mut buf = ChannelBuffer::default();
buf.append(b"hello ");
buf.append(b"world");
assert_eq!(buf.end(), 11);
let (start, data) = buf.read_from(0, 1024);
assert_eq!((start, data.as_slice()), (0, b"hello world".as_slice()));
let (start, data) = buf.read_from(6, 3);
assert_eq!((start, data.as_slice()), (6, b"wor".as_slice()));
let (start, data) = buf.read_from(11, 1024);
assert_eq!((start, data.len()), (11, 0));
}
#[test]
fn channel_buffer_trims_to_retention_and_advances_base() {
let mut buf = ChannelBuffer::default();
let chunk = vec![7u8; CHANNEL_RETENTION];
buf.append(&chunk);
buf.append(b"tail");
assert_eq!(buf.base, 4);
assert_eq!(buf.end(), CHANNEL_RETENTION as u64 + 4);
let (start, data) = buf.read_from(0, 8);
assert_eq!(start, 4);
assert_eq!(data.len(), 8);
}
#[tokio::test]
async fn write_stdin_dedupes_retries_and_rejects_gaps() {
let (exec, mut rx) = test_execution(&stdin_spec());
let st = exec.write_stdin(0, b"hello", false).await.unwrap();
assert_eq!(st.bytes_written, 5);
let st = exec.write_stdin(0, b"hello", false).await.unwrap();
assert_eq!(st.bytes_written, 5);
let st = exec.write_stdin(3, b"lo world", false).await.unwrap();
assert_eq!(st.bytes_written, 11);
let err = exec.write_stdin(99, b"x", false).await.unwrap_err();
assert!(matches!(err, VmmError::StdinGap { accepted: 11, .. }));
let mut forwarded = Vec::new();
while let Ok(msg) = rx.try_recv() {
match msg {
ExecInputMsg::Stdin(data) => forwarded.extend_from_slice(&data),
other => panic!("unexpected input message: {other:?}"),
}
}
assert_eq!(forwarded, b"hello world");
}
#[tokio::test]
async fn write_stdin_eof_closes_and_tty_rejects_eof() {
let (exec, mut rx) = test_execution(&stdin_spec());
let st = exec.write_stdin(0, b"in", true).await.unwrap();
assert!(st.closed);
assert!(matches!(rx.recv().await, Some(ExecInputMsg::Stdin(_))));
assert!(matches!(rx.recv().await, Some(ExecInputMsg::Eof)));
let err = exec.write_stdin(2, b"more", false).await.unwrap_err();
assert!(matches!(err, VmmError::Config(_)));
let st = exec.write_stdin(0, b"in", false).await.unwrap();
assert_eq!(st.bytes_written, 2);
let tty_spec = ExecutionSpec {
tty: true,
..stdin_spec()
};
let (tty_exec, _tty_rx) = test_execution(&tty_spec);
let err = tty_exec.write_stdin(0, b"", true).await.unwrap_err();
assert!(matches!(err, VmmError::Config(_)));
}
#[tokio::test]
async fn attach_replays_from_offsets_and_closes_after_exit() {
let (exec, _rx) = test_execution(&stdin_spec());
{
let mut st = exec.state.lock().unwrap();
st.stdout.append(b"out-data");
st.stderr.append(b"err");
}
exec.bump();
let (snapshot, mut out) = exec.attach(2, 0);
assert!(snapshot.is_running());
let first = out.recv().await.unwrap();
assert_eq!(first.channel, ExecutionChannel::Stdout);
assert_eq!(first.offset, 2);
assert_eq!(first.data, b"t-data");
let second = out.recv().await.unwrap();
assert_eq!(second.channel, ExecutionChannel::Stderr);
assert_eq!(second.offset, 0);
assert_eq!(second.data, b"err");
exec.state.lock().unwrap().stdout.append(b"+live");
exec.bump();
let live = out.recv().await.unwrap();
assert_eq!(live.offset, 8);
assert_eq!(live.data, b"+live");
exec.mark_exited(&Ok(ExitStatus::Code(0)));
assert!(out.recv().await.is_none());
let done = exec.wait(Duration::ZERO).await;
assert_eq!(done.exit_status, Some(ExitStatus::Code(0)));
}
#[tokio::test]
async fn wait_times_out_then_resolves_on_exit() {
let (exec, _rx) = test_execution(&stdin_spec());
assert!(exec.wait(Duration::ZERO).await.is_running());
assert!(exec.wait(Duration::from_millis(20)).await.is_running());
let exiter = {
let exec = Arc::clone(&exec);
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(20)).await;
exec.mark_exited(&Ok(ExitStatus::Signaled(9)));
})
};
let snap = exec.wait(Duration::from_secs(5)).await;
assert_eq!(snap.exit_status, Some(ExitStatus::Signaled(9)));
exiter.await.unwrap();
}
#[tokio::test]
async fn signal_and_resize_validate_state() {
let (exec, mut rx) = test_execution(&stdin_spec());
exec.signal(15).await.unwrap();
assert!(matches!(rx.recv().await, Some(ExecInputMsg::Signal(15))));
assert!(matches!(
exec.signal(0).await.unwrap_err(),
VmmError::Config(_)
));
assert!(matches!(
exec.resize(80, 24).await.unwrap_err(),
VmmError::Config(_)
));
exec.mark_exited(&Ok(ExitStatus::Code(0)));
assert!(matches!(
exec.signal(9).await.unwrap_err(),
VmmError::WrongState { .. }
));
}
#[tokio::test]
async fn interrupt_sandbox_marks_running_executions_but_keeps_them_readable() {
let registry = Arc::new(ExecutionRegistry::default());
let (exec, _rx) = test_execution(&stdin_spec());
registry
.inner
.lock()
.unwrap()
.live
.insert(("sandbox-1".into(), "exec-1".into()), Arc::clone(&exec));
registry.interrupt_sandbox("sandbox-1");
assert!(registry.get("sandbox-1", "exec-1").is_ok());
let snap = exec.snapshot();
assert!(!snap.is_running());
assert_eq!(snap.error.as_deref(), Some("sandbox stopped"));
let (done, _rx2) = test_execution(&stdin_spec());
done.mark_exited(&Ok(ExitStatus::Code(3)));
registry
.inner
.lock()
.unwrap()
.live
.insert(("sandbox-1".into(), "exec-2".into()), Arc::clone(&done));
registry.interrupt_sandbox("sandbox-1");
assert_eq!(done.snapshot().exit_status, Some(ExitStatus::Code(3)));
}
#[tokio::test]
async fn start_reservation_is_idempotent_per_command() {
let registry = Arc::new(ExecutionRegistry::default());
let key = ("s".to_owned(), "e".to_owned());
let cmd = vec!["echo".to_owned()];
let Reserve::Slot(slot) = registry.reserve(key.clone(), &cmd).unwrap() else {
panic!("expected a fresh slot");
};
let (tx, _rx) = mpsc::channel(1);
let exec = Arc::new(Execution::new(
"e".into(),
"s".into(),
&ExecutionSpec {
cmd: cmd.clone(),
..ExecutionSpec::default()
},
tx,
));
slot.commit(&exec);
assert!(matches!(
registry.reserve(key.clone(), &cmd).unwrap(),
Reserve::Existing(_)
));
assert!(matches!(
registry.reserve(key, &["other".to_owned()]),
Err(VmmError::AlreadyExists(_))
));
let key2 = ("s".to_owned(), "e2".to_owned());
{
let Reserve::Slot(_slot) = registry.reserve(key2.clone(), &cmd).unwrap() else {
panic!("expected a fresh slot");
};
assert!(matches!(
registry.reserve(key2.clone(), &cmd).unwrap(),
Reserve::AwaitPending(_)
));
assert!(matches!(
registry.reserve(key2.clone(), &["other".to_owned()]),
Err(VmmError::AlreadyExists(_))
));
}
assert!(matches!(
registry.reserve(key2, &cmd).unwrap(),
Reserve::Slot(_)
));
}
#[tokio::test]
async fn a_pending_start_wakes_matching_retries_with_the_committed_execution() {
let registry = Arc::new(ExecutionRegistry::default());
let key = ("s".to_owned(), "e".to_owned());
let cmd = vec!["sleep".to_owned()];
let Reserve::Slot(slot) = registry.reserve(key.clone(), &cmd).unwrap() else {
panic!("expected a fresh slot");
};
let Reserve::AwaitPending(mut waiter) = registry.reserve(key.clone(), &cmd).unwrap() else {
panic!("a matching retry must await the in-flight start");
};
let (tx, _rx) = mpsc::channel(1);
let exec = Arc::new(Execution::new(
"e".into(),
"s".into(),
&ExecutionSpec {
cmd: cmd.clone(),
..ExecutionSpec::default()
},
tx,
));
slot.commit(&exec);
waiter.changed().await.expect("pending start signals");
assert!(matches!(
registry.reserve(key.clone(), &cmd).unwrap(),
Reserve::Existing(_)
));
let key2 = ("s".to_owned(), "e2".to_owned());
let Reserve::Slot(failed) = registry.reserve(key2.clone(), &cmd).unwrap() else {
panic!("expected a fresh slot");
};
let Reserve::AwaitPending(mut waiter) = registry.reserve(key2.clone(), &cmd).unwrap()
else {
panic!("a matching retry must await the in-flight start");
};
drop(failed);
waiter.changed().await.expect("unwound start signals");
assert!(matches!(
registry.reserve(key2, &cmd).unwrap(),
Reserve::Slot(_)
));
}
}