Skip to content

Commit 0fa8e37

Browse files
committed
server: abort startup if a stop request is received while awaiting
1 parent 310288e commit 0fa8e37

File tree

3 files changed

+93
-95
lines changed

3 files changed

+93
-95
lines changed

bin/propolis-server/src/lib/vm/state_driver.rs

+75-86
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@
9090
//! [`ensure`]: crate::vm::ensure
9191
9292
use std::{
93-
collections::VecDeque,
9493
sync::{Arc, Mutex},
9594
time::Duration,
9695
};
@@ -142,6 +141,12 @@ pub(super) enum VmStartReason {
142141
ExplicitRequest,
143142
}
144143

144+
enum VmStartOutcome {
145+
Succeeded,
146+
Failed,
147+
Aborted,
148+
}
149+
145150
/// A kind of event the state driver can handle.
146151
#[derive(Debug)]
147152
enum InputQueueEvent {
@@ -154,10 +159,6 @@ struct InputQueueInner {
154159
/// State change requests from the external API.
155160
external_requests: request_queue::ExternalRequestQueue,
156161

157-
/// State change requests from the external API that were previously read
158-
/// but not handled immediately.
159-
self_request: VecDeque<ExternalRequest>,
160-
161162
/// State change requests from the VM's components. These take precedence
162163
/// over external state change requests.
163164
guest_events: super::guest_event::GuestEventQueue,
@@ -170,7 +171,6 @@ impl InputQueueInner {
170171
log, auto_start,
171172
),
172173
guest_events: super::guest_event::GuestEventQueue::default(),
173-
self_request: Default::default(),
174174
}
175175
}
176176
}
@@ -209,9 +209,6 @@ impl InputQueue {
209209
/// - Guest events: These are signals raised from the VM's vCPUs and
210210
/// devices (e.g. a request to reboot or halt the VM arising from a vCPU
211211
/// asserting a virtual chipset signal).
212-
/// - Self-requests: The state driver may buffer external requests for
213-
/// later processing by pushing them to the self-request queue. See
214-
/// [`Self::push_self_request`].
215212
/// - External requests: These are state change requests received via the
216213
/// server API. See [`super::request_queue`] for more details about how
217214
/// these requests are queued.
@@ -228,8 +225,6 @@ impl InputQueue {
228225
let mut guard = self.inner.lock().unwrap();
229226
if let Some(guest_event) = guard.guest_events.pop_front() {
230227
return InputQueueEvent::GuestEvent(guest_event);
231-
} else if let Some(req) = guard.self_request.pop_front() {
232-
return InputQueueEvent::ExternalRequest(req);
233228
} else if let Some(req) = guard.external_requests.pop_front() {
234229
return InputQueueEvent::ExternalRequest(req);
235230
}
@@ -246,19 +241,6 @@ impl InputQueue {
246241
}
247242
}
248243

249-
/// Pushes a self-requested state change request to this queue.
250-
///
251-
/// This routine may only be called from the state driver task.
252-
fn push_self_request(&self, req: ExternalRequest) {
253-
let mut guard = self.inner.lock().unwrap();
254-
guard.self_request.push_back(req);
255-
256-
// Since this routine is only called from the state driver task, the
257-
// driver is by definition not waiting for a new event at this point, so
258-
// it's not necessary to signal the notify here (the next call to
259-
// dequeue an event will always pick this event up).
260-
}
261-
262244
/// Notifies the external request queue that the state driver has completed
263245
/// a request from that queue.
264246
fn notify_request_completed(&self, state: CompletedRequest) {
@@ -508,8 +490,9 @@ impl StateDriver {
508490

509491
let final_state = if migrated_in {
510492
match self.start_vm(VmStartReason::MigratedIn).await {
511-
Ok(()) => self.event_loop().await,
512-
Err(_) => InstanceState::Failed,
493+
VmStartOutcome::Succeeded => self.event_loop().await,
494+
VmStartOutcome::Failed => InstanceState::Failed,
495+
VmStartOutcome::Aborted => InstanceState::Destroyed,
513496
}
514497
} else {
515498
self.event_loop().await
@@ -552,7 +535,7 @@ impl StateDriver {
552535
async fn start_vm(
553536
&mut self,
554537
start_reason: VmStartReason,
555-
) -> anyhow::Result<()> {
538+
) -> VmStartOutcome {
556539
info!(self.log, "starting instance"; "reason" => ?start_reason);
557540

558541
// Tell listeners that the VM's components are now starting up and not
@@ -591,9 +574,9 @@ impl StateDriver {
591574
for (name, dev) in objects.device_map() {
592575
info!(self.log, "sending start request to {}", name);
593576
let res = dev.start();
594-
if let Err(e) = &res {
577+
if let Err(e) = res {
595578
error!(self.log, "startup failed for {}: {:?}", name, e);
596-
return res;
579+
return VmStartOutcome::Failed;
597580
}
598581
}
599582

@@ -642,82 +625,85 @@ impl StateDriver {
642625
// the VM while it was being started. If such a request is seen, send a
643626
// self-request to stop just before returning so that the VM will stop
644627
// immediately.
645-
let mut stopped_while_starting = false;
628+
enum Selection {
629+
BackendFuture(anyhow::Result<()>),
630+
Event(InputQueueEvent),
631+
}
646632
loop {
647-
let event = tokio::select! {
633+
let selection = tokio::select! {
648634
// If the VM successfully starts, return immediately and let
649635
// the caller process any events that may happen to be on the
650636
// queue.
651637
biased;
652638

653639
res = &mut block_backend_fut => {
654-
// If the VM started up successfully, publish that it is
655-
// running and queue up any external requests that were
656-
// deferred while startup was ongoing.
657-
//
658-
// If startup failed, just return the error without changing
659-
// any state or processing any additional requests. The
660-
// caller will move the instance to the appropriate terminal
661-
// state and clean up the VM as needed.
662-
if res.is_ok() {
663-
let objects = &self.objects;
664-
objects.lock_exclusive().await.resume_vcpus();
665-
self.external_state
666-
.update(ExternalStateUpdate::Instance(InstanceState::Running));
667-
668-
self.input_queue.notify_request_completed(
669-
CompletedRequest::Start { succeeded: true },
670-
);
671-
672-
if stopped_while_starting {
673-
self.input_queue.push_self_request(
674-
ExternalRequest::stop()
675-
);
676-
}
677-
678-
info!(&self.log, "VM successfully started");
679-
} else {
680-
self.input_queue.notify_request_completed(
681-
CompletedRequest::Start { succeeded: false },
682-
);
683-
}
684-
685-
return res;
640+
Selection::BackendFuture(res)
686641
}
687642

688-
dequeued = self.input_queue.wait_for_next_event() => {
689-
dequeued
643+
event = self.input_queue.wait_for_next_event() => {
644+
Selection::Event(event)
690645
}
691646
};
692647

693-
// The VM's vCPUs only start when the block backend startup future
694-
// resolves and is selected above. If control reached that point,
695-
// that branch wasn't selected, so the vCPUs should still be paused,
696-
// which means the dequeued event should not be a guest event.
697-
let InputQueueEvent::ExternalRequest(req) = event else {
698-
unreachable!("can't get guest events before vCPUs start");
648+
let req: ExternalRequest = match selection {
649+
Selection::BackendFuture(Ok(())) => {
650+
let objects = &self.objects;
651+
objects.lock_exclusive().await.resume_vcpus();
652+
self.external_state.update(ExternalStateUpdate::Instance(
653+
InstanceState::Running,
654+
));
655+
656+
self.input_queue.notify_request_completed(
657+
CompletedRequest::Start { succeeded: true },
658+
);
659+
660+
info!(&self.log, "VM successfully started");
661+
return VmStartOutcome::Succeeded;
662+
}
663+
664+
Selection::BackendFuture(Err(e)) => {
665+
info!(&self.log, "VM startup failed: {e}");
666+
self.input_queue.notify_request_completed(
667+
CompletedRequest::Start { succeeded: false },
668+
);
669+
670+
return VmStartOutcome::Failed;
671+
}
672+
673+
// The VM's vCPUs only start when the block backend startup
674+
// future resolves and is selected above. If control reached
675+
// that point, that branch wasn't selected, so the vCPUs should
676+
// still be paused, which means the dequeued event should not be
677+
// a guest event.
678+
Selection::Event(InputQueueEvent::GuestEvent(_)) => {
679+
unreachable!("can't get guest events before vCPUs start")
680+
}
681+
682+
Selection::Event(InputQueueEvent::ExternalRequest(req)) => req,
699683
};
700684

701-
// Handle requests to reconfigure one of the existing Crucible
702-
// volumes inline, but buffer other requests so that they can be
703-
// handled after the VM has finished starting.
704-
//
705-
// Buffering some requests and servicing others can theoretically
706-
// change the order in which those requests are retired. That's not
707-
// a problem here because the request queue will stop accepting new
708-
// VCR change requests once a request to stop has been queued.
709685
match req {
710686
ExternalRequest::State(StateChangeRequest::Stop) => {
711687
info!(
712688
&self.log,
713689
"got request to stop while still starting"
714690
);
715691

716-
// Remember that the VM should stop once it has started.
717-
// It's not safe to issue a self-request here because the
718-
// next loop iteration will simply pop the self-request back
719-
// off the queue and reach this path once more.
720-
stopped_while_starting = true;
692+
// Don't send any pause/halt notifications here, since
693+
// (depending on what async work was in flight when this
694+
// notification was received) there may be a
695+
// partially-started component that is not prepared to be
696+
// paused and halted. Instead, simply move the VM to
697+
// Stopped, return an "aborted" status, and let the caller
698+
// arrange to drop all the VM's components. (Note that no
699+
// vCPUs have started yet, so no guest work is in flight at
700+
// this point.)
701+
self.external_state.update(ExternalStateUpdate::Instance(
702+
InstanceState::Stopped,
703+
));
704+
705+
self.input_queue.notify_stopped();
706+
return VmStartOutcome::Aborted;
721707
}
722708
ExternalRequest::Component(
723709
ComponentChangeRequest::ReconfigureCrucibleVolume {
@@ -810,10 +796,13 @@ impl StateDriver {
810796
match request {
811797
ExternalRequest::State(StateChangeRequest::Start) => {
812798
match self.start_vm(VmStartReason::ExplicitRequest).await {
813-
Ok(()) => HandleEventOutcome::Continue,
814-
Err(_) => HandleEventOutcome::Exit {
799+
VmStartOutcome::Succeeded => HandleEventOutcome::Continue,
800+
VmStartOutcome::Failed => HandleEventOutcome::Exit {
815801
final_state: InstanceState::Failed,
816802
},
803+
VmStartOutcome::Aborted => HandleEventOutcome::Exit {
804+
final_state: InstanceState::Destroyed,
805+
},
817806
}
818807
}
819808
ExternalRequest::State(StateChangeRequest::MigrateAsSource {

lib/propolis/src/block/mod.rs

+16
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,16 @@ pub trait Backend: Send + Sync + 'static {
251251
///
252252
/// Spawning of any tasks required to do such request processing can be done
253253
/// as part of this start-up.
254+
///
255+
/// This operation will be invoked only once per backend (when its VM
256+
/// starts). Block backends are not explicitly resumed during VM lifecycle
257+
/// events; instead, their corresponding devices will stop issuing new
258+
/// requests while paused and resume issuing them when they are resumed.
259+
///
260+
/// WARNING: The caller may abort VM startup and cancel the future created
261+
/// by this routine. In this case the caller may not call [`stop`] prior to
262+
/// dropping the backend. This routine is, however, guaranteed to be called
263+
/// before the VM's vCPUs are started.
254264
async fn start(&self) -> anyhow::Result<()>;
255265

256266
/// Stop attempting to process new [Request]s from [Device] (if attached)
@@ -260,6 +270,12 @@ pub trait Backend: Send + Sync + 'static {
260270
///
261271
/// If any tasks were spawned as part of [Backend::start()], they should be
262272
/// brought to rest as part of this call.
273+
///
274+
/// This operation will be invoked only once per backend (when its VM
275+
/// stops). Block backends are not explicitly paused during VM lifecycle
276+
/// events; instead, their corresponding devices will stop issuing new
277+
/// requests when they are told to pause (and will only report they are
278+
/// fully paused when all their in-flight requests have completed).
263279
async fn stop(&self) -> ();
264280

265281
/// Attempt to detach from associated [Device]

phd-tests/tests/src/server_state_machine.rs

+2-9
Original file line numberDiff line numberDiff line change
@@ -133,16 +133,9 @@ async fn stop_while_blocked_on_start_test(ctx: &Framework) {
133133
.await
134134
.unwrap();
135135

136-
// Send a stop request. This should enqueue successfully, but the VM won't
137-
// stop right away because it's still starting.
136+
// Send a stop request. This should enqueue successfully, and the VM should
137+
// shut down even though activation is blocked.
138138
vm.stop().await?;
139-
140-
// Unblock Crucible startup by fixing the broken disk's VCR.
141-
disk.disable_vcr_black_hole();
142-
disk.set_generation(2);
143-
vm.replace_crucible_vcr(disk).await?;
144-
145-
// Eventually the instance should shut down.
146139
vm.wait_for_state(InstanceState::Destroyed, Duration::from_secs(60))
147140
.await?;
148141
}

0 commit comments

Comments
 (0)