Skip to content

Commit 6fbd1ce

Browse files
committed
fix(vmm): bind swtpm lifecycle to its VM
1 parent 5c1daaa commit 6fbd1ce

1 file changed

Lines changed: 132 additions & 0 deletions

File tree

dstack/vmm/src/app.rs

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,26 @@ pub struct App {
255255
pub(crate) pull_status: Arc<Mutex<std::collections::HashMap<String, PullStatus>>>,
256256
}
257257

258+
#[derive(Debug, PartialEq, Eq)]
259+
enum DependencyAction {
260+
KeepRunning,
261+
StopDependents,
262+
StopVm,
263+
Finished,
264+
}
265+
266+
fn dependency_action(main_running: bool, dependents_running: &[bool]) -> DependencyAction {
267+
if dependents_running.is_empty() {
268+
DependencyAction::Finished
269+
} else if !main_running {
270+
DependencyAction::StopDependents
271+
} else if dependents_running.iter().any(|running| !running) {
272+
DependencyAction::StopVm
273+
} else {
274+
DependencyAction::KeepRunning
275+
}
276+
}
277+
258278
impl App {
259279
pub(crate) fn lock(&self) -> MutexGuard<'_, AppState> {
260280
self.state.lock().or_panic("mutex poisoned")
@@ -278,6 +298,7 @@ impl App {
278298
cid_pool,
279299
vms: HashMap::new(),
280300
active_forwards: HashMap::new(),
301+
dependency_monitors: HashSet::new(),
281302
})),
282303
config: Arc::new(config),
283304
forward_service: Arc::new(tokio::sync::Mutex::new(ForwardService::new())),
@@ -376,6 +397,7 @@ impl App {
376397
vm_state.config.clone()
377398
};
378399
if !is_running {
400+
self.wait_for_dependent_processes_stopped(id).await?;
379401
let work_dir = self.work_dir(id);
380402
for path in [work_dir.serial_pty(), work_dir.qmp_socket()] {
381403
if path.symlink_metadata().is_ok() {
@@ -397,6 +419,7 @@ impl App {
397419
let vm_state = state.get_mut(id).context("VM not found")?;
398420
vm_state.state.runtime_networks = runtime_networks;
399421
}
422+
let has_dependent_processes = processes.len() > 1;
400423
for process in processes {
401424
if let Err(err) = self.supervisor.deploy(&process).await {
402425
self.stop_dependent_processes(id).await;
@@ -413,6 +436,9 @@ impl App {
413436
.with_context(|| format!("failed to start process {}", process.id));
414437
}
415438
}
439+
if has_dependent_processes {
440+
self.spawn_dependency_monitor(id);
441+
}
416442

417443
let mut state = self.lock();
418444
let vm_state = state.get_mut(id).context("VM not found")?;
@@ -451,6 +477,85 @@ impl App {
451477
}
452478
}
453479

480+
async fn wait_for_dependent_processes_stopped(&self, id: &str) -> Result<()> {
481+
for _ in 0..50 {
482+
let running = self.supervisor.list().await?.into_iter().any(|process| {
483+
let note = serde_json::from_str::<ProcessAnnotation>(&process.config.note)
484+
.unwrap_or_default();
485+
note.live_for.as_deref() == Some(id) && process.state.status.is_running()
486+
});
487+
if !running {
488+
return Ok(());
489+
}
490+
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
491+
}
492+
bail!("timed out waiting for dependent processes of VM {id} to stop")
493+
}
494+
495+
/// Keep auxiliary processes fail-closed with their owning VM. If QEMU
496+
/// exits, all dependents are stopped. If a required dependent exits first,
497+
/// QEMU is stopped rather than continuing with a broken emulated device.
498+
fn spawn_dependency_monitor(&self, id: &str) {
499+
{
500+
let mut state = self.lock();
501+
if !state.dependency_monitors.insert(id.to_string()) {
502+
return;
503+
}
504+
}
505+
let app = self.clone();
506+
let id = id.to_string();
507+
tokio::spawn(async move {
508+
loop {
509+
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
510+
let processes = match app.supervisor.list().await {
511+
Ok(processes) => processes,
512+
Err(err) => {
513+
warn!(vm_id = id, "failed to inspect dependent processes: {err:?}");
514+
continue;
515+
}
516+
};
517+
let main_running = processes
518+
.iter()
519+
.find(|process| process.config.id == id)
520+
.is_some_and(|process| process.state.status.is_running());
521+
let dependents = processes
522+
.iter()
523+
.filter(|process| {
524+
serde_json::from_str::<ProcessAnnotation>(&process.config.note)
525+
.unwrap_or_default()
526+
.live_for
527+
.as_deref()
528+
== Some(id.as_str())
529+
})
530+
.collect::<Vec<_>>();
531+
let dependent_states = dependents
532+
.iter()
533+
.map(|process| process.state.status.is_running())
534+
.collect::<Vec<_>>();
535+
match dependency_action(main_running, &dependent_states) {
536+
DependencyAction::KeepRunning => continue,
537+
DependencyAction::Finished => break,
538+
DependencyAction::StopDependents => {
539+
app.stop_dependent_processes(&id).await;
540+
break;
541+
}
542+
DependencyAction::StopVm => {
543+
error!(vm_id = id, "required VM process exited; stopping VM");
544+
if let Err(err) = app.supervisor.stop(&id).await {
545+
debug!(
546+
vm_id = id,
547+
"failed to stop VM after dependent exit: {err:?}"
548+
);
549+
}
550+
app.stop_dependent_processes(&id).await;
551+
break;
552+
}
553+
}
554+
}
555+
app.lock().dependency_monitors.remove(&id);
556+
});
557+
}
558+
454559
pub async fn remove_vm(&self, id: &str) -> Result<()> {
455560
{
456561
let mut state = self.lock();
@@ -802,6 +907,13 @@ impl App {
802907
}
803908
}
804909

910+
// Re-establish lifecycle monitoring after a VMM restart. The monitor
911+
// also reconciles a QEMU or dependent process that exited while VMM was
912+
// unavailable.
913+
for id in self.lock().vms.keys().cloned().collect::<Vec<_>>() {
914+
self.spawn_dependency_monitor(&id);
915+
}
916+
805917
// Restore port forwarding for running bridge-mode VMs with persisted guest IPs
806918
let vm_ids: Vec<String> = self.lock().vms.keys().cloned().collect();
807919
for id in vm_ids {
@@ -1593,6 +1705,24 @@ mod tests {
15931705
hex::encode(vec![byte; len])
15941706
}
15951707

1708+
#[test]
1709+
fn dependency_lifecycle_is_fail_closed() {
1710+
assert_eq!(
1711+
dependency_action(true, &[true]),
1712+
DependencyAction::KeepRunning
1713+
);
1714+
assert_eq!(
1715+
dependency_action(false, &[true]),
1716+
DependencyAction::StopDependents
1717+
);
1718+
assert_eq!(dependency_action(true, &[false]), DependencyAction::StopVm);
1719+
assert_eq!(
1720+
dependency_action(false, &[false]),
1721+
DependencyAction::StopDependents
1722+
);
1723+
assert_eq!(dependency_action(true, &[]), DependencyAction::Finished);
1724+
}
1725+
15961726
#[test]
15971727
fn gpu_config_has_gpus_only_when_resolved_gpu_list_is_non_empty() {
15981728
assert!(!GpuConfig::default().has_gpus());
@@ -2194,6 +2324,8 @@ pub(crate) struct AppState {
21942324
vms: HashMap<String, VmState>,
21952325
/// Tracks active port forwarding rules per VM ID (bridge mode only).
21962326
active_forwards: HashMap<String, Vec<ForwardRule>>,
2327+
/// VM IDs with an active auxiliary-process lifecycle monitor.
2328+
dependency_monitors: HashSet<String>,
21972329
}
21982330

21992331
impl AppState {

0 commit comments

Comments
 (0)