-
-
Notifications
You must be signed in to change notification settings - Fork 604
Add timeouts for commit hooks #2547
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
base: master
Are you sure you want to change the base?
Conversation
Opening early as a draft. Initial changes are working and have tests added for them just need to figure out how we are going to implement a timeout value, if thats user configuration or a default impl. |
Was wanting to get some feedback on this before progressing further, is this the correct place to implement the timeout logic? @extrawurst |
This would impact other users like Gitbutler. @Byron what do you think about this change? |
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.
From an implementation standpoint, this looks good. The only thing I would add is tests for non-zero timeouts (maybe that’s something you already planned or maybe I missed something).
Also worth mentioning as its done in this PR, I've changed the test shell shebang to use |
I've also noticed that while running tests that since i use the non POSIX Shell (nushell) that i need to prefix the command with SHELL=bash, i was wondering if it may be worth testing if the $SHELL is a known non compliant shell and trying to look for a default POSIX shell in the path? |
Now that the implementation is solid how do i go about with setting the timeout, how do we expose configurable value for the user and should there be a default timeout? |
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.
Thanks for letting me know, @extrawurst.
The changes seem to not apply to git2-hooks
, so wouldn't affect GitButler in its current form. However, that would change if timeouts would move into git2-hooks
as I seem to be proposing :).
PS: I unsubscribed as this PR is relevant to me only once git2-hooks
is involved. You can always ping me to change that.
@Byron I've updated the implementation and it is now inside git2-hooks, I've added new functions with suffixed |
The last remaining thing to do is move the tests i had written over to the git2_hooks crate, even so the current tests in asyncgit should be able to cover most cases i think. |
I was also thinking about the initial issue where the problem was an infinite wait because the hook was waiting for input, maybe a solution could be when a hook is running a new window is shown with the stdio from the hook and also allowing stdin, not sure if that would work how im thinking though. |
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.
Thanks a lot for the quick turnaround!
The new implementation definitely addresses the previous issue, and now the timeout has an actual effect on the hook.
When looking at git2-hooks
exclusively, I am not sure I saw a test for this though.
@@ -66,6 +67,10 @@ pub enum HookResult { | |||
/// path of the hook that was run | |||
hook: PathBuf, | |||
}, | |||
TimedOut { |
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.
Docs are missing.
Probably the crate could use a deny(missing_docs)
as well.
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.
I looked at adding deny(missing_docs)
to the crate but there are a number of errors that are outside the scope of this PR.
I'll get some tests put into the git2-hooks crate and resolve these issue when I get some times on the weekend to do so, please let me know if there is anything else that should be looked into for when i get to it. |
b945cb6
to
0474735
Compare
The idea would be to leave the
I don't think this can be a solution, as hooking up Nonetheless I agree that timeouts for hooks may be solving for 'buggy' hooks, and it might be better to fix those (or understand precisely why they maybe hang legitimately so that can be fixed on the side of |
I've made this change so run_hooks is left alone functionally and there is now the with_timeout version.
I think for now its best to track this in a new issue / PR, this one is pretty complete and ready to go as it is. |
@DaRacci please update with master |
…meouts All hooks functions now have their own with_timeout variant inside git2-hooks and asyncgit. The previous implementation of using a new thread with a timeout has been ditched in favour of a child command which we can now poll and kill to prevent the hook from continueing. I've added a hard coded default 5 second timeout but this will be handled by the Options struct later
Also adds tests for this function and the usage of timeouts in git2-hooks
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.
Hi @DaRacci. Thank you for your work on this, I appreciate it. This all seems well though-out and usable.
I do have some points regarding some details of your implementation I'd like to discuss before I give the go-ahead.
@@ -247,7 +346,7 @@ mod tests { | |||
|
|||
assert_eq!(res, HookResult::NoHookFound); | |||
|
|||
let hook = b"#!/bin/sh | |||
let hook = b"#!/usr/bin/env sh |
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.
This is only tests, so who cares, but I'm curious why you changed that.
For the POSIX shell shebang to /bin/sh
is portable, /usr/bin/env
isn't. The latter is only relevant for more exotic languages like python, etc, AFAIK.
Did you have trouble running these tests on your machine without that change? Doesn't even nixos provide a link from /bin/sh to bash by default?
"Timeout", | ||
&self.options.borrow().hook_timeout().map_or_else( | ||
|| "None".to_string(), | ||
|d| format!("{d:?}"), |
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.
Using fmt::Debug
straight into the UI seems smelly.
/// Git hook: `commit_msg` | ||
/// | ||
/// See [`hooks_commit_msg`] for more details. | ||
pub fn hooks_commit_msg_with_timeout( |
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.
This function shares code with hooks_commit_msg. Normally, I'd not be too pedantic about that, but here I see a clear chance that this would be missed in future changes.
Maybe hooks_commit_msg
should call hooks_commit_msg_with_timeout(..., None)
and hooks_commit_msg_with_timeout
's last parameter should be Option<Duration>
? This would also offer a unified interface to any users of git2-hooks.
Same comment is valid for the other functions in this file.
pub fn run_hook_with_timeout( | ||
&self, | ||
args: &[&str], | ||
timeout: Duration, |
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.
Duration::ZERO
signals "no timeout" in band on a public API. Should we make this Option<Duration>
?
Same for all other interfaces this is passed through.
// Sleep Time=(8^2)×1=64, capped by `MAX_SLEEP_MILLIS` of 50 | ||
// Actual Sleep: 50 milliseconds | ||
// Total Sleep: 190 milliseconds |
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.
//
instead of ///
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.
(Also, this is phrased as rustdoc docs, but describes internal parameters and the function isn't even parsed as it's not part of the interface. Maybe consider moving this comment into the function?)
loop { | ||
if is_complete()? { | ||
return Ok(true); | ||
} |
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.
Let's make the purpose and "good path" of this loop explicit:
loop { | |
if is_complete()? { | |
return Ok(true); | |
} | |
while !is_complete()? { |
and end function on Ok(true)
below.
if timer.elapsed() > timeout { | ||
return Ok(false); | ||
} |
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.
Between your two calls to timer.elapsed()
in this function, timeout - timer.elapsed()
may become negative and panic in line 259.
Reduce this to one call and remove any doubt about panicking:
if timer.elapsed() > timeout { | |
return Ok(false); | |
} | |
let remaining_time = match timeout.checked_sub(timer.elapsed()) { | |
Some(t) => t, | |
None => return Ok(false), | |
}; |
// Ensure we do not sleep more than the remaining time | ||
let remaining_time = timeout - timer.elapsed(); | ||
if remaining_time < sleep_time { | ||
sleep_time = remaining_time; | ||
} |
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.
// Ensure we do not sleep more than the remaining time | |
let remaining_time = timeout - timer.elapsed(); | |
if remaining_time < sleep_time { | |
sleep_time = remaining_time; | |
} |
(See comments above)
let file = temp_dir.path().join("test"); | ||
let hook = format!( | ||
"#!/usr/bin/env sh | ||
sleep 1 |
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.
I'd say a test stalling for 1 s due to some I/O is common enough to warrant setting this to a longer time. If everything is alright, this should not affect the test.
sleep 1 | |
sleep 60 |
let timeout = Duration::from_millis(100); | ||
let start = std::time::Instant::now(); | ||
let result = | ||
timeout_with_quadratic_backoff(timeout, || Ok(false)); | ||
let elapsed = start.elapsed(); | ||
|
||
assert_eq!(result.unwrap(), false); | ||
assert!(elapsed < timeout + Duration::from_millis(10)); |
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.
I don't think this is stable at all. If you want to make it stable, you need to offer your own Instant mock or offer a sans-i/o implementation for quadratic back-off. Same for all other tests that tightly rely on timing.
Apologies, I'm in the middle of building & swapping to a new computer I'll get back to this when I'm able to. |
This Pull Request closes #2546.
It changes the following:
I followed the checklist:
make check
without errors