Change serve_forever (and thus LFS and SCS servers) to new futures

Summary: As part of the tokio-0.2 migration effort, prepare `serve_forever` users to rely only on standard futures.

Reviewed By: krallin

Differential Revision: D19396792

fbshipit-source-id: 324272dd62c3d37a255cdb6f0eb825ca09420371
This commit is contained in:
Simon Farnsworth 2020-01-21 16:10:07 -08:00 committed by Facebook Github Bot
parent 563f267f8c
commit 7ab550edff
3 changed files with 85 additions and 163 deletions

View File

@ -6,18 +6,20 @@
* directory of this source tree. * directory of this source tree.
*/ */
use std::{cmp::min, fs, future::Future, io, path::Path, str::FromStr, thread, time::Duration}; use std::{cmp::min, fs, future::Future, io, path::Path, str::FromStr, time::Duration};
use anyhow::{bail, format_err, Context, Error, Result}; use anyhow::{bail, format_err, Context, Error, Result};
use clap::ArgMatches; use clap::ArgMatches;
use cloned::cloned; use cloned::cloned;
use fbinit::FacebookInit; use fbinit::FacebookInit;
use futures::sync::oneshot::Receiver; use futures::{Future as OldFuture, IntoFuture};
use futures::{future as old_future, sync, Future as OldFuture, IntoFuture}; use futures_ext::{BoxFuture, FutureExt as OldFutureExt};
use futures_ext::{BoxFuture, FutureExt}; use futures_preview::{future, StreamExt, TryFutureExt};
use panichandler::Fate;
use signal_hook::{iterator::Signals, SIGINT, SIGTERM};
use slog::{debug, error, info, Logger}; use slog::{debug, error, info, Logger};
use tokio_preview::{
signal::unix::{signal, SignalKind},
time,
};
use crate::args; use crate::args;
use crate::monitoring; use crate::monitoring;
@ -30,7 +32,7 @@ use mercurial_types::{HgChangesetId, HgManifestId};
use metaconfig_types::MetadataDBConfig; use metaconfig_types::MetadataDBConfig;
use mononoke_types::ChangesetId; use mononoke_types::ChangesetId;
use sql_ext::MysqlOptions; use sql_ext::MysqlOptions;
use stats::{schedule_stats_aggregation, schedule_stats_aggregation_preview}; use stats::schedule_stats_aggregation_preview;
pub const ARG_SHUTDOWN_GRACE_PERIOD: &str = "shutdown-grace-period"; pub const ARG_SHUTDOWN_GRACE_PERIOD: &str = "shutdown-grace-period";
pub const ARG_FORCE_SHUTDOWN_PERIOD: &str = "force-shutdown-period"; pub const ARG_FORCE_SHUTDOWN_PERIOD: &str = "force-shutdown-period";
@ -317,75 +319,45 @@ pub fn create_runtime(
/// called. This should perform any steps required to quiesce the /// called. This should perform any steps required to quiesce the
/// server. Requests should still be accepted. /// server. Requests should still be accepted.
/// ///
/// After the configured quiesce timeout, the `server` future is /// After the configured quiesce timeout, the `shutdown` future is awaited.
/// cancelled, and the `shutdown` callback is called. This should do /// This should do any additional work to stop accepting connections and wait
/// any additional work to stop accepting connections and wait until all /// until all outstanding requests have been handled. The `server` future
/// outstanding requests have been handled. /// continues to run while `shutdown` is being awaited.
/// ///
/// Currently the `shutdown` callback can return `true` to indicate that /// Once `shutdown` returns, the `server` future is cancelled, and the process
/// the runtime should wait until it is idle before shutting down. Note /// exits. If `shutdown_timeout` is exceeded, an error is returned.
/// that this option will be removed in Tokio 0.2.
/// pub fn serve_forever<Server, QuiesceFn, ShutdownFut>(
/// When `shutdown` completes, or when the force shutdown timer expires, the mut runtime: tokio_compat::runtime::Runtime,
/// runtime will be shutdown and the process will exit.
pub fn serve_forever<Server, QuiesceFn, ShutdownFn>(
runtime: tokio_compat::runtime::Runtime,
server: Server, server: Server,
logger: &Logger, logger: &Logger,
quiesce: QuiesceFn, quiesce: QuiesceFn,
shutdown_grace_period: Duration, shutdown_grace_period: Duration,
shutdown: ShutdownFn, shutdown: ShutdownFut,
shutdown_timeout: Duration, shutdown_timeout: Duration,
) -> Result<(), Error> ) -> Result<(), Error>
where where
Server: OldFuture<Item = (), Error = ()> + Send + 'static, Server: Future<Output = ()> + Send + 'static,
QuiesceFn: FnOnce(), QuiesceFn: FnOnce(),
ShutdownFn: FnOnce() -> bool, ShutdownFut: Future<Output = ()>,
{ {
// Block until receiving a signal that tells us to exit. runtime.block_on_std(async {
let block = || -> Result<(), Error> { let mut terminate = signal(SignalKind::terminate())?;
let signals = Signals::new(&[SIGTERM, SIGINT])?; let mut interrupt = signal(SignalKind::interrupt())?;
for signal in signals.forever() { // This future becomes ready when we receive a termination signal
info!(&logger, "Signalled: {}", signal); let signalled = future::select(terminate.next(), interrupt.next());
break;
}
Ok(())
};
block_on_fn(
runtime,
server,
logger,
block,
quiesce,
shutdown_grace_period,
shutdown,
shutdown_timeout,
)?;
Ok(()) let stats_agg = schedule_stats_aggregation_preview()
} .map_err(|_| Error::msg("Failed to create stats aggregation worker"))?;
// Note: this returns a JoinHandle, which we drop, thus detaching the task
// It thus does not count towards shutdown_on_idle below
tokio_preview::task::spawn(stats_agg);
pub fn block_on_fn<Server, QuiesceFn, ShutdownFn, BlockFn>( // Spawn the server onto its own task
runtime: tokio_compat::runtime::Runtime, tokio_preview::task::spawn(server);
server: Server,
logger: &Logger,
block: BlockFn,
quiesce: QuiesceFn,
shutdown_grace_period: Duration,
shutdown: ShutdownFn,
shutdown_timeout: Duration,
) -> Result<(), Error>
where
Server: OldFuture<Item = (), Error = ()> + Send + 'static,
QuiesceFn: FnOnce(),
ShutdownFn: FnOnce() -> bool,
BlockFn: FnOnce() -> Result<(), Error>,
{
let (shutdown_pub, shutdown_sub) = sync::oneshot::channel::<()>();
let main = join_stats_agg(server, shutdown_sub)?;
runtime.spawn(main);
block()?; // Now wait for the termination signal
signalled.await;
// Shutting down: wait for the grace period. // Shutting down: wait for the grace period.
quiesce(); quiesce();
@ -394,31 +366,14 @@ where
"Waiting {}s before shutting down server", "Waiting {}s before shutting down server",
shutdown_grace_period.as_secs(), shutdown_grace_period.as_secs(),
); );
thread::sleep(shutdown_grace_period);
time::delay_for(shutdown_grace_period).await;
info!(&logger, "Shutting down..."); info!(&logger, "Shutting down...");
let _ = shutdown_pub.send(()); time::timeout(shutdown_timeout, shutdown)
.map_err(|_| Error::msg("Timed out shutting down server"))
// Create a background thread to panic if we fail to shutdown within the timeout. .await
panichandler::set_panichandler(Fate::Abort); })
thread::spawn(move || {
thread::sleep(shutdown_timeout);
panic!("Timed out shutting down runtime");
});
if shutdown() {
runtime
.shutdown_on_idle()
.wait()
.map_err(|_| Error::msg("Failed to shutdown runtime!"))?;
} else {
runtime
.shutdown_now()
.wait()
.map_err(|_| Error::msg("Failed to shutdown runtime!"))?;
}
Ok(())
} }
/// Executes the future and waits for it to finish. /// Executes the future and waits for it to finish.
@ -457,35 +412,6 @@ where
}) })
} }
/// Join the future with stats aggregator and return a new joined future
fn join_stats_agg<F>(future: F, shutdown_sub: Receiver<()>) -> Result<BoxFuture<(), ()>, Error>
where
F: OldFuture<Item = (), Error = ()> + Send + 'static,
{
let stats_agg = schedule_stats_aggregation()
.map_err(|_| Error::msg("Failed to create stats aggregation worker"))?
.discard();
let main = (stats_agg, future)
.into_future()
.select2(shutdown_sub)
.then({
move |res| -> Result<(), ()> {
match res {
Ok(old_future::Either::B(_)) => Ok(()),
Err(old_future::Either::A(_)) => Err(()),
_ => {
// NOTE: We need to panic here, because otherwise main is going to be blocked on
// waiting for a signal forever. This shouldn't normally ever happen.
unreachable!("Terminated or signal listener was dropped.")
}
}
}
});
Ok(main.boxify())
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
@ -493,7 +419,6 @@ mod test {
use anyhow::Error; use anyhow::Error;
use futures_preview::future::lazy; use futures_preview::future::lazy;
use slog_glog_fmt; use slog_glog_fmt;
use tokio_timer::sleep;
fn create_logger() -> Logger { fn create_logger() -> Logger {
slog_glog_fmt::facebook_logger().unwrap() slog_glog_fmt::facebook_logger().unwrap()
@ -522,23 +447,4 @@ mod test {
let res = block_execute(future, fb, "test_app", &logger, &matches); let res = block_execute(future, fb, "test_app", &logger, &matches);
assert!(res.is_err()); assert!(res.is_err());
} }
#[test]
fn test_block_on_fn_shutsdown() {
let logger = create_logger();
let matches = exec_matches();
let runtime = args::init_runtime(&matches).unwrap();
let server = sleep(Duration::from_secs(42)).discard();
block_on_fn(
runtime,
server,
&logger,
|| -> Result<(), Error> { Ok(()) },
|| (),
Duration::from_secs(0),
|| true,
Duration::from_secs(10),
)
.unwrap();
}
} }

View File

@ -17,10 +17,16 @@ use cloned::cloned;
use fbinit::FacebookInit; use fbinit::FacebookInit;
use futures::{Future, IntoFuture}; use futures::{Future, IntoFuture};
use futures_ext::FutureExt as Futures01Ext; use futures_ext::FutureExt as Futures01Ext;
use futures_preview::TryFutureExt; use futures_preview::{
use futures_util::{compat::Future01CompatExt, future::try_join_all, try_join}; channel::oneshot,
compat::Future01CompatExt,
future::{lazy, select, try_join_all},
FutureExt, TryFutureExt,
};
// TODO: When we get rid of old futures, this can come from `futures` (can't while new futures is called `futures_preview`)
use futures_util::try_join;
use gotham::{bind_server, bind_server_with_pre_state}; use gotham::{bind_server, bind_server_with_pre_state};
use slog::{info, warn}; use slog::{error, info, warn};
use std::collections::HashMap; use std::collections::HashMap;
use std::net::ToSocketAddrs; use std::net::ToSocketAddrs;
use std::sync::{atomic::AtomicBool, atomic::Ordering, Arc}; use std::sync::{atomic::AtomicBool, atomic::Ordering, Arc};
@ -285,7 +291,7 @@ fn main(fb: FacebookInit) -> Result<(), Error> {
let mut runtime = args::init_runtime(&matches)?; let mut runtime = args::init_runtime(&matches)?;
let repos: HashMap<_, _> = runtime let repos: HashMap<_, _> = runtime
.block_on(try_join_all(futs).compat())? .block_on_std(try_join_all(futs))?
.into_iter() .into_iter()
.collect(); .collect();
@ -410,20 +416,28 @@ fn main(fb: FacebookInit) -> Result<(), Error> {
}; };
info!(&logger, "Listening on {:?}", addr); info!(&logger, "Listening on {:?}", addr);
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
serve_forever( serve_forever(
runtime, runtime,
server, select(
server.compat().map_err({
let logger = logger.clone();
move |e| error!(&logger, "Unhandled error: {:?}", e)
}),
shutdown_rx,
)
.map(|_| ()),
&logger, &logger,
move || will_exit.store(true, Ordering::Relaxed), move || will_exit.store(true, Ordering::Relaxed),
args::get_shutdown_grace_period(&matches)?, args::get_shutdown_grace_period(&matches)?,
move || { lazy(move |_| {
// Currently we rely on `shutdown_on_idle` waiting for all let _ = shutdown_tx.send(());
// in-flight requests to complete. // Currently we kill off in-flight requests as soon as we've closed the listener.
// // If this is a problem in prod, this would be the point at which to wait
// TODO: in preparation for migration to Tokio 0.2, wait // for all connections to shut down.
// until all requests have completed. // To do this properly, we'd need to track the `Connection` futures that Gotham
true // gets from Hyper, tell them to gracefully shutdown, then wait for them to complete
}, }),
args::get_shutdown_timeout(&matches)?, args::get_shutdown_timeout(&matches)?,
)?; )?;

View File

@ -21,7 +21,7 @@ use fb303::server::make_FacebookService_server;
use fb303_core::server::make_BaseService_server; use fb303_core::server::make_BaseService_server;
use fbinit::FacebookInit; use fbinit::FacebookInit;
use futures_ext::FutureExt as Futures01Ext; use futures_ext::FutureExt as Futures01Ext;
use futures_preview::{FutureExt, TryFutureExt}; use futures_preview::{compat::Future01CompatExt, FutureExt, TryFutureExt};
use metaconfig_parser::RepoConfigs; use metaconfig_parser::RepoConfigs;
use mononoke_api::{CoreContext, Mononoke}; use mononoke_api::{CoreContext, Mononoke};
use panichandler::Fate; use panichandler::Fate;
@ -31,6 +31,7 @@ use srserver::service_framework::{
BuildModule, Fb303Module, ProfileModule, ServiceFramework, ThriftStatsModule, BuildModule, Fb303Module, ProfileModule, ServiceFramework, ThriftStatsModule,
}; };
use srserver::{ThriftServer, ThriftServerBuilder}; use srserver::{ThriftServer, ThriftServerBuilder};
use tokio_preview::task;
mod commit_id; mod commit_id;
mod errors; mod errors;
@ -168,16 +169,17 @@ fn main(fb: FacebookInit) -> Result<(), Error> {
.expect("failed to start thrift service"); .expect("failed to start thrift service");
serve_forever( serve_forever(
runtime, runtime,
monitoring_forever.discard(), monitoring_forever.discard().compat().map(|_| ()),
&logger, &logger,
move || will_exit.store(true, Ordering::Relaxed), move || will_exit.store(true, Ordering::Relaxed),
args::get_shutdown_grace_period(&matches)?, args::get_shutdown_grace_period(&matches)?,
move || { async {
// Note that async blocks are lazy, so this isn't called until first poll
let _ = task::spawn_blocking(move || {
// Calling `stop` blocks until the service has completed all requests. // Calling `stop` blocks until the service has completed all requests.
service_framework.stop(); service_framework.stop();
})
// Runtime should shutdown now. .await;
false
}, },
args::get_shutdown_timeout(&matches)?, args::get_shutdown_timeout(&matches)?,
)?; )?;