JPERF-811: Fix flaky shouldTolerateEarlyFinish test by waiting until …#23
JPERF-811: Fix flaky shouldTolerateEarlyFinish test by waiting until …#23
Conversation
|
Related discussion: #13 (comment) |
| processCommand: String, | ||
| timeout: Duration | ||
| ) = this.newConnection().use { | ||
| it.execute("wait `pgrep '$processCommand'`", timeout) |
There was a problem hiding this comment.
Probably a bit of an overkill, but I don't have a better idea how we can wait for the process to finish.
It's also a shotgun, because we are waiting not only for the one process we started, but also all of other processes that might be named the same. I think we don't expect there to be any other processes like that anyway.
There was a problem hiding this comment.
I think the chance of other processes with the same name is minimal, and this solution should be sufficient.
| val failResult = fail.stop(Duration.ofMillis(20)) | ||
| repeat(1000) { | ||
| val fail = sshHost.runInBackground("nonexistant-command") | ||
| sshHost.waitForAllProcessesToFinish("nonexistant-command", Duration.ofMillis(100)) |
There was a problem hiding this comment.
Timeout with Duration.ofMillis(50) wasn't enough. I had a run where in 1000 repeats it timed out.
I believe that those kind of timeouts are inevitable when we are working with OS. There could always be some huge lag spike where this and any other processes with timeout would just fail.
Anyway it's better to timeout on a ssh command where an exception explicitly says that it was a timeout then when we fail because we didn't get a response. In the former it's obvious that we can just increase the timeout to reduce the likelyhood of the flakiness.
There was a problem hiding this comment.
I believe that those kind of timeouts are inevitable when we are working with OS.
Yeah, process scheduling can introduce random delays, but the delay distribution is not open-ended. It will never take 1 hour.
Anyway it's better to timeout on a ssh command where an exception explicitly says that it was a timeout then when we fail because we didn't get a response. In the former it's obvious that we can just increase the timeout to reduce the likelyhood of the flakiness.
Exactly ❤️
|
|
||
| val fail = sshHost.runInBackground("nonexistent-command") | ||
| val failResult = fail.stop(Duration.ofMillis(20)) | ||
| repeat(1000) { |
There was a problem hiding this comment.
I intend to remove this repeat block or at least redice the number to e.g. 10. I pushed it only to show a proof that the wait helps (maybe I should push without the fix first 🤔).
There was a problem hiding this comment.
There was a problem hiding this comment.
You can have a PR with repeat open (and red) and then open PRs targeting that red branch. They'd show it's green. And it would allow alternatives to be explored.
…OS actually starts (and finishes) the process before interrupting it
a1ba4f2 to
e79bb92
Compare
|
I removed the |
|
|
||
| val fail = sshHost.runInBackground("nonexistent-command") | ||
| val fail = sshHost.runInBackground("nonexistant-command") | ||
| sshHost.waitForAllProcessesToFinish("nonexistant-command", Duration.ofMillis(100)) |
There was a problem hiding this comment.
Now the test shows that the runInBackground API cannot be used easily. We should fix the problem in the API rather than in tests.
There was a problem hiding this comment.
We could wait for the process to start before returning from runInBackground similarly to how my solution proposal does it in this test, however this approach is very system specific (usage of wait and pgrep). I don't know how we could do that in more portable way.
There was a problem hiding this comment.
Another approach would be to just return some specific SshConnection.SshResult as part of BackgroundProcess.stop when we don't really get the exitStatus, however I don't know what would that be and we need to return something if we want to maintain the current API of BackgroundProcess
There was a problem hiding this comment.
@dagguh I'd like to fix the flakiness of the test. This is my main goal.
My understanding of possible implementations of your suggestion is to either make it less portable or break the API (see my 2 previous comments). I don't like any of those options and I'm a bit stuck with this PR. Do you have any other ideas how this could be fixed? If not then maybe you have opinion about which of those 2 is better?
There was a problem hiding this comment.
@pczuj why are we afraid to break the API and properly apply semantic versioning?
There was a problem hiding this comment.
break the API and properly apply semantic versioning
Yes, we can just break the API, it's a tool in our belt.
It can require some extra effort. Let's say we release ssh:3.0.0. Theinfrastructure is a ssh consumer:
api("com.atlassian.performance.tools:ssh:[2.3.0,3.0.0)")
It will have a choice:
ssh:[2.3.0,3.0.0)- stay on oldssh, this is the default state. It takes no effort, but what was the point ofssh:3.0.0, if no consumer needs it?ssh:[2.3.0,4.0.0)- use the new, without dropping the old. Only possible if the removed API was not used.ssh:[3.0.0,4.0.0)- use the new, drop the old. This means that all consumers ofinfrastructure, which are still onssh:[2.0.0,3.0.0), will no longer be compatible. Therefore that bump is breakage ofinfrastructure(its POM contract), which is a another major release. This bump would cascade to multiple libs:aws-infrastructureandjira-performance-tests, even if they don't usesshdirectly.
We used scenario 3 many times in the past. It's a bit annoying, but not that scary.
We successfully used scenario 2 as well.
There was a problem hiding this comment.
We should fix the problem in the API rather than in tests.
I didn't mean we have to break API. I meant: the tests found a real flakiness pain, let's fix it for the consumers as well.
this approach is very system specific (usage of
waitandpgrep)
I would avoid it too. Not only does it lose generality, but also more moving parts: brittle and complex.
| val fail = sshHost.runInBackground("nonexistent-command") | ||
| val fail = sshHost.runInBackground("nonexistant-command") | ||
| sshHost.waitForAllProcessesToFinish("nonexistant-command", Duration.ofMillis(100)) | ||
| val failResult = fail.stop(Duration.ofMillis(20)) |
There was a problem hiding this comment.
So this fails due to command.exitStatus must not be null.
The exitStatus is supposed to be filled by if (exit-status), but it misses it and falls into if (exit-signal):
We can see that exitSignal = INT, so that's our tryToInterrupt. But why is req coming in with different values? Where exactly is the race condition? 🤔
There was a problem hiding this comment.
The close in readOutput fails if the command was interrupted.
Softening it unveils the real flakiness source:
SSH command failed to finish in extended time (PT0.025S): SshjExecutedCommand(stdout=^Cbash: nonexistent-command-290: command not found
There was a problem hiding this comment.
BTW. It seems arbitrary to have both SshResult and SshjExecutedCommand and two ways of reading stdout
There was a problem hiding this comment.
I traced it down to ancient times behind the wall: https://bulldog.internal.atlassian.com/browse/JPT-292
There was a problem hiding this comment.
Hmm, I already had the fixes, but I was checking the effect of each commit via rebase (to populate the CHANGELOG correctly). And it turns out that failed to finish in extended time (PT0.025S) happens even without the fixes. Sometimes it's a timeout, sometimes it's a null. I must have mixed up the observations around exitStatus=null with the wrong actual symptom. Gotta, really hammer down my understanding of the problem.


…OS actually starts (and finishes) the process before interrupting it