Skip to content

Commit 6eff2b8

Browse files
committed
wip: graceful shutdown
1 parent fa04c67 commit 6eff2b8

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
@@ -1502,6 +1502,8 @@ impl Shuttle {
15021502
.spawn()
15031503
.context("spawning runtime process")?
15041504
};
1505+
#[allow(unused)]
1506+
let pid = child.id().context("getting runtime's PID")?;
15051507

15061508
// Start background tasks for reading child's stdout and stderr
15071509
let raw = run_args.raw;
@@ -1554,7 +1556,7 @@ impl Shuttle {
15541556
});
15551557

15561558
#[cfg(target_family = "unix")]
1557-
let exit_result = {
1559+
let (exit_result, interrupted) = {
15581560
let mut sigterm_notif =
15591561
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
15601562
.expect("Can not get the SIGTERM signal receptor");
@@ -1563,20 +1565,20 @@ impl Shuttle {
15631565
.expect("Can not get the SIGINT signal receptor");
15641566
tokio::select! {
15651567
exit_result = child.wait() => {
1566-
Some(exit_result)
1568+
(Some(exit_result), false)
15671569
}
15681570
_ = sigterm_notif.recv() => {
15691571
eprintln!("Received SIGTERM.");
1570-
None
1572+
(None, false)
15711573
},
15721574
_ = sigint_notif.recv() => {
15731575
eprintln!("Received SIGINT.");
1574-
None
1576+
(None, true)
15751577
}
15761578
}
15771579
};
15781580
#[cfg(target_family = "windows")]
1579-
let exit_result = {
1581+
let (exit_result, interrupted) = {
15801582
let mut ctrl_break_notif = tokio::signal::windows::ctrl_break()
15811583
.expect("Can not get the CtrlBreak signal receptor");
15821584
let mut ctrl_c_notif =
@@ -1589,27 +1591,27 @@ impl Shuttle {
15891591
.expect("Can not get the CtrlShutdown signal receptor");
15901592
tokio::select! {
15911593
exit_result = child.wait() => {
1592-
Some(exit_result)
1594+
(Some(exit_result), false)
15931595
}
15941596
_ = ctrl_break_notif.recv() => {
15951597
eprintln!("Received ctrl-break.");
1596-
None
1598+
(None, false)
15971599
},
15981600
_ = ctrl_c_notif.recv() => {
15991601
eprintln!("Received ctrl-c.");
1600-
None
1602+
(None, true)
16011603
},
16021604
_ = ctrl_close_notif.recv() => {
16031605
eprintln!("Received ctrl-close.");
1604-
None
1606+
(None, false)
16051607
},
16061608
_ = ctrl_logoff_notif.recv() => {
16071609
eprintln!("Received ctrl-logoff.");
1608-
None
1610+
(None, false)
16091611
},
16101612
_ = ctrl_shutdown_notif.recv() => {
16111613
eprintln!("Received ctrl-shutdown.");
1612-
None
1614+
(None, false)
16131615
}
16141616
}
16151617
};
@@ -1624,24 +1626,46 @@ impl Shuttle {
16241626
bail!("Failed to wait for runtime process to exit: {e}");
16251627
}
16261628
None => {
1627-
eprintln!("Stopping runtime.");
1628-
child.kill().await?;
1629-
if run_args.build_args.docker {
1630-
let status = tokio::process::Command::new("docker")
1631-
.arg("stop")
1632-
.arg(name)
1633-
.kill_on_drop(true)
1634-
.stdout(Stdio::null())
1635-
.spawn()
1636-
.context("spawning 'docker stop'")?
1637-
.wait()
1638-
.await
1639-
.context("waiting for 'docker stop'")?;
1640-
1641-
if !status.success() {
1642-
eprintln!("WARN: 'docker stop' failed");
1629+
#[cfg(target_family = "unix")]
1630+
{
1631+
eprintln!("Stopping runtime.");
1632+
if run_args.build_args.docker {
1633+
let status = tokio::process::Command::new("docker")
1634+
.arg("stop")
1635+
.arg(name)
1636+
.kill_on_drop(true)
1637+
.stdout(Stdio::null())
1638+
.spawn()
1639+
.context("spawning 'docker stop'")?
1640+
.wait()
1641+
.await
1642+
.context("waiting for 'docker stop'")?;
1643+
1644+
if !status.success() {
1645+
eprintln!("WARN: 'docker stop' failed");
1646+
}
1647+
} else if interrupted {
1648+
nix::sys::signal::kill(
1649+
nix::unistd::Pid::from_raw(pid as i32),
1650+
nix::sys::signal::SIGINT,
1651+
)
1652+
.context("Sending SIGINT to runtime process")?;
1653+
match tokio::time::timeout(Duration::from_secs(30), child.wait()).await {
1654+
Ok(exit_result) => {
1655+
debug!("Runtime exited {:?}", exit_result)
1656+
}
1657+
Err(_) => {
1658+
eprintln!("Runtime shutdown timed out. Sending SIGKILL");
1659+
child.kill().await?;
1660+
}
1661+
};
1662+
} else {
1663+
child.kill().await?;
16431664
}
16441665
}
1666+
1667+
#[cfg(target_family = "windows")]
1668+
child.kill().await?;
16451669
}
16461670
}
16471671

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)