Skip to content

Commit 0dc36d3

Browse files
committed
Fix slow process exit issue
The slow process exit is caused by two issues. 1. Blocking tokio operations. This can either be caused by `spawn_blocking` (which is internally used by a lot of tokio std operations) and using block_on. The "guard" pattern can especially causes this because tokio is not expecting to block in a runtime thread. 2. Not receiving destroy event. We listen for the destroy event to determine if the removal is successful. However we only `.await` it after sending the removal call. So if the removal takes a short time, then the container could be destroyed before we start listening (since future does nothing until polled). This is fixed by spawning.
1 parent ec636b6 commit 0dc36d3

File tree

3 files changed

+68
-59
lines changed

3 files changed

+68
-59
lines changed

src/docker/container.rs

+17-20
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,29 @@ pub struct Container {
1717
pub(super) remove_event: Shared<BoxFuture<'static, Option<EventMessage>>>,
1818
}
1919

20-
pub struct ContainerGuard(Option<Container>, Timeout);
21-
22-
impl Drop for ContainerGuard {
23-
fn drop(&mut self) {
24-
let container = self.0.take().unwrap();
25-
let timeout = self.1;
26-
let _ = futures::executor::block_on(container.remove(timeout));
27-
}
28-
}
29-
3020
impl Container {
3121
pub fn id(&self) -> &str {
3222
&self.id
3323
}
3424

35-
pub fn guard(&self, timeout: Timeout) -> ContainerGuard {
36-
ContainerGuard(Some(self.clone()), timeout)
37-
}
38-
3925
pub async fn remove(&self, timeout: Timeout) -> Result<()> {
40-
self.rename(format!("removing-{}", self.id)).await?;
41-
let options = bollard::container::RemoveContainerOptions {
42-
force: true,
43-
..Default::default()
44-
};
45-
let _ = self.docker.remove_container(&self.id, Some(options)).await;
26+
log::info!("Removing container {}", self.id);
27+
28+
// Since we passed "--rm" flag, docker will automatically start removing the container.
29+
// Ignore any error for manual removal.
30+
let _: Result<()> = async {
31+
self.rename(format!("removing-{}", self.id)).await?;
32+
let options = bollard::container::RemoveContainerOptions {
33+
force: true,
34+
..Default::default()
35+
};
36+
self.docker
37+
.remove_container(&self.id, Some(options))
38+
.await?;
39+
Ok(())
40+
}
41+
.await;
42+
4643
if let Timeout::Some(duration) = timeout {
4744
let _ = tokio::time::timeout(duration, self.remove_event.clone()).await;
4845
} else {

src/docker/docker.rs

+6-9
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,11 @@ fn container_removed_future(
6262
..Default::default()
6363
};
6464

65-
let removed = docker
66-
.events(Some(options))
67-
.map(|evt| evt.ok())
68-
.take(1)
69-
.collect::<Vec<_>>()
70-
.map(|vec| vec.into_iter().next().flatten())
71-
.boxed()
72-
.shared();
65+
let mut events = docker.events(Some(options));
7366

74-
removed
67+
// Spawn the future to start listening event.
68+
tokio::spawn(async move { events.next().await?.ok() })
69+
.map(|x| x.ok().flatten())
70+
.boxed()
71+
.shared()
7572
}

src/main.rs

+45-30
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ use cli::{Device, LogFormat, Symlink, Timeout};
77
use docker::{Container, Docker};
88
use hotplug::{Event as HotPlugEvent, HotPlug, PluggedDevice};
99

10-
use std::{fmt::Display, path::Path, process::ExitCode};
10+
use std::pin::pin;
11+
use std::{fmt::Display, path::Path};
1112
use tokio_stream::StreamExt;
1213

1314
use anyhow::{bail, Context, Result};
@@ -148,9 +149,9 @@ fn run_hotplug(
148149
}
149150
}
150151

151-
async fn hotplug_main() -> Result<ExitCode> {
152+
async fn hotplug_main() -> Result<u8> {
152153
let args = Args::parse();
153-
let mut status = ExitCode::SUCCESS;
154+
let mut status = 0;
154155

155156
match args.action {
156157
Action::Run {
@@ -184,7 +185,6 @@ async fn hotplug_main() -> Result<ExitCode> {
184185

185186
let docker = Docker::connect_with_defaults()?;
186187
let container = docker.run(docker_args).await?;
187-
let _guard = container.guard(remove_timeout);
188188
let _ = container.pipe_signals();
189189

190190
let hub_path = root_device.hub()?.syspath().to_owned();
@@ -197,45 +197,60 @@ async fn hotplug_main() -> Result<ExitCode> {
197197
}
198198
};
199199

200-
let stream = tokio_stream::empty()
200+
let stream = pin!(tokio_stream::empty()
201201
.merge(hotplug_stream)
202-
.merge(container_stream);
203-
204-
tokio::pin!(stream);
205-
while let Some(event) = stream.next().await {
206-
let event = event?;
207-
info!("{}", event);
208-
match event {
209-
Event::Remove(dev) if dev.syspath() == hub_path => {
210-
info!("Hub device detached. Stopping container.");
211-
status = ExitCode::from(root_unplugged_exit_code);
212-
container.kill(15).await?;
213-
break;
214-
}
215-
Event::Stopped(_, code) => {
216-
if code >= 0 && code < 256 && (code as u8) != root_unplugged_exit_code {
217-
status = ExitCode::from(code as u8);
218-
} else {
219-
status = ExitCode::FAILURE;
202+
.merge(container_stream));
203+
204+
let result: Result<()> = async {
205+
tokio::pin!(stream);
206+
while let Some(event) = stream.next().await {
207+
let event = event?;
208+
info!("{}", event);
209+
match event {
210+
Event::Remove(dev) if dev.syspath() == hub_path => {
211+
info!("Hub device detached. Stopping container.");
212+
status = root_unplugged_exit_code;
213+
container.kill(15).await?;
214+
break;
215+
}
216+
Event::Stopped(_, code) => {
217+
status = 1;
218+
if let Ok(code) = u8::try_from(code) {
219+
// Use the container exit code, but only if it won't be confused
220+
// with the pre-defined root_unplugged_exit_code.
221+
if code != root_unplugged_exit_code {
222+
status = code;
223+
}
224+
} else {
225+
status = 1;
226+
}
227+
break;
220228
}
221-
break;
229+
_ => {}
222230
}
223-
_ => {}
224231
}
232+
Ok(())
225233
}
234+
.await;
235+
236+
let _ = container.remove(remove_timeout).await;
237+
result?
226238
}
227239
};
228240

229241
Ok(status)
230242
}
231243

232244
#[tokio::main]
233-
async fn main() -> ExitCode {
234-
match hotplug_main().await {
245+
async fn main() {
246+
let code = match hotplug_main().await {
235247
Ok(code) => code,
236248
Err(err) => {
237-
let _ = eprintln!("Error: {err:?}");
238-
ExitCode::FAILURE
249+
eprintln!("Error: {err:?}");
250+
1
239251
}
240-
}
252+
};
253+
// Upon returning from `main`, tokio will attempt to shutdown, but if there're any blocking
254+
// operation (e.g. fs operations), then the shutdown will hang.
255+
std::process::exit(code.into());
241256
}

0 commit comments

Comments
 (0)