Introduce shutdown fairings.

Also adds 'Client::terminate()' to run graceful shutdown in testing.

Resolves #1707.
This commit is contained in:
Sergio Benitez 2022-05-07 06:11:04 -05:00
parent 1575f47753
commit 7908dc43ca
12 changed files with 353 additions and 30 deletions

View File

@ -66,6 +66,9 @@ enum AdHocKind {
/// sent to a client. /// sent to a client.
Response(Box<dyn for<'r, 'b> Fn(&'r Request<'_>, &'b mut Response<'r>) Response(Box<dyn for<'r, 'b> Fn(&'r Request<'_>, &'b mut Response<'r>)
-> BoxFuture<'b, ()> + Send + Sync + 'static>), -> BoxFuture<'b, ()> + Send + Sync + 'static>),
/// An ad-hoc **shutdown** fairing. Called on shutdown.
Shutdown(Once<dyn for<'a> FnOnce(&'a Rocket<Orbit>) -> BoxFuture<'a, ()> + Send + 'static>),
} }
impl AdHoc { impl AdHoc {
@ -181,6 +184,27 @@ impl AdHoc {
AdHoc { name, kind: AdHocKind::Response(Box::new(f)) } AdHoc { name, kind: AdHocKind::Response(Box::new(f)) }
} }
/// Constructs an `AdHoc` shutdown fairing named `name`. The function `f`
/// will be called by Rocket when [shutdown is triggered].
///
/// [shutdown is triggered]: crate::config::Shutdown#triggers
///
/// # Example
///
/// ```rust
/// use rocket::fairing::AdHoc;
///
/// // A fairing that prints a message just before launching.
/// let fairing = AdHoc::on_shutdown("Bye!", |_| Box::pin(async move {
/// println!("Rocket is on its way back!");
/// }));
/// ```
pub fn on_shutdown<F: Send + Sync + 'static>(name: &'static str, f: F) -> AdHoc
where F: for<'a> FnOnce(&'a Rocket<Orbit>) -> BoxFuture<'a, ()>
{
AdHoc { name, kind: AdHocKind::Shutdown(Once::new(Box::new(f))) }
}
/// Constructs an `AdHoc` launch fairing that extracts a configuration of /// Constructs an `AdHoc` launch fairing that extracts a configuration of
/// type `T` from the configured provider and stores it in managed state. If /// type `T` from the configured provider and stores it in managed state. If
/// extractions fails, pretty-prints the error message and aborts launch. /// extractions fails, pretty-prints the error message and aborts launch.
@ -229,6 +253,7 @@ impl Fairing for AdHoc {
AdHocKind::Liftoff(_) => Kind::Liftoff, AdHocKind::Liftoff(_) => Kind::Liftoff,
AdHocKind::Request(_) => Kind::Request, AdHocKind::Request(_) => Kind::Request,
AdHocKind::Response(_) => Kind::Response, AdHocKind::Response(_) => Kind::Response,
AdHocKind::Shutdown(_) => Kind::Shutdown,
}; };
Info { name: self.name, kind } Info { name: self.name, kind }
@ -258,4 +283,10 @@ impl Fairing for AdHoc {
f(req, res).await f(req, res).await
} }
} }
async fn on_shutdown(&self, rocket: &Rocket<Orbit>) {
if let AdHocKind::Shutdown(ref f) = self.kind {
(f.take())(rocket).await
}
}
} }

View File

@ -19,6 +19,7 @@ pub struct Fairings {
liftoff: Vec<usize>, liftoff: Vec<usize>,
request: Vec<usize>, request: Vec<usize>,
response: Vec<usize>, response: Vec<usize>,
shutdown: Vec<usize>,
} }
macro_rules! iter { macro_rules! iter {
@ -46,6 +47,7 @@ impl Fairings {
.chain(self.liftoff.iter()) .chain(self.liftoff.iter())
.chain(self.request.iter()) .chain(self.request.iter())
.chain(self.response.iter()) .chain(self.response.iter())
.chain(self.shutdown.iter())
} }
pub fn add(&mut self, fairing: Box<dyn Fairing>) { pub fn add(&mut self, fairing: Box<dyn Fairing>) {
@ -90,6 +92,7 @@ impl Fairings {
remove(i, &mut self.liftoff); remove(i, &mut self.liftoff);
remove(i, &mut self.request); remove(i, &mut self.request);
remove(i, &mut self.response); remove(i, &mut self.response);
remove(i, &mut self.shutdown);
} }
} }
@ -99,6 +102,7 @@ impl Fairings {
if this_info.kind.is(Kind::Liftoff) { self.liftoff.push(index); } if this_info.kind.is(Kind::Liftoff) { self.liftoff.push(index); }
if this_info.kind.is(Kind::Request) { self.request.push(index); } if this_info.kind.is(Kind::Request) { self.request.push(index); }
if this_info.kind.is(Kind::Response) { self.response.push(index); } if this_info.kind.is(Kind::Response) { self.response.push(index); }
if this_info.kind.is(Kind::Shutdown) { self.shutdown.push(index); }
} }
pub fn append(&mut self, others: &mut Fairings) { pub fn append(&mut self, others: &mut Fairings) {
@ -153,6 +157,12 @@ impl Fairings {
} }
} }
#[inline(always)]
pub async fn handle_shutdown(&self, rocket: &Rocket<Orbit>) {
let shutdown_futures = iter!(self.shutdown).map(|f| f.on_shutdown(rocket));
futures::future::join_all(shutdown_futures).await;
}
pub fn audit(&self) -> Result<(), &[Info]> { pub fn audit(&self) -> Result<(), &[Info]> {
match self.failures.is_empty() { match self.failures.is_empty() {
true => Ok(()), true => Ok(()),
@ -184,6 +194,7 @@ impl std::fmt::Debug for Fairings {
.field("liftoff", &debug_info(iter!(self.liftoff))) .field("liftoff", &debug_info(iter!(self.liftoff)))
.field("request", &debug_info(iter!(self.request))) .field("request", &debug_info(iter!(self.request)))
.field("response", &debug_info(iter!(self.response))) .field("response", &debug_info(iter!(self.response)))
.field("shutdown", &debug_info(iter!(self.shutdown)))
.finish() .finish()
} }
} }

View File

@ -18,7 +18,7 @@ use std::ops::BitOr;
/// # let _unused_info = /// # let _unused_info =
/// Info { /// Info {
/// name: "Example Fairing", /// name: "Example Fairing",
/// kind: Kind::Ignite | Kind::Liftoff | Kind::Request | Kind::Response /// kind: Kind::Ignite | Kind::Liftoff | Kind::Request | Kind::Response | Kind::Shutdown
/// } /// }
/// # ; /// # ;
/// ``` /// ```
@ -40,6 +40,7 @@ pub struct Info {
/// * Liftoff /// * Liftoff
/// * Request /// * Request
/// * Response /// * Response
/// * Shutdown
/// ///
/// Two `Kind` structures can be `or`d together to represent a combination. For /// Two `Kind` structures can be `or`d together to represent a combination. For
/// instance, to represent a fairing that is both an ignite and request fairing, /// instance, to represent a fairing that is both an ignite and request fairing,
@ -66,9 +67,12 @@ impl Kind {
/// `Kind` flag representing a request for a 'response' callback. /// `Kind` flag representing a request for a 'response' callback.
pub const Response: Kind = Kind(1 << 3); pub const Response: Kind = Kind(1 << 3);
/// `Kind` flag representing a request for a 'shutdown' callback.
pub const Shutdown: Kind = Kind(1 << 4);
/// `Kind` flag representing a /// `Kind` flag representing a
/// [singleton](crate::fairing::Fairing#singletons) fairing. /// [singleton](crate::fairing::Fairing#singletons) fairing.
pub const Singleton: Kind = Kind(1 << 4); pub const Singleton: Kind = Kind(1 << 5);
/// Returns `true` if `self` is a superset of `other`. In other words, /// Returns `true` if `self` is a superset of `other`. In other words,
/// returns `true` if all of the kinds in `other` are also in `self`. /// returns `true` if all of the kinds in `other` are also in `self`.
@ -141,6 +145,7 @@ impl std::fmt::Display for Kind {
write("liftoff", Kind::Liftoff)?; write("liftoff", Kind::Liftoff)?;
write("request", Kind::Request)?; write("request", Kind::Request)?;
write("response", Kind::Response)?; write("response", Kind::Response)?;
write("shutdown", Kind::Shutdown)?;
write("singleton", Kind::Singleton) write("singleton", Kind::Singleton)
} }
} }

View File

@ -101,14 +101,15 @@ pub type Result<T = Rocket<Build>, E = Rocket<Build>> = std::result::Result<T, E
/// ///
/// ## Fairing Callbacks /// ## Fairing Callbacks
/// ///
/// There are four kinds of fairing callbacks: launch, liftoff, request, and /// There are five kinds of fairing callbacks: launch, liftoff, request,
/// response. A fairing can request any combination of these callbacks through /// response, and shutdown. A fairing can request any combination of these
/// the `kind` field of the [`Info`] structure returned from the `info` method. /// callbacks through the `kind` field of the [`Info`] structure returned from
/// Rocket will only invoke the callbacks identified in the fairing's [`Kind`]. /// the `info` method. Rocket will only invoke the callbacks identified in the
/// fairing's [`Kind`].
/// ///
/// The four callback kinds are as follows: /// The callback kinds are as follows:
/// ///
/// * **Ignite (`on_ignite`)** /// * **<a name="ignite">Ignite</a> (`on_ignite`)**
/// ///
/// An ignite callback, represented by the [`Fairing::on_ignite()`] method, /// An ignite callback, represented by the [`Fairing::on_ignite()`] method,
/// is called just prior to liftoff, during ignition. The state of the /// is called just prior to liftoff, during ignition. The state of the
@ -126,15 +127,20 @@ pub type Result<T = Rocket<Build>, E = Rocket<Build>> = std::result::Result<T, E
/// ignite fairing returns `Err`, launch will be aborted. All ignite /// ignite fairing returns `Err`, launch will be aborted. All ignite
/// fairings are executed even if one or more signal a failure. /// fairings are executed even if one or more signal a failure.
/// ///
/// * **Liftoff (`on_liftoff`)** /// * **<a name="liftoff">Liftoff</a> (`on_liftoff`)**
/// ///
/// A liftoff callback, represented by the [`Fairing::on_liftoff()`] method, /// A liftoff callback, represented by the [`Fairing::on_liftoff()`] method,
/// is called immediately after a Rocket application has launched. At this /// is called immediately after a Rocket application has launched. At this
/// point, Rocket has opened a socket for listening but has not yet begun /// point, Rocket has opened a socket for listening but has not yet begun
/// accepting connections. A liftoff callback can inspect the `Rocket` /// accepting connections. A liftoff callback can inspect the `Rocket`
/// instance that has launched but not otherwise gracefully abort launch. /// instance that has launched and even schedule a shutdown using
/// [`Shutdown::notify()`](crate::Shutdown::notify()) via
/// [`Rocket::shutdown()`].
/// ///
/// * **Request (`on_request`)** /// Liftoff fairings are run concurrently; resolution of all fairings is
/// awaited before resuming request serving.
///
/// * **<a name="request">Request</a> (`on_request`)**
/// ///
/// A request callback, represented by the [`Fairing::on_request()`] method, /// A request callback, represented by the [`Fairing::on_request()`] method,
/// is called just after a request is received, immediately after /// is called just after a request is received, immediately after
@ -147,7 +153,7 @@ pub type Result<T = Rocket<Build>, E = Rocket<Build>> = std::result::Result<T, E
/// via response callbacks. Any modifications to a request are persisted and /// via response callbacks. Any modifications to a request are persisted and
/// can potentially alter how a request is routed. /// can potentially alter how a request is routed.
/// ///
/// * **Response (`on_response`)** /// * **<a name="response">Response</a> (`on_response`)**
/// ///
/// A response callback, represented by the [`Fairing::on_response()`] /// A response callback, represented by the [`Fairing::on_response()`]
/// method, is called when a response is ready to be sent to the client. At /// method, is called when a response is ready to be sent to the client. At
@ -162,6 +168,33 @@ pub type Result<T = Rocket<Build>, E = Rocket<Build>> = std::result::Result<T, E
/// request. Additionally, Rocket will automatically strip the body for /// request. Additionally, Rocket will automatically strip the body for
/// `HEAD` requests _after_ response fairings have run. /// `HEAD` requests _after_ response fairings have run.
/// ///
/// * **<a name="shutdown">Shutdown</a> (`on_shutdown`)**
///
/// A shutdown callback, represented by the [`Fairing::on_shutdown()`]
/// method, is called when [shutdown is triggered]. At this point, graceful
/// shutdown has commenced but not completed; no new requests are accepted
/// but the application may still be actively serving existing requests.
///
/// Rocket guarantees, however, that all requests are completed or aborted
/// once [grace and mercy periods] have expired. This implies that a
/// shutdown fairing that (asynchronously) sleeps for `grace + mercy + ε`
/// seconds before executing any logic will execute said logic after all
/// requests have been processed or aborted. Note that such fairings may
/// wish to operate using the `Ok` return value of [`Rocket::launch()`]
/// instead.
///
/// All registered shutdown fairings are run concurrently; resolution of all
/// fairings is awaited before resuming shutdown. Shutdown fairings do not
/// affect grace and mercy periods. In other words, any time consumed by
/// shutdown fairings is not added to grace and mercy periods.
///
/// ***Note: Shutdown fairings are only run during testing if the `Client`
/// is terminated using [`Client::terminate()`].***
///
/// [shutdown is triggered]: crate::config::Shutdown#triggers
/// [grace and mercy periods]: crate::config::Shutdown#summary
/// [`Client::terminate()`]: crate::local::blocking::Client::terminate()
///
/// # Singletons /// # Singletons
/// ///
/// In general, any number of instances of a given fairing type can be attached /// In general, any number of instances of a given fairing type can be attached
@ -246,6 +279,11 @@ pub type Result<T = Rocket<Build>, E = Rocket<Build>> = std::result::Result<T, E
/// /* ... */ /// /* ... */
/// # unimplemented!() /// # unimplemented!()
/// } /// }
///
/// async fn on_shutdown(&self, rocket: &Rocket<Orbit>) {
/// /* ... */
/// # unimplemented!()
/// }
/// } /// }
/// ``` /// ```
/// ///
@ -426,6 +464,8 @@ pub trait Fairing: Send + Sync + Any + 'static {
/// The ignite callback. Returns `Ok` if ignition should proceed and `Err` /// The ignite callback. Returns `Ok` if ignition should proceed and `Err`
/// if ignition and launch should be aborted. /// if ignition and launch should be aborted.
/// ///
/// See [Fairing Callbacks](#ignite) for complete semantics.
///
/// This method is called during ignition and if `Kind::Ignite` is in the /// This method is called during ignition and if `Kind::Ignite` is in the
/// `kind` field of the `Info` structure for this fairing. The `rocket` /// `kind` field of the `Info` structure for this fairing. The `rocket`
/// parameter is the `Rocket` instance that is currently being built for /// parameter is the `Rocket` instance that is currently being built for
@ -438,6 +478,8 @@ pub trait Fairing: Send + Sync + Any + 'static {
/// The liftoff callback. /// The liftoff callback.
/// ///
/// See [Fairing Callbacks](#liftoff) for complete semantics.
///
/// This method is called just after launching the application if /// This method is called just after launching the application if
/// `Kind::Liftoff` is in the `kind` field of the `Info` structure for this /// `Kind::Liftoff` is in the `kind` field of the `Info` structure for this
/// fairing. The `Rocket` parameter corresponds to the lauched application. /// fairing. The `Rocket` parameter corresponds to the lauched application.
@ -449,6 +491,8 @@ pub trait Fairing: Send + Sync + Any + 'static {
/// The request callback. /// The request callback.
/// ///
/// See [Fairing Callbacks](#request) for complete semantics.
///
/// This method is called when a new request is received if `Kind::Request` /// This method is called when a new request is received if `Kind::Request`
/// is in the `kind` field of the `Info` structure for this fairing. The /// is in the `kind` field of the `Info` structure for this fairing. The
/// `&mut Request` parameter is the incoming request, and the `&Data` /// `&mut Request` parameter is the incoming request, and the `&Data`
@ -461,6 +505,8 @@ pub trait Fairing: Send + Sync + Any + 'static {
/// The response callback. /// The response callback.
/// ///
/// See [Fairing Callbacks](#response) for complete semantics.
///
/// This method is called when a response is ready to be issued to a client /// This method is called when a response is ready to be issued to a client
/// if `Kind::Response` is in the `kind` field of the `Info` structure for /// if `Kind::Response` is in the `kind` field of the `Info` structure for
/// this fairing. The `&Request` parameter is the request that was routed, /// this fairing. The `&Request` parameter is the request that was routed,
@ -470,6 +516,21 @@ pub trait Fairing: Send + Sync + Any + 'static {
/// ///
/// The default implementation of this method does nothing. /// The default implementation of this method does nothing.
async fn on_response<'r>(&self, _req: &'r Request<'_>, _res: &mut Response<'r>) {} async fn on_response<'r>(&self, _req: &'r Request<'_>, _res: &mut Response<'r>) {}
/// The shutdown callback.
///
/// See [Fairing Callbacks](#shutdown) for complete semantics.
///
/// This method is called when [shutdown is triggered] if `Kind::Shutdown`
/// is in the `kind` field of the `Info` structure for this fairing. The
/// `Rocket` parameter corresponds to the running application.
///
/// [shutdown is triggered]: crate::config::Shutdown#triggers
///
/// ## Default Implementation
///
/// The default implementation of this method does nothing.
async fn on_shutdown(&self, _rocket: &Rocket<Orbit>) { }
} }
#[crate::async_trait] #[crate::async_trait]
@ -498,4 +559,9 @@ impl<T: Fairing> Fairing for std::sync::Arc<T> {
async fn on_response<'r>(&self, req: &'r Request<'_>, res: &mut Response<'r>) { async fn on_response<'r>(&self, req: &'r Request<'_>, res: &mut Response<'r>) {
(self as &T).on_response(req, res).await (self as &T).on_response(req, res).await
} }
#[inline]
async fn on_shutdown(&self, rocket: &Rocket<Orbit>) {
(self as &T).on_shutdown(rocket).await
}
} }

View File

@ -451,7 +451,8 @@ impl<'v> TempFile<'v> {
.unwrap_or(Limits::FILE); .unwrap_or(Limits::FILE);
let temp_dir = req.rocket().config().temp_dir.relative(); let temp_dir = req.rocket().config().temp_dir.relative();
let file = task::spawn_blocking(move || NamedTempFile::new_in(temp_dir)).await; let file = task::spawn_blocking(move || NamedTempFile::new_in(temp_dir));
let file = file.await;
let file = file.map_err(|_| io::Error::new(io::ErrorKind::Other, "spawn_block panic"))??; let file = file.map_err(|_| io::Error::new(io::ErrorKind::Other, "spawn_block panic"))??;
let (file, temp_path) = file.into_parts(); let (file, temp_path) = file.into_parts();

View File

@ -2,7 +2,7 @@ use std::fmt;
use parking_lot::RwLock; use parking_lot::RwLock;
use crate::{Rocket, Phase, Orbit, Error}; use crate::{Rocket, Phase, Orbit, Ignite, Error};
use crate::local::asynchronous::{LocalRequest, LocalResponse}; use crate::local::asynchronous::{LocalRequest, LocalResponse};
use crate::http::{Method, uri::Origin, private::cookie}; use crate::http::{Method, uri::Origin, private::cookie};
@ -63,6 +63,7 @@ impl Client {
} }
// WARNING: This is unstable! Do not use this method outside of Rocket! // WARNING: This is unstable! Do not use this method outside of Rocket!
// This is used by the `Client` doctests.
#[doc(hidden)] #[doc(hidden)]
pub fn _test<T, F>(f: F) -> T pub fn _test<T, F>(f: F) -> T
where F: FnOnce(&Self, LocalRequest<'_>, LocalResponse<'_>) -> T + Send where F: FnOnce(&Self, LocalRequest<'_>, LocalResponse<'_>) -> T + Send
@ -101,6 +102,13 @@ impl Client {
LocalRequest::new(self, method, uri) LocalRequest::new(self, method, uri)
} }
pub(crate) async fn _terminate(self) -> Rocket<Ignite> {
let rocket = self.rocket;
rocket.shutdown().notify();
rocket.fairings.handle_shutdown(&rocket).await;
rocket.into_ignite()
}
// Generates the public API methods, which call the private methods above. // Generates the public API methods, which call the private methods above.
pub_client_impl!("use rocket::local::asynchronous::Client;" @async await); pub_client_impl!("use rocket::local::asynchronous::Client;" @async await);
} }

View File

@ -1,7 +1,7 @@
use std::fmt; use std::fmt;
use std::cell::RefCell; use std::cell::RefCell;
use crate::{Rocket, Phase, Orbit, Error}; use crate::{Rocket, Phase, Orbit, Ignite, Error};
use crate::local::{asynchronous, blocking::{LocalRequest, LocalResponse}}; use crate::local::{asynchronous, blocking::{LocalRequest, LocalResponse}};
use crate::http::{Method, uri::Origin}; use crate::http::{Method, uri::Origin};
@ -78,6 +78,15 @@ impl Client {
self.inner()._with_raw_cookies(f) self.inner()._with_raw_cookies(f)
} }
pub(crate) fn _terminate(mut self) -> Rocket<Ignite> {
let runtime = tokio::runtime::Builder::new_current_thread().build().unwrap();
let runtime = self.runtime.replace(runtime);
let inner = self.inner.take().expect("invariant broken: self.inner is Some");
let rocket = runtime.block_on(inner._terminate());
runtime.shutdown_timeout(std::time::Duration::from_secs(1));
rocket
}
#[inline(always)] #[inline(always)]
fn _req<'c, 'u: 'c, U>(&'c self, method: Method, uri: U) -> LocalRequest<'c> fn _req<'c, 'u: 'c, U>(&'c self, method: Method, uri: U) -> LocalRequest<'c>
where U: TryInto<Origin<'u>> + fmt::Display where U: TryInto<Origin<'u>> + fmt::Display
@ -97,8 +106,9 @@ impl std::fmt::Debug for Client {
impl Drop for Client { impl Drop for Client {
fn drop(&mut self) { fn drop(&mut self) {
let client = self.inner.take(); if let Some(client) = self.inner.take() {
self.block_on(async { drop(client) }); self.block_on(async { drop(client) });
}
} }
} }

View File

@ -95,6 +95,29 @@ macro_rules! pub_client_impl {
Self::_new(rocket, false) $(.$suffix)? Self::_new(rocket, false) $(.$suffix)?
} }
/// Terminates `Client` by initiating a graceful shutdown via
/// [`Shutdown::notify()`] and running shutdown fairings.
///
/// This method _must_ be called on a `Client` if graceful shutdown is
/// required for testing as `Drop` _does not_ signal `Shutdown` nor run
/// shutdown fairings. Returns the instance of `Rocket` being managed by
/// this client after all shutdown fairings run to completion.
///
/// [`Shutdown::notify()`]: crate::Shutdown::notify()
///
/// ```rust,no_run
#[doc = $import]
///
/// # fn f(client: Client) {
/// let client: Client = client;
/// let rocket = client.terminate();
/// # }
/// ```
#[inline(always)]
pub $($prefix)? fn terminate(self) -> Rocket<Ignite> {
Self::_terminate(self) $(.$suffix)?
}
#[doc(hidden)] #[doc(hidden)]
pub $($prefix)? fn debug_with(routes: Vec<crate::Route>) -> Result<Self, Error> { pub $($prefix)? fn debug_with(routes: Vec<crate::Route>) -> Result<Self, Error> {
let rocket = crate::custom(crate::Config::debug_default()); let rocket = crate::custom(crate::Config::debug_default());

View File

@ -664,7 +664,7 @@ impl Rocket<Ignite> {
} }
impl Rocket<Orbit> { impl Rocket<Orbit> {
fn into_ignite(self) -> Rocket<Ignite> { pub(crate) fn into_ignite(self) -> Rocket<Ignite> {
Rocket(Igniting { Rocket(Igniting {
router: self.0.router, router: self.0.router,
fairings: self.0.fairings, fairings: self.0.fairings,

View File

@ -502,10 +502,15 @@ impl Rocket<Orbit> {
biased; biased;
_ = shutdown => { _ = shutdown => {
// Run shutdown fairings. We compute `sleep()` for grace periods
// beforehand to ensure we don't add shutdown fairing completion
// time, which is arbitrary, to these periods.
info!("Shutdown requested. Waiting for pending I/O..."); info!("Shutdown requested. Waiting for pending I/O...");
let grace_timer = sleep(Duration::from_secs(grace)); let grace_timer = sleep(Duration::from_secs(grace));
let mercy_timer = sleep(Duration::from_secs(grace + mercy)); let mercy_timer = sleep(Duration::from_secs(grace + mercy));
let shutdown_timer = sleep(Duration::from_secs(grace + mercy + 1)); let shutdown_timer = sleep(Duration::from_secs(grace + mercy + 1));
rocket.fairings.handle_shutdown(&*rocket).await;
tokio::pin!(grace_timer, mercy_timer, shutdown_timer); tokio::pin!(grace_timer, mercy_timer, shutdown_timer);
tokio::select! { tokio::select! {
biased; biased;

View File

@ -0,0 +1,151 @@
#[macro_use] extern crate rocket;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use rocket::fairing::AdHoc;
// Want to test:
//
// * stalled connection + sleep in shutdown -> conn closed
// * stalled shutdown fairing stalls shutdown but not > grace + mercy
// - sleep < grace + mercy
// - sleep > grace + mercy
#[derive(Default)]
struct Flags {
liftoff: AtomicBool,
shutdown: AtomicUsize
}
#[test]
fn shutdown_fairing_runs() {
use rocket::local::blocking::Client;
let rocket = rocket::build()
.manage(Flags::default())
.attach(AdHoc::on_liftoff("Liftoff Flag", |rocket| Box::pin(async move {
let flags = rocket.state::<Flags>().unwrap();
flags.liftoff.store(true, Ordering::SeqCst);
})))
.attach(AdHoc::on_shutdown("Shutdown Flag", |rocket| Box::pin(async move {
let flags = rocket.state::<Flags>().unwrap();
flags.shutdown.fetch_add(1, Ordering::SeqCst);
})));
let client = Client::debug(rocket).unwrap();
let flags = client.rocket().state::<Flags>().unwrap();
assert!(flags.liftoff.load(Ordering::SeqCst));
assert_eq!(0, flags.shutdown.load(Ordering::SeqCst));
let rocket = client.terminate();
let flags = rocket.state::<Flags>().unwrap();
assert_eq!(1, flags.shutdown.load(Ordering::SeqCst));
}
#[async_test]
async fn async_shutdown_fairing_runs() {
use rocket::local::asynchronous::Client;
let rocket = rocket::build()
.manage(Flags::default())
.attach(AdHoc::on_liftoff("Liftoff Flag", |rocket| Box::pin(async move {
let flags = rocket.state::<Flags>().unwrap();
flags.liftoff.store(true, Ordering::SeqCst);
})))
.attach(AdHoc::on_shutdown("Shutdown Flag", |rocket| Box::pin(async move {
let flags = rocket.state::<Flags>().unwrap();
flags.shutdown.fetch_add(1, Ordering::SeqCst);
})));
let client = Client::debug(rocket).await.unwrap();
let flags = client.rocket().state::<Flags>().unwrap();
assert!(flags.liftoff.load(Ordering::SeqCst));
assert_eq!(0, flags.shutdown.load(Ordering::SeqCst));
let rocket = client.terminate().await;
let flags = rocket.state::<Flags>().unwrap();
assert_eq!(1, flags.shutdown.load(Ordering::SeqCst));
}
#[async_test]
async fn multiple_shutdown_fairing_runs() {
use rocket::local::asynchronous::Client;
let rocket = rocket::build()
.manage(Flags::default())
.attach(AdHoc::on_shutdown("Shutdown Flag 1", |rocket| Box::pin(async move {
let flags = rocket.state::<Flags>().unwrap();
flags.shutdown.fetch_add(1, Ordering::SeqCst);
})))
.attach(AdHoc::on_shutdown("Shutdown Flag 2", |rocket| Box::pin(async move {
let flags = rocket.state::<Flags>().unwrap();
flags.shutdown.fetch_add(1, Ordering::SeqCst);
})));
let client = Client::debug(rocket).await.unwrap();
let flags = client.rocket().state::<Flags>().unwrap();
assert_eq!(0, flags.shutdown.load(Ordering::SeqCst));
let rocket = client.terminate().await;
let flags = rocket.state::<Flags>().unwrap();
assert_eq!(2, flags.shutdown.load(Ordering::SeqCst));
}
#[async_test]
async fn async_slow_shutdown_doesnt_elongate_grace() {
use rocket::local::asynchronous::Client;
let mut config = rocket::Config::debug_default();
config.shutdown.grace = 1;
config.shutdown.mercy = 1;
let rocket = rocket::build()
.manage(Flags::default())
.configure(config)
.attach(AdHoc::on_shutdown("Slow Shutdown", |rocket| Box::pin(async move {
tokio::time::sleep(std::time::Duration::from_secs(4)).await;
let flags = rocket.state::<Flags>().unwrap();
flags.shutdown.fetch_add(1, Ordering::SeqCst);
})));
let client = Client::debug(rocket).await.unwrap();
let flags = client.rocket().state::<Flags>().unwrap();
assert_eq!(0, flags.shutdown.load(Ordering::SeqCst));
let start = std::time::Instant::now();
let rocket = client.terminate().await;
let elapsed = start.elapsed();
let flags = rocket.state::<Flags>().unwrap();
assert!(elapsed > std::time::Duration::from_secs(2));
assert!(elapsed < std::time::Duration::from_secs(5));
assert_eq!(1, flags.shutdown.load(Ordering::SeqCst));
}
#[test]
fn background_tasks_dont_prevent_terminate() {
use rocket::local::blocking::Client;
#[get("/")]
fn index() {
tokio::task::spawn(async {
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
});
tokio::task::spawn_blocking(|| {
std::thread::sleep(std::time::Duration::from_secs(10));
});
}
let mut config = rocket::Config::debug_default();
config.shutdown.grace = 1;
config.shutdown.mercy = 1;
let rocket = rocket::build().configure(config).mount("/", routes![index]);
let client = Client::debug(rocket).unwrap();
let response = client.get("/").dispatch();
assert!(response.status().class().is_success());
drop(response);
let _ = client.terminate();
}

View File

@ -72,8 +72,8 @@ order in which fairings are attached may be significant.
### Callbacks ### Callbacks
There are four events for which Rocket issues fairing callbacks. Each of these There are five events for which Rocket issues fairing callbacks. Each of these
events is described below: events is breifly described below and in details in the [`Fairing`] trait docs:
* **Ignite (`on_ignite`)** * **Ignite (`on_ignite`)**
@ -106,7 +106,16 @@ events is described below:
example, response fairings can also be used to inject headers into all example, response fairings can also be used to inject headers into all
outgoing responses. outgoing responses.
* **Shutdown (`on_shutdown`)**
A shutdown callback is called when [shutdown is triggered]. At this point,
graceful shutdown has commenced but not completed; no new requests are
accepted but the application may still be actively serving existing
requests. All registered shutdown fairings are run concurrently; resolution
of all fairings is awaited before resuming shutdown.
[ignition]: @api/rocket/struct.Rocket.html#method.ignite [ignition]: @api/rocket/struct.Rocket.html#method.ignite
[shutdown is triggered]: @api/rocket/config/struct.Shutdown.html#triggers
## Implementing ## Implementing
@ -115,8 +124,8 @@ Recall that a fairing is any type that implements the [`Fairing`] trait. A
[`Info`] structure. This structure is used by Rocket to assign a name to the [`Info`] structure. This structure is used by Rocket to assign a name to the
fairing and determine the set of callbacks the fairing is registering for. A fairing and determine the set of callbacks the fairing is registering for. A
`Fairing` can implement any of the available callbacks: [`on_ignite`], `Fairing` can implement any of the available callbacks: [`on_ignite`],
[`on_liftoff`], [`on_request`], and [`on_response`]. Each callback has a default [`on_liftoff`], [`on_request`], [`on_response`], and [`on_shutdown`]. Each
implementation that does absolutely nothing. callback has a default implementation that does absolutely nothing.
[`Info`]: @api/rocket/fairing/struct.Info.html [`Info`]: @api/rocket/fairing/struct.Info.html
[`info`]: @api/rocket/fairing/trait.Fairing.html#tymethod.info [`info`]: @api/rocket/fairing/trait.Fairing.html#tymethod.info
@ -124,6 +133,7 @@ implementation that does absolutely nothing.
[`on_liftoff`]: @api/rocket/fairing/trait.Fairing.html#method.on_liftoff [`on_liftoff`]: @api/rocket/fairing/trait.Fairing.html#method.on_liftoff
[`on_request`]: @api/rocket/fairing/trait.Fairing.html#method.on_request [`on_request`]: @api/rocket/fairing/trait.Fairing.html#method.on_request
[`on_response`]: @api/rocket/fairing/trait.Fairing.html#method.on_response [`on_response`]: @api/rocket/fairing/trait.Fairing.html#method.on_response
[`on_shutdown`]: @api/rocket/fairing/trait.Fairing.html#method.on_shutdown
### Requirements ### Requirements
@ -204,17 +214,16 @@ documentation](@api/rocket/fairing/trait.Fairing.html#example).
## Ad-Hoc Fairings ## Ad-Hoc Fairings
For simple occasions, implementing the `Fairing` trait can be cumbersome. This For simpler cases, implementing the `Fairing` trait can be cumbersome. This is
is why Rocket provides the [`AdHoc`] type, which creates a fairing from a simple why Rocket provides the [`AdHoc`] type, which creates a fairing from a simple
function or closure. Using the `AdHoc` type is easy: simply call the function or closure. Using the `AdHoc` type is easy: simply call the
`on_ignite`, `on_liftoff`, `on_request`, or `on_response` constructors on `on_ignite`, `on_liftoff`, `on_request`, `on_response`, or `on_shutdown`
`AdHoc` to create an `AdHoc` structure from a function or closure. constructors on `AdHoc` to create a fairing from a function or closure.
As an example, the code below creates a `Rocket` instance with two attached As an example, the code below creates a `Rocket` instance with two attached
ad-hoc fairings. The first, a liftoff fairing named "Liftoff Printer", simply ad-hoc fairings. The first, a liftoff fairing named "Liftoff Printer", prints a
prints a message indicating that the application has launched. The second named message indicating that the application has launched. The second named "Put
"Put Rewriter", a request fairing, rewrites the method of all requests to be Rewriter", a request fairing, rewrites the method of all requests to be `PUT`.
`PUT`.
```rust ```rust
use rocket::fairing::AdHoc; use rocket::fairing::AdHoc;
@ -226,6 +235,9 @@ rocket::build()
}))) })))
.attach(AdHoc::on_request("Put Rewriter", |req, _| Box::pin(async move { .attach(AdHoc::on_request("Put Rewriter", |req, _| Box::pin(async move {
req.set_method(Method::Put); req.set_method(Method::Put);
})))
.attach(AdHoc::on_shutdown("Shutdown Printer", |_| Box::pin(async move {
println!("...shutdown has commenced!");
}))); })));
``` ```