Skip to content

Commit c5577c0

Browse files
committed
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.
1 parent 1b42e4c commit c5577c0

File tree

3 files changed

+95
-15
lines changed

3 files changed

+95
-15
lines changed

Cargo.lock

Lines changed: 26 additions & 1 deletion
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
@@ -78,11 +78,14 @@ conduit-git-http-backend = "0.8"
7878
civet = "0.9"
7979
conduit-hyper = "0.1.3"
8080

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

src/bin/server.rs

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
#![deny(warnings)]
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

1316
enum Server {
1417
Civet(CivetServer),
15-
Hyper(HyperService<conduit_middleware::MiddlewareBuilder>),
18+
Hyper(tokio::runtime::Runtime),
1619
}
1720

1821
use Server::*;
@@ -53,7 +56,32 @@ fn main() {
5356

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

6492
println!("listening on port {}", port);
6593

94+
// Give tokio a chance to spawn the first worker thread
95+
thread::sleep(Duration::from_millis(10));
96+
6697
// Creating this file tells heroku to tell nginx that the application is ready
6798
// to receive traffic.
6899
if heroku {
100+
println!("Writing to /tmp/app-initialized");
69101
File::create("/tmp/app-initialized").unwrap();
70102
}
71103

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

0 commit comments

Comments
 (0)