-
Notifications
You must be signed in to change notification settings - Fork 3
Remove async code from the Drop handler and make cancellation more gentle for uv
#90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
574d5ba
9ea1160
816b204
94946c4
0b6daa5
3e66c7a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ use tokio::{ | |
| fs, | ||
| io::{AsyncRead, BufReader, AsyncBufReadExt}, | ||
| process::{Child, Command}, | ||
| runtime::Handle, | ||
| sync::{ | ||
| Mutex, | ||
| oneshot::{ | ||
|
|
@@ -28,10 +29,19 @@ use tokio::{ | |
| time::{timeout, Duration}, | ||
| }; | ||
|
|
||
| #[cfg(unix)] | ||
| use nix::{ | ||
| unistd::Pid, | ||
| sys::signal::{ | ||
| Signal, | ||
| killpg, | ||
| }, | ||
| }; | ||
|
|
||
| use tokio_util::sync::CancellationToken; | ||
|
|
||
| use tower_package::{Manifest, Package}; | ||
| use tower_telemetry::debug; | ||
| use tower_telemetry::{debug, error}; | ||
| use tower_uv::Uv; | ||
|
|
||
| use crate::{ | ||
|
|
@@ -49,7 +59,7 @@ pub struct LocalApp { | |
| waiter: Mutex<oneshot::Receiver<i32>>, | ||
|
|
||
| // terminator is what we use to flag that we want to terminate the child process. | ||
| terminator: Mutex<CancellationToken>, | ||
| terminator: CancellationToken, | ||
|
|
||
| // execute_handle keeps track of the current state of the execution lifecycle. | ||
| execute_handle: Option<JoinHandle<Result<(), Error>>>, | ||
|
|
@@ -271,20 +281,28 @@ async fn execute_local_app(opts: StartOptions, sx: oneshot::Sender<i32>, cancel_ | |
|
|
||
| impl Drop for LocalApp { | ||
| fn drop(&mut self) { | ||
| // We want to ensure that we cancel the process if it is still running. | ||
| let _ = self.terminate(); | ||
| // CancellationToken::cancel() is not async | ||
| self.terminator.cancel(); | ||
|
|
||
| // Optionally spawn a task to wait for the handle | ||
| if let Some(execute_handle) = self.execute_handle.take() { | ||
| if let Ok(handle) = Handle::try_current() { | ||
| handle.spawn(async move { | ||
| let _ = execute_handle.await; | ||
| }); | ||
| } | ||
|
Comment on lines
+289
to
+293
|
||
| } | ||
| } | ||
| } | ||
|
|
||
| impl App for LocalApp { | ||
| async fn start(opts: StartOptions) -> Result<Self, Error> { | ||
| let cancel_token = CancellationToken::new(); | ||
| let terminator = Mutex::new(cancel_token.clone()); | ||
| let terminator = CancellationToken::new(); | ||
|
|
||
| let (sx, rx) = oneshot::channel::<i32>(); | ||
| let waiter = Mutex::new(rx); | ||
|
|
||
| let handle = tokio::spawn(execute_local_app(opts, sx, cancel_token)); | ||
| let handle = tokio::spawn(execute_local_app(opts, sx, terminator.clone())); | ||
| let execute_handle = Some(handle); | ||
|
|
||
| Ok(Self { | ||
|
|
@@ -323,8 +341,7 @@ impl App for LocalApp { | |
| } | ||
|
|
||
| async fn terminate(&mut self) -> Result<(), Error> { | ||
| let terminator = self.terminator.lock().await; | ||
| terminator.cancel(); | ||
| self.terminator.cancel(); | ||
|
|
||
| // Now we should wait for the join handle to finish. | ||
| if let Some(execute_handle) = self.execute_handle.take() { | ||
|
|
@@ -421,11 +438,57 @@ fn make_env_vars(ctx: &tower_telemetry::Context, env: &str, cwd: &PathBuf, secs: | |
| res | ||
| } | ||
|
|
||
| #[cfg(unix)] | ||
| async fn kill_child_process(ctx: &tower_telemetry::Context, mut child: Child) { | ||
| let pid = match child.id() { | ||
| Some(pid) => pid, | ||
| None => { | ||
| // We didn't get anything, so we can't do anything. Let's just exit with a debug | ||
| // message. | ||
| error!(ctx: &ctx, "child process has no pid, cannot kill"); | ||
| return; | ||
| } | ||
| }; | ||
|
|
||
| // This is the actual converted pid. | ||
| let pid = Pid::from_raw(pid as i32); | ||
|
|
||
| // We first send a SIGTERM to ensure that the child processes are terminated. Using SIGKILL | ||
| // (default behavior in Child::kill) can leave orphaned processes behind. | ||
| killpg( | ||
| pid, | ||
| Signal::SIGTERM | ||
| ).ok(); | ||
|
|
||
| // If it doesn't die after 2 seconds then we'll forcefully kill it. This timeout should be less | ||
| // than the overall timeout for the process (which should likely live on the context as a | ||
| // deadline). | ||
| let timeout = timeout( | ||
| Duration::from_secs(2), | ||
| child.wait() | ||
| ).await; | ||
|
|
||
| if timeout.is_err() { | ||
| killpg( | ||
| pid, | ||
| Signal::SIGKILL | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe a bit old school but i reckon a SIGHUP before SIGKILL might be an idea |
||
| ).ok(); | ||
| } | ||
| } | ||
|
|
||
| #[cfg(not(unix))] | ||
| async fn kill_child_process(ctx: &tower_telemetry::Context, mut child: Child) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok so in windows will default back to whatever
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, apparently (untested by me) it's not an issue on Windows. |
||
| match child.kill().await { | ||
| Ok(_) => debug!(ctx: &ctx, "child process killed successfully"), | ||
| Err(e) => debug!(ctx: &ctx, "failed to kill child process: {}", e), | ||
| }; | ||
| } | ||
|
|
||
| async fn wait_for_process(ctx: tower_telemetry::Context, cancel_token: &CancellationToken, mut child: Child) -> i32 { | ||
| let code = loop { | ||
| if cancel_token.is_cancelled() { | ||
| debug!(ctx: &ctx, "process cancelled, terminating child process"); | ||
| let _ = child.kill().await; | ||
| kill_child_process(&ctx, child).await; | ||
| break -1; // return -1 to indicate that the process was cancelled. | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If
Handle::try_current()fails, the execute handle is never awaited, potentially leaving the spawned task running indefinitely. Consider handling this case or documenting the expected behavior when no runtime handle is available.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above.