Skip to content

Conversation

beltran
Copy link

@beltran beltran commented Jul 19, 2017

This PR is related to #2707 and is trying to fix the same problem.
Thank you @nirs for the review,

@the-knights-who-say-ni
Copy link

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:
Copy link
Contributor

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.

Copy link
Author

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 = []
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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.

Copy link
Author

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.

@nirs
Copy link
Contributor

nirs commented Jul 19, 2017

@bjmb, I think we can use the same issue number, having several solutions for a bug is a good thing.

@beltran beltran changed the title Asyncore alternative fix bpo-30931: Asyncore alternative fix Jul 19, 2017
Lib/asyncore.py Outdated
self._fileno = self.socket.fileno()
self.add_channel()

def fileno(self):
Copy link
Contributor

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()
Copy link
Contributor

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.

Copy link
Author

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?

Copy link
Contributor

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.

@beltran
Copy link
Author

beltran commented Jul 19, 2017

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 fileno(), the downside is that is probably less efficient

@nirs
Copy link
Contributor

nirs commented Jul 20, 2017

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))
Copy link
Contributor

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

Copy link
Contributor

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?

Copy link
Author

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

Copy link
Contributor

@nirs nirs left a 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 :-)

@beltran
Copy link
Author

beltran commented Jul 20, 2017

👍 thank you for the reviews and your patience!

@nirs
Copy link
Contributor

nirs commented Jul 20, 2017

@bjmb, you need to sign the CLA, look at the comments from @the-knights-who-say-ni.

@beltran
Copy link
Author

beltran commented Jul 20, 2017

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)
Copy link
Member

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.

Copy link
Author

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

Copy link
Member

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:
Copy link
Member

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.

Copy link
Author

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.

Copy link
Member

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.

Copy link
Author

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
Copy link
Member

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?

Copy link
Author

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.

Copy link
Member

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.

Copy link
Author

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.

Copy link
Member

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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this method.

Copy link
Author

@beltran beltran left a 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:
Copy link
Author

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
Copy link
Author

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)
Copy link
Author

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)
Copy link
Member

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:
Copy link
Member

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
Copy link
Member

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.

@nirs
Copy link
Contributor

nirs commented Jul 21, 2017

@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.

@terryjreedy
Copy link
Member

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.

@vstinner
Copy link
Member

I wrote an alternative to this alternative fix: PR #2854.

@vstinner
Copy link
Member

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.

@vstinner vstinner closed this Jul 26, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants