-
-
Notifications
You must be signed in to change notification settings - Fork 32.9k
bpo-30931: Asyncore alternative fix #2764
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
Conversation
Hello, and thanks for your contribution! I'm a bot set up to make sure that the project can legally accept your contribution by verifying you have signed the PSF contributor agreement (CLA). Unfortunately our records indicate you have not signed the CLA. For legal reasons we need you to sign this before we can look at your contribution. Please follow the steps outlined in the CPython devguide to rectify this issue. Thanks again to your contribution and we look forward to looking at it! |
Lib/asyncore.py
Outdated
|
||
for obj in r: | ||
if obj.fileno() is None: | ||
if obj.fileno() is -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.
This may fail, there may be multiple -1 instances, unlike the single None instance. Better use == when comparing a number. But see my comment bellow about using obj.closing instead.
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.
Yes... that's right
Lib/asyncore.py
Outdated
if flags: | ||
pollster.register(fd, flags) | ||
|
||
ready = [] |
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.
We use ready only after the poll(), so better introduce it there.
Lib/asyncore.py
Outdated
self.del_channel() | ||
if self.socket is not None: | ||
try: | ||
self._fileno = -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.
_fileno is initialized to None in __init__
, and set to None in del_channel
, so we should change it there.
Lib/asyncore.py
Outdated
|
||
def fileno(self): | ||
return self.socket.fileno() | ||
return self._fileno |
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 wonder if there is code out there, assuming that file_dispatcher.fileno() raises socket.error(EBADF) when the socket is closed, and this changes the behavior in incompatible way.
So maybe it is better use self.socket.fileno() as you used before, and use another check to find if a dispatcher is closed. For example, we have the unused "closing" flag, that asyncore.dispatcher define, but never use - we can do:
--- a/Lib/asyncore.py
+++ b/Lib/asyncore.py
@@ -391,6 +391,9 @@ class dispatcher:
return self._fileno
def close(self):
+ if self.closing:
+ return
+ self.closing = True
self.connected = False
self.accepting = False
self.connecting = False
And then use:
if obj.closing:
instead of:
if obj.fileno() == -1:
It may also be more efficient.
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.
Yes that makes sense, there are certainly some subtleties that I'm missing.
@bjmb, I think we can use the same issue number, having several solutions for a bug is a good thing. |
Lib/asyncore.py
Outdated
self._fileno = self.socket.fileno() | ||
self.add_channel() | ||
|
||
def fileno(self): |
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 did not notice before that you added this - since we inherit from dispatcher, we don't need to implement it.
raise | ||
|
||
def fileno(self): | ||
return self.socket.fileno() |
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 compatible with 2.7 and older 3.x, when this class had getattr, and fileno() was delegated to the self.socket.
But I found that we have incompatible implementation in file_wrapper:
def close(self):
if self.fd < 0:
return
os.close(self.fd)
self.fd = -1
def fileno(self):
return self.fd
I think we should have consistent implementation of fileno(), I would check how it behave in regular file objects and make both implementations the same. Both should return -1 when you close the fd, or raise EBADF. For backward compatibility raising EBADF should be less risky.
Finally, using self.socket means it will raise AttributeError if the socket is None - this was fixed lately in python 3 in close(), by checking if the socket is None. If we go with socket.fileno(), we need to check for None and raise EBADF.
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.
Looks like that in close self.fd
is only set to -1
in python3, in python2 just os.close(self.fd)
is called. The behaviour for regular file object when fileno()
is called on them is to raise
>>> f.close()
>>> f.fileno()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file
would raising this be a possible solution?
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.
dispatcher and file_wrapper try to behave like a socket, so using using socket.fileno() seems like the best approach, and it is also compatible with older code that was calling dispatcher.fileno() and dispatches.socket.fileno().
I think we can leave file_wrapper as is for now, since the patch fixes the issue without this change.
So the only issue in this version of the patch is calling fileno() when dispatcher.socket is None. Not sure that this is a real issue, it means that the instance is not created or configured properly.
Regarding setting self.fd to -1 - this is a bug in 2.7 that should be fixed.
Another idea is to wrap the socket with an int, and use in select like class int_with_socket(int):
def __new__(cls, socket):
obj = int.__new__(cls, socket._fileno)
obj.socket = socket
return obj then we would append that object to the list r.append(int_with_socket(obj)) and we would call the socket in the int: for int_wrapper in r:
read(int_wrapper.socket) we wouldn't have to worry about compatibility issues with |
Wrapping a dispatcher with an int is interesting, but I think it fits David Beazley talk more than production code. We have very simple and elegant fileno() interface, and using it would be best choice. |
Lib/asyncore.py
Outdated
if obj is None: | ||
continue | ||
readwrite(obj, flags) | ||
ready.append((map[fd], flags)) |
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 loop can be simplified to:
ready = [(map[fd], flags) for fd, flags in r]
if self.closing: | ||
return | ||
self.closing = 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.
You separated this to make closing state change more clear, right?
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.
Yes, visually is what I would do for my code
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.
Look good to me, we need to find someone who can merge this :-)
👍 thank you for the reviews and your patience! |
@bjmb, you need to sign the CLA, look at the comments from @the-knights-who-say-ni. |
Yes, I did it on Friday, haven't hear back since, I guess it may takes some days to get processed |
time.sleep(timeout) | ||
return | ||
|
||
r, w, e = select.select(r, w, e, 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.
asyncore is close to its death. I'm not able to accept such large changes. I would prefer changes as small as possible, so please use the same method than for poll():
ready = [(map[fd], flags) for fd, flags in r]
The thing is that asyncore code base is old and error-prone, the test suite is very small. I cannot affort the risk of introducing a regression.
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 can be done like this but I think the closing
variable should remain
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.
Just write a second PR when the first one is merged. I would just be easier to review it. IMHO you are fixing two bugs at once, it makes the review harder.
ready = [(map[fd], flags) for fd, flags in r] | ||
|
||
for obj, flags in ready: | ||
if not obj.closing: |
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.
Would you mind to first address the first bug in a first PR, map modified in this loop, and then work on a new PR (once the first PR is merged) to add closing?
Again, I prefer very small changes.
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.
adding closing is part of fixing the bug, the point of it is not to called readwrite on a socket that has been closed by another socket on a previous readwrite.
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 "ready = [...]", I don't see how you could have such race condition. Again, let's move slowly, step by step.
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.
But if you store all the objects on ready you can't call just call all of them because an object previous in the list might have closed a posterior one.
raise unittest.SkipTest("The test is meaningful only if the fd for the old and " | ||
"the new dispatcher are the same") | ||
|
||
self.flag = 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.
Sorry, I don't understand why we need so much complicated code to test an altered map. Can't we just have two readable objects and the first one clears the map, so the second loop iteartion is supposed to fail? Just make sure that we called the read handler of the two objects even if the map was cleared?
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 I understand this, if the first readable object closes the second and removes it from the map then it will hit the comparison
if obj is None:
and the second readable object won't be called as it shouldn't.
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 "ready = [...]", you can remove "if obj is None:" from the loop. Problem solved.
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 before, some check must be done you have to check if the object has been closed by a previous one in while readwrite was called.
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.
Sorry, we are talking about two different bugs. I'm talking about a first class of bugs. I didn't say that a fix PR using "ready=[...]" would fix all bugs. Just that it would be easier to review, easy to merge, backport, etc. And then it would become easier to discuss solutions for more complex bugs.
def fileno(self): | ||
if self.socket is None: | ||
raise socket.error(EBADF, 'Bad file descriptor') | ||
return self.socket.fileno() |
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.
Please remove this method.
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 the review @Haypo
ready = [(map[fd], flags) for fd, flags in r] | ||
|
||
for obj, flags in ready: | ||
if not obj.closing: |
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.
adding closing is part of fixing the bug, the point of it is not to called readwrite on a socket that has been closed by another socket on a previous readwrite.
raise unittest.SkipTest("The test is meaningful only if the fd for the old and " | ||
"the new dispatcher are the same") | ||
|
||
self.flag = 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.
I don't think I understand this, if the first readable object closes the second and removes it from the map then it will hit the comparison
if obj is None:
and the second readable object won't be called as it shouldn't.
time.sleep(timeout) | ||
return | ||
|
||
r, w, e = select.select(r, w, e, 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.
It can be done like this but I think the closing
variable should remain
time.sleep(timeout) | ||
return | ||
|
||
r, w, e = select.select(r, w, e, 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.
Just write a second PR when the first one is merged. I would just be easier to review it. IMHO you are fixing two bugs at once, it makes the review harder.
ready = [(map[fd], flags) for fd, flags in r] | ||
|
||
for obj, flags in ready: | ||
if not obj.closing: |
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 "ready = [...]", I don't see how you could have such race condition. Again, let's move slowly, step by step.
raise unittest.SkipTest("The test is meaningful only if the fd for the old and " | ||
"the new dispatcher are the same") | ||
|
||
self.flag = 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.
Using "ready = [...]", you can remove "if obj is None:" from the loop. Problem solved.
@bjmb, I think we should first fix the closing issue - this is a very small and safe path that will be easy to get merged, and also to backport to older versions. We also need a test for this. |
nirs and haypo, the way to fix [CLA not signed], once it has been, it to delete the tag. The knight will then recheck and comfirm. |
I wrote an alternative to this alternative fix: PR #2854. |
IMHO adding a closing attribute has a too high risk of regression: see http://bugs.python.org/issue30985 for the discussion. I prefer to only rely on the map, your PR 2707 or my PR 2854. |
This PR is related to #2707 and is trying to fix the same problem.
Thank you @nirs for the review,