Skip to content

Commit 4e68e0d

Browse files
committed
wip: graceful shutdown
1 parent 764a3cd commit 4e68e0d

File tree

6 files changed

+94
-43
lines changed

6 files changed

+94
-43
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ hyper-util = { version = "0.1.10", features = ["full"] }
5959
ignore = "0.4.20"
6060
indicatif = "0.17.3"
6161
indoc = "2.0.1"
62+
nix = "0.30"
6263
percent-encoding = "2.2"
6364
portpicker = "0.1.1"
6465
pretty_assertions = "1.3.0"

cargo-shuttle/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ hyper-util = { workspace = true }
4141
ignore = { workspace = true }
4242
indicatif = { workspace = true }
4343
indoc = { workspace = true }
44+
nix = { workspace = true, features = ["signal"] }
4445
portpicker = { workspace = true }
4546
regex = { workspace = true }
4647
reqwest = { workspace = true }

cargo-shuttle/src/lib.rs

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1503,6 +1503,8 @@ impl Shuttle {
15031503
.spawn()
15041504
.context("spawning runtime process")?
15051505
};
1506+
#[allow(unused)]
1507+
let pid = child.id().context("getting runtime's PID")?;
15061508

15071509
// Start background tasks for reading child's stdout and stderr
15081510
let raw = run_args.raw;
@@ -1555,7 +1557,7 @@ impl Shuttle {
15551557
});
15561558

15571559
#[cfg(target_family = "unix")]
1558-
let exit_result = {
1560+
let (exit_result, interrupted) = {
15591561
let mut sigterm_notif =
15601562
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
15611563
.expect("Can not get the SIGTERM signal receptor");
@@ -1564,20 +1566,20 @@ impl Shuttle {
15641566
.expect("Can not get the SIGINT signal receptor");
15651567
tokio::select! {
15661568
exit_result = child.wait() => {
1567-
Some(exit_result)
1569+
(Some(exit_result), false)
15681570
}
15691571
_ = sigterm_notif.recv() => {
15701572
eprintln!("Received SIGTERM.");
1571-
None
1573+
(None, false)
15721574
},
15731575
_ = sigint_notif.recv() => {
15741576
eprintln!("Received SIGINT.");
1575-
None
1577+
(None, true)
15761578
}
15771579
}
15781580
};
15791581
#[cfg(target_family = "windows")]
1580-
let exit_result = {
1582+
let (exit_result, interrupted) = {
15811583
let mut ctrl_break_notif = tokio::signal::windows::ctrl_break()
15821584
.expect("Can not get the CtrlBreak signal receptor");
15831585
let mut ctrl_c_notif =
@@ -1590,27 +1592,27 @@ impl Shuttle {
15901592
.expect("Can not get the CtrlShutdown signal receptor");
15911593
tokio::select! {
15921594
exit_result = runtime.wait() => {
1593-
Some(exit_result)
1595+
(Some(exit_result), false)
15941596
}
15951597
_ = ctrl_break_notif.recv() => {
15961598
eprintln!("Received ctrl-break.");
1597-
None
1599+
(None, false)
15981600
},
15991601
_ = ctrl_c_notif.recv() => {
16001602
eprintln!("Received ctrl-c.");
1601-
None
1603+
(None, true)
16021604
},
16031605
_ = ctrl_close_notif.recv() => {
16041606
eprintln!("Received ctrl-close.");
1605-
None
1607+
(None, false)
16061608
},
16071609
_ = ctrl_logoff_notif.recv() => {
16081610
eprintln!("Received ctrl-logoff.");
1609-
None
1611+
(None, false)
16101612
},
16111613
_ = ctrl_shutdown_notif.recv() => {
16121614
eprintln!("Received ctrl-shutdown.");
1613-
None
1615+
(None, false)
16141616
}
16151617
}
16161618
};
@@ -1625,24 +1627,46 @@ impl Shuttle {
16251627
bail!("Failed to wait for runtime process to exit: {e}");
16261628
}
16271629
None => {
1628-
eprintln!("Stopping runtime.");
1629-
child.kill().await?;
1630-
if run_args.build_args.docker {
1631-
let status = tokio::process::Command::new("docker")
1632-
.arg("stop")
1633-
.arg(name)
1634-
.kill_on_drop(true)
1635-
.stdout(Stdio::null())
1636-
.spawn()
1637-
.context("spawning 'docker stop'")?
1638-
.wait()
1639-
.await
1640-
.context("waiting for 'docker stop'")?;
1641-
1642-
if !status.success() {
1643-
eprintln!("WARN: 'docker stop' failed");
1630+
#[cfg(target_family = "unix")]
1631+
{
1632+
eprintln!("Stopping runtime.");
1633+
if run_args.build_args.docker {
1634+
let status = tokio::process::Command::new("docker")
1635+
.arg("stop")
1636+
.arg(name)
1637+
.kill_on_drop(true)
1638+
.stdout(Stdio::null())
1639+
.spawn()
1640+
.context("spawning 'docker stop'")?
1641+
.wait()
1642+
.await
1643+
.context("waiting for 'docker stop'")?;
1644+
1645+
if !status.success() {
1646+
eprintln!("WARN: 'docker stop' failed");
1647+
}
1648+
} else if interrupted {
1649+
nix::sys::signal::kill(
1650+
nix::unistd::Pid::from_raw(pid as i32),
1651+
nix::sys::signal::SIGINT,
1652+
)
1653+
.context("Sending SIGINT to runtime process")?;
1654+
match tokio::time::timeout(Duration::from_secs(30), child.wait()).await {
1655+
Ok(exit_result) => {
1656+
debug!("Runtime exited {:?}", exit_result)
1657+
}
1658+
Err(_) => {
1659+
eprintln!("Runtime shutdown timed out. Sending SIGKILL");
1660+
child.kill().await?;
1661+
}
1662+
};
1663+
} else {
1664+
child.kill().await?;
16441665
}
16451666
}
1667+
1668+
#[cfg(target_family = "windows")]
1669+
child.kill().await?;
16461670
}
16471671
}
16481672

runtime/src/rt.rs

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::{
33
iter::FromIterator,
44
net::{IpAddr, Ipv4Addr, SocketAddr},
55
process::exit,
6+
time::Duration,
67
};
78

89
use anyhow::Context;
@@ -253,16 +254,28 @@ pub async fn start(
253254
//
254255
info!("Starting service");
255256

256-
let service_bind = service.bind(service_addr);
257-
258257
#[cfg(target_family = "unix")]
259-
let interrupted = {
258+
async fn shutdown_signal() {
260259
let mut sigterm_notif =
261260
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
262261
.expect("Can not get the SIGTERM signal receptor");
263262
let mut sigint_notif =
264263
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())
265264
.expect("Can not get the SIGINT signal receptor");
265+
tokio::select! {
266+
_ = sigterm_notif.recv() => {
267+
tracing::warn!("Runtime received SIGTERM. Shutting down...");
268+
},
269+
_ = sigint_notif.recv() => {
270+
tracing::warn!("Runtime received SIGINT. Shutting down...");
271+
}
272+
}
273+
}
274+
275+
let service_bind = service.clone().bind(service_addr);
276+
277+
#[cfg(target_family = "unix")]
278+
let interrupted = {
266279
tokio::select! {
267280
res = service_bind => {
268281
if let Err(e) = res {
@@ -272,12 +285,7 @@ pub async fn start(
272285
tracing::warn!("Service terminated on its own. Shutting down the runtime...");
273286
false
274287
}
275-
_ = sigterm_notif.recv() => {
276-
tracing::warn!("Received SIGTERM. Shutting down the runtime...");
277-
true
278-
},
279-
_ = sigint_notif.recv() => {
280-
tracing::warn!("Received SIGINT. Shutting down the runtime...");
288+
_ = shutdown_signal() => {
281289
true
282290
}
283291
}
@@ -306,19 +314,19 @@ pub async fn start(
306314
_ = ctrl_break_notif.recv() => {
307315
tracing::warn!("Received ctrl-break. Shutting down the runtime...");
308316
true
309-
},
317+
}
310318
_ = ctrl_c_notif.recv() => {
311319
tracing::warn!("Received ctrl-c. Shutting down the runtime...");
312320
true
313-
},
321+
}
314322
_ = ctrl_close_notif.recv() => {
315323
tracing::warn!("Received ctrl-close. Shutting down the runtime...");
316324
true
317-
},
325+
}
318326
_ = ctrl_logoff_notif.recv() => {
319327
tracing::warn!("Received ctrl-logoff. Shutting down the runtime...");
320328
true
321-
},
329+
}
322330
_ = ctrl_shutdown_notif.recv() => {
323331
tracing::warn!("Received ctrl-shutdown. Shutting down the runtime...");
324332
true
@@ -327,7 +335,19 @@ pub async fn start(
327335
};
328336

329337
if interrupted {
330-
return 10;
338+
match tokio::time::timeout(Duration::from_secs(60), service.shutdown()).await {
339+
Err(_) => {
340+
tracing::error!("Service graceful shutdown timed out internally");
341+
return 11;
342+
}
343+
Ok(Err(shutdown_err)) => {
344+
tracing::error!("Service shutdown error: {shutdown_err}");
345+
return 12;
346+
}
347+
_ => {
348+
return 10;
349+
}
350+
};
331351
}
332352

333353
0

service/src/lib.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,14 @@ impl<R: Serialize + DeserializeOwned + Send> IntoResource<R> for R {
107107
/// An `Into<Service>` implementor is what is returned in the `shuttle_runtime::main` macro
108108
/// in order to run it on the Shuttle servers.
109109
#[async_trait]
110-
pub trait Service: Send {
110+
pub trait Service: Send + Clone {
111111
/// This function is run exactly once on startup of a deployment.
112112
///
113113
/// The passed [`SocketAddr`] receives proxied HTTP traffic from your Shuttle subdomain (or custom domain).
114114
/// Binding to the address is only relevant if this service is an HTTP server.
115-
async fn bind(mut self, addr: SocketAddr) -> Result<(), error::Error>;
115+
async fn bind(self, addr: SocketAddr) -> Result<(), error::Error>;
116+
117+
async fn shutdown(self) -> Result<(), CustomError> {
118+
Ok(())
119+
}
116120
}

0 commit comments

Comments
 (0)