Skip to content

Commit 290a639

Browse files
committed
[sled-agent] VMM graceful shutdown timeout
Presently, sled-agent's `InstanceRunner` has two mechanisms for shutting down a VMM: sending an instance state PUT request to the `propolis-server` process for the `Stopped` state, or forcibly terminating the `propolis-server` and tearing down the zone. At present, when a request to stop an instance is sent to the sled-agent, it uses the first mechanism, where Propolis is politely asked to stop the instance --- which I'll refer to as "graceful shutdown". The forceful termination path is used when asked to unregister an instance where the VMM has not started up yet, when encountering an unrecoverable VMM error, or when killing an instance that was making use of an expunged disk. Currently, these two paths don't really overlap: when Nexus asks a sled-agent to stop an instance, all it will do is politely ask Propolis to please stop the instance gracefully, and will only fall back to violently shooting the zone in the face if Propolis returns the error that indicates it never knew about that instance in the first place. This means that, should a VMM get *stuck* while shutting down the instance, stopping it will never complete successfully, and the Propolis zone won't get cleaned up. This can happen due to e.g. [a Crucible activation that will never complete][1]. Thus, the sled-agent should attempt to violently terminate a Propolis zone when a graceful shutdown of the VMM fails to complete in a timely manner. This commit introduces a timeout for the graceful shutdown process. Now, when we send a PUT request to Propolis with the `Stopped` instance state, the sled-agent will start a 10-minute timer. If no update from Propolis that indicates the instance has transitioned to `Stopped` is received before the timer completes, the sled-agent will proceed with the forceful termination of the Propolis zone. Fixes #4004. [1]: #4004 (comment)
1 parent be94d13 commit 290a639

File tree

1 file changed

+59
-1
lines changed

1 file changed

+59
-1
lines changed

sled-agent/src/instance.rs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ use sled_storage::manager::StorageHandle;
4747
use slog::Logger;
4848
use std::net::IpAddr;
4949
use std::net::SocketAddr;
50+
use std::pin::Pin;
5051
use std::sync::Arc;
52+
use std::time::Duration;
5153
use tokio::sync::{mpsc, oneshot};
5254
use uuid::Uuid;
5355

@@ -472,8 +474,31 @@ struct InstanceRunner {
472474
}
473475

474476
impl InstanceRunner {
477+
/// How long to wait for VMM shutdown to complete before forcefully
478+
/// terminating the zone.
479+
const STOP_GRACE_PERIOD: Duration = Duration::from_secs(60 * 10);
480+
475481
async fn run(mut self, mut terminate_rx: mpsc::Receiver<TerminateRequest>) {
476482
use InstanceRequest::*;
483+
484+
// Timeout for stopping the instance gracefully.
485+
//
486+
// When we send Propolis a put-state request to transition to
487+
// Stopped`, we start this timer. If Propolis does not report back
488+
// that it has stopped the instance within `STOP_GRACE_PERIOD`, we
489+
// will forcibly terminate the VMM. This is to ensure that a totally
490+
// stuck VMM does not prevent the Propolis zone from being cleaned up.
491+
let mut stop_timeout = None;
492+
async fn stop_timeout_completed(
493+
stop_timeout: &mut Option<Pin<Box<tokio::time::Sleep>>>,
494+
) {
495+
if let Some(ref mut timeout) = stop_timeout {
496+
timeout.await
497+
} else {
498+
std::future::pending().await
499+
}
500+
}
501+
477502
while !self.should_terminate {
478503
tokio::select! {
479504
biased;
@@ -539,6 +564,24 @@ impl InstanceRunner {
539564
},
540565
}
541566
},
567+
568+
// We are waiting for the VMM to stop, and the grace period has
569+
// elapsed without us hearing back from Propolis that it has
570+
// stopped the instance. Time to terminate it violently!
571+
//
572+
// Note that this must be a lower priority in the `select!` than
573+
// instance monitor requests, as we would prefer to honor a
574+
// message from the instance monitor indicating a successful
575+
// stop, even if we have reached the timeout.
576+
_ = stop_timeout_completed(&mut stop_timeout) => {
577+
warn!(
578+
self.log,
579+
"Instance failed to stop within the grace period, \
580+
terminating it violently!",
581+
);
582+
self.terminate(false).await;
583+
}
584+
542585
// Requests to terminate the instance take priority over any
543586
// other request to the instance.
544587
request = terminate_rx.recv() => {
@@ -584,7 +627,22 @@ impl InstanceRunner {
584627
tx.send(Ok(self.current_state()))
585628
.map_err(|_| Error::FailedSendClientClosed)
586629
},
587-
PutState{ state, tx } => {
630+
PutState { state, tx } => {
631+
// If we're going to stop the instance, start
632+
// the timeout after which we will forcefully
633+
// terminate the VMM.
634+
if let VmmStateRequested::Stopped = state {
635+
// Only start the stop timeout if there
636+
// isn't one already, so that additional
637+
// requests to stop coming in don't reset
638+
// the clock.
639+
if stop_timeout.is_none() {
640+
stop_timeout = Some(Box::pin(tokio::time::sleep(
641+
Self::STOP_GRACE_PERIOD
642+
)));
643+
}
644+
}
645+
588646
tx.send(self.put_state(state).await
589647
.map(|r| VmmPutStateResponse { updated_runtime: Some(r) })
590648
.map_err(|e| e.into()))

0 commit comments

Comments
 (0)