-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Remove dead workers immediately after killing. #1084
Conversation
96c2f30
to
dc3f522
Compare
if not worker: | ||
continue | ||
worker.tmp.close() | ||
self.reap_dead_worker(wpid) |
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.
Note that this did not call self.cfg.worker_exit(self, worker)
before, but will now. I believe that that was an oversight and this is a correct change, but could use a double-check if it was intentional.
dc3f522
to
de31a19
Compare
extra={"metric": "gunicorn.workers", | ||
"value": len(workers), | ||
"value": len(self.WORKERS), |
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.
Before, workers
and self.WORKERS
could be out of sync here if workers were killed, because they would be reaped asynchronously. Because both their insertion (via spawn_worker
) and removal (via kill_worker
) are now synchronous, self.WORKERS
is the correct value here.
I think |
Oh, I see. But if we do that, then we can't pop synchronously, because by the time we get the |
I'll wait for others to comment and take a look again tomorrow. |
Thanks for doing this. |
No problem @tilgovi. If I read your comments correctly, you arrived at same conclusion as I did here in terms of cleaning up immediately versus needing to keep the |
As I look more into this, I'm starting to think that the original suggestion of keeping a 'killed worker' list is correct. Otherwise killing and waiting for |
This also organizes the shared "worker cleanup" operations like closing `worker.tmp` and calling `self.cfg.worker_exit`. This means that `self.WORKERS` will always be updated after spawning/killing, and may only be out of sync due to unknown dead processes (handled in `reap_workers`).
de31a19
to
15c43f0
Compare
worker.tmp.close() | ||
self.cfg.worker_exit(self, worker) | ||
except: | ||
pass |
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.
Removed because this portion of spawn_worker
is executed on the worker itself, and we let the parent handle its exit in reap_workers
.
Ok, I believe this is correct now. It removes them from |
This is ready for review, if anyone's available to take a look. My main interest in this PR is to unblock #1078 |
continue | ||
worker.tmp.close() | ||
|
||
worker = (self.WORKERS.pop(wpid, None) |
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 would add a comment here to say to explain that this function might happen before the SIGCHLD handler for this pid.
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 assume that's why this is like this.
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.
No, reap_workers
is only called in response to SIGCHILD
as before.
self.DYING_WORKERS
exists so that kill_worker
can remove pids from self.WORKERS
while we're waiting for SIGCHILD
to come in and be processed.
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.
That means that SIGCHILD
can be caused either by the child exiting unexpectedly, or because we killed it intentionally. In the first case, that worker would have still been in self.WORKERS
. In the second, we would have moved it to self.DYING_WORKERS
.
Two small comments but otherwise I think it looks fine. |
I'd like @berkerpeksag or @benoitc to read this over, but I feel pretty good about it. |
Awesome. I made one additional commit to make sure that dying workers are included in |
@@ -424,7 +425,7 @@ def murder_workers(self): | |||
""" | |||
if not self.timeout: | |||
return | |||
workers = list(self.WORKERS.items()) | |||
workers = list(self.WORKERS.items() + self.DYING_WORKERS.items()) |
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.
Unfortunately, this will raise TypeError
in Python 3. I'd write this like
workers = list(self.WORKERS.items()) + list(self.DYING_WORKERS.items())
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.
Fixed, thank you!
Looks good to me (except my comment about Python 3). Could you also squash the commits into one? Thanks! (Should we get this in for the next release or wait for 20.0?) |
try: | ||
worker = self.WORKERS.pop(pid) | ||
worker.tmp.close() | ||
self.cfg.worker_exit(self, worker) |
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 hooks disappeared in the change. Should probably come back
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.
It's in reap_workers
on line 469.
I will take the time to read that patch over the week-end. To be honnest I am quite uncomfortable to maintain 2 lists there and I am not sure that all races conditions are solved right now. Like what happen if I try to kill a worker that died but we didn't get yet the signal? We should probably start to add some tests there. (but that outside the scope of this PR).
|
I reviewed and we discussed the two places for races. It is the cause of the two places where we pop from both lists. Is there a reason you are uncomfortable with two lists? In my opinion, it makes reasoning about the concurrency easier. |
@tilgovi I am worried about race condition when you remove a socket from Anyway I like the idea of marking a socket as dead ASAP and remove it from the count. I wonder if we can then just keep a reference of them in the |
I hear you. Signals are tricky places for concurrency problems. I'll double check over this and see if there's anything I'd change. On Tue, Jul 21, 2015, 21:50 Benoit Chesneau notifications@github.com
|
@benoitc @tilgovi Are you referring to Sounds like you might be referring to this? worker = self.WORKERS.pop(pid, None)
# <-- If `reap_workers` were run here, we would not clean up the worker
# until it later timed out and got collected in `murder_workers()`.
# That would be annoying (though probably not problematic).
#
# However, that can't happen: the signal handling just adds to SIG_QUEUE,
# which is processed in the main run loop. `reap_workers` will never
# ever be run behind our backs.
if worker is not None:
self.DYING_WORKERS.setdefault(pid, worker) |
Nope, I'm wrong about that and not sure how I overlooked it: So that is a potential problem. Signal handlers are supposed to do very little, so I'm not sure what the rationale is for calling My guess is that it was done that way to keep Does that make sense? |
It seems that the original code calling |
Yes let's queue the call and then only modify the lists outside the signal On Wed, Jul 22, 2015, 09:20 Robert Estelle notifications@github.com wrote:
|
4e349e4
to
22662c9
Compare
The new `DYING_WORKERS` list means that we don't need to run `reap_workers` immediately on SIG_CHLD; and in fact, we shouldn't. The original code calling `reap_workers()` directly in a signal handler at any time was likely unsafe to begin with: `reap_workers` calls `os.waitpid()`, calls `self.cfg.worker_exit()`, and modifies `self.WORKERS`. All of those are potentially unsafe in a signal handler. And not sure what raising It also can raise `HaltServer`, which wouldn't be caught and turned into `self.halt` in the main loop as intended.
22662c9
to
180fa0e
Compare
After going through git blame, there was no rationale recorded for the very low limit: it was present in the first commit introducing the signal queue. Since SIG_CHLD gets queued now, it would be entirely possible for the previous limit of 5 queued signals to be exceeded during `manage_workers`.
110654b
to
989a423
Compare
No please don't. The reason we are handling SIGCHLD differently is because of portability. Also it is handled outside of the queue, because really you want it to handle it ASAP not somewhere in the signal queue. The signal queue is here mostly to reduce the signal noise. So you don't want it too large since we put the priority on the monitoring. I would like to keep that behaviour for now. @erydo yes I was speaking about There are really good points anyway in this PR that we should really keep. In Thoughts? |
|
@erydo there are different concerns thre. And I would like to separate them.
About 1 and 2 I reread the code and looked at your changes and I think we could mix our concerns by implementing a priority queue. All signals will be queued there and we would assign a priority to them. Then this queue will be handled in the main loop. As for the waitpid I would like to keep it that way. This is a common pattern and we can discuss some changes about it later. Would that work for you? About 3, I would also note that I am working on a patch implementing the IMSG framework from the OpenBSD project to improve the communication with the workers and removing the temporary file trick. I will have a working patch over the weekend and this is also why I want to keep the changes simple for now while I agree we are doing too much works to handle race conditions. |
closing the issue. I'm uncomfortable with that change as noted above. thanks for the patch anyway. |
This also organizes the shared "worker cleanup" operations like closing
worker.tmp
and callingself.cfg.worker_exit
.This means that
self.WORKERS
will always be updated after spawning/killing, and may only be out of sync due to unknown dead processes (handled inreap_workers
).This was done based on conversation in #1078. The original suggestion was to maintain a dead worker list
but I believe that this is a little simpler and accomplishes the same thing.and I eventually came to the same conclusion.