Skip to content

Commit 002e63d

Browse files
committed
Auto merge of #1672 - jtgeibel:hyper-graceful-shutdown, r=jtgeibel
Add support for graceful shutdown of the server The server process now intercepts SIGINT and SIGTERM to initiate a graceful shutdown of the server. This is useful when tracking memory leaks locally, as both `hyper` and `civet` are given a chance to return memory and shutdown cleanly. However, this will not improve things in production as we also run an instance of `nginx`, which will close all connections after receiving a SIGTERM from Heroku. From my preliminary investigation, it appears we may need to customize the buildpack to change this behavior. Additionally, the server will now briefly sleep before notifying Heroku that it is ready to receive connections. Also, when using `hyper` the `Runtime` is configured to use 4 background threads. This overrides the default, which is one thread per CPU and provides consistency between differently sized dynos. The number of `conduit` background threads are still configured via `SERVER_THREADS` and defaults to 50 in production.
2 parents 16e8d71 + beeebb4 commit 002e63d

File tree

3 files changed

+96
-14
lines changed

3 files changed

+96
-14
lines changed

Cargo.lock

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,14 @@ conduit-git-http-backend = "0.8"
7777
civet = "0.9"
7878
conduit-hyper = "0.1.3"
7979

80+
futures = "0.1"
81+
tokio = "0.1"
82+
hyper = "0.12"
83+
ctrlc = { version = "3.0", features = ["termination"] }
84+
8085
[dev-dependencies]
8186
conduit-test = "0.8"
82-
hyper = "0.12"
8387
hyper-tls = "0.3"
84-
futures = "0.1"
8588
lazy_static = "1.0"
8689
tokio-core = "0.1"
8790

src/bin/server.rs

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
#![deny(warnings, clippy::all, rust_2018_idioms)]
22

33
use cargo_registry::{boot, App, Env};
4-
use jemalloc_ctl;
54
use std::{
65
fs::File,
7-
sync::{mpsc::channel, Arc},
6+
sync::{mpsc::channel, Arc, Mutex},
7+
thread,
8+
time::Duration,
89
};
910

1011
use civet::Server as CivetServer;
1112
use conduit_hyper::Service as HyperService;
13+
use futures::Future;
14+
use jemalloc_ctl;
1215
use reqwest::Client;
1316

1417
enum Server {
1518
Civet(CivetServer),
16-
Hyper(HyperService<conduit_middleware::MiddlewareBuilder>),
19+
Hyper(tokio::runtime::Runtime),
1720
}
1821

1922
use Server::*;
@@ -56,7 +59,34 @@ fn main() {
5659

5760
let server = if dotenv::var("USE_HYPER").is_ok() {
5861
println!("Booting with a hyper based server");
59-
Hyper(HyperService::new(app, threads as usize))
62+
let addr = ([127, 0, 0, 1], port).into();
63+
let service = HyperService::new(app, threads as usize);
64+
let server = hyper::Server::bind(&addr).serve(service);
65+
66+
let (tx, rx) = futures::sync::oneshot::channel::<()>();
67+
let server = server
68+
.with_graceful_shutdown(rx)
69+
.map_err(|e| log::error!("Server error: {}", e));
70+
71+
ctrlc_handler(move || tx.send(()).unwrap_or(()));
72+
73+
let mut rt = tokio::runtime::Builder::new()
74+
.core_threads(4)
75+
.name_prefix("hyper-server-worker-")
76+
.after_start(|| {
77+
log::debug!("Stared thread {}", thread::current().name().unwrap_or("?"))
78+
})
79+
.before_stop(|| {
80+
log::debug!(
81+
"Stopping thread {}",
82+
thread::current().name().unwrap_or("?")
83+
)
84+
})
85+
.build()
86+
.unwrap();
87+
rt.spawn(server);
88+
89+
Hyper(rt)
6090
} else {
6191
println!("Booting with a civet based server");
6292
let mut cfg = civet::Config::new();
@@ -66,19 +96,43 @@ fn main() {
6696

6797
println!("listening on port {}", port);
6898

99+
// Give tokio a chance to spawn the first worker thread
100+
thread::sleep(Duration::from_millis(10));
101+
69102
// Creating this file tells heroku to tell nginx that the application is ready
70103
// to receive traffic.
71104
if heroku {
105+
println!("Writing to /tmp/app-initialized");
72106
File::create("/tmp/app-initialized").unwrap();
73107
}
74108

75-
if let Hyper(server) = server {
76-
let addr = ([127, 0, 0, 1], port).into();
77-
server.run(addr);
78-
} else {
79-
// Civet server is already running, but we need to block the main thread forever
80-
// TODO: handle a graceful shutdown by just waiting for a SIG{INT,TERM}
81-
let (_tx, rx) = channel::<()>();
82-
rx.recv().unwrap();
109+
// Block the main thread until the server has shutdown
110+
match server {
111+
Hyper(rt) => rt.shutdown_on_idle().wait().unwrap(),
112+
Civet(server) => {
113+
let (tx, rx) = channel::<()>();
114+
ctrlc_handler(move || tx.send(()).unwrap_or(()));
115+
rx.recv().unwrap();
116+
drop(server);
117+
}
83118
}
119+
120+
println!("Server has gracefully shutdown!");
121+
}
122+
123+
fn ctrlc_handler<F>(f: F)
124+
where
125+
F: FnOnce() + Send + 'static,
126+
{
127+
let call_once = Mutex::new(Some(f));
128+
129+
ctrlc::set_handler(move || {
130+
if let Some(f) = call_once.lock().unwrap().take() {
131+
println!("Starting graceful shutdown");
132+
f();
133+
} else {
134+
println!("Already sent signal to start graceful shutdown");
135+
}
136+
})
137+
.unwrap();
84138
}

0 commit comments

Comments
 (0)