Skip to content

Conversation

beltran
Copy link

@beltran beltran commented Jul 21, 2017

Could you take a look at this @nirs ?

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.

Looks good, but need to check why the windows CI is failing.

@@ -651,6 +651,7 @@ def writable(self):
server = BaseServer(self.family, self.addr, TestHandler)
client = TestClient(self.family, server.address)
self.loop_waiting_for_flag(client)
self.assertTrue(client.closing)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think a separate simpler test for closing once and twiice is better, similar to the file_wrapper tests. Here we are mixing 2 issues in the same test. The purpose of this test is handling of ECONNRESET/EPIPE, while the closing test is about 1. setting closing on close, and 2. handling double close gracefully.

@@ -388,6 +388,10 @@ def recv(self, buffer_size):
raise

def close(self):
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.

Looks good

Copy link
Member

Choose a reason for hiding this comment

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

IMHO this change should be documented in asyncore documentation using a ".. versionchanged:: 3.7" markup.

@beltran beltran force-pushed the closing_asyncore branch from 5610268 to 673904c Compare July 21, 2017 19:52
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.

Looks good to me, @Haypo, do you want to take a look?

Copy link
Contributor

@giampaolo giampaolo left a comment

Choose a reason for hiding this comment

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

LGTM

@giampaolo giampaolo self-assigned this Jul 25, 2017
def test_close(self):
d = asyncore.dispatcher()
d.close()
self.assertTrue(d.closing)
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a sec: I would check d.closing is False before close().

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.

This will break user code adding closing-once to their dispatcher subclass using dispatcher.closing:

def close(self):
    if self.closing:
        return
    self.closing = True
    asyncore.dispatcher.close(self)

I expect this to be very common. Using the undocumented closing attribute is wrong, but we cannot do this.

I suggest to use the private _fileno to detect closing, see file_wrapper.close().

@giampaolo
Copy link
Contributor

This will break user code adding closing-once to their dispatcher subclass using dispatcher.closing:

Uh? I don't understand how it would break code.

Also, I suggest to call this "__closed" instead, and add a comment telling all new attributes added in the future should have a "__" prefix, see discussion at:
http://bugs.python.org/issue30985

@nirs
Copy link
Contributor

nirs commented Jul 26, 2017

@giampaolo, see the code example in my comment - if the subclass set closing to True befor calling the superclass close(), superclass will not nothing :-)

@giampaolo
Copy link
Contributor

Ah sorry, I get what you mean now. Yes, there is no way to be 100% sure names won't clash, but with __closed the chances should be way lower.

@nirs
Copy link
Contributor

nirs commented Jul 26, 2017

@giampaolo, @Haypo, with #2854, I don't think we need to detect if a dispatcher was closed.

The dispatcher is already protected in python 3.7 in multiple ways:

  • del_channel is using self._fileno
  • close() catch EBADF from self.socket.close()
  • socket.close() does not raise EBADF when calling multiple times (tested only on Linux)

So we don't need any change :-)

If we want to have cleaner close-once semantics, we can add 2 lines:

    def close(self):
        if self._fileno is None:
            return

@giampaolo
Copy link
Contributor

+1 for relying on _fileno

@vstinner
Copy link
Member

IMHO https://bugs.python.org/issue30931 is now well defined: the use case is described, the constraints have been listed, different implementations have been discussed, etc.

I'm sorry, but I don't get the use case for http://bugs.python.org/issue30985 since internally, asyncore doesn't use closing!? It was said multiple times that there is a high risk of regression. Ok. What about the benefits?

The only mentionned use case is to fix a race condition, but as I wrote at http://bugs.python.org/issue30985#msg299165 , the closing attribute is not needed to fix the race condition.

Note: We usually prefer to discuss issues at bugs.python.org.

@giampaolo
Copy link
Contributor

By relying on _fileno there's no risk of breaking compatibility. The check should go in regardless of https://bugs.python.org/issue30931 because a double close() can still occur on read() followed write().

@nirs
Copy link
Contributor

nirs commented Jul 26, 2017

With #2854, I don't think we can have write() after read() if read() has closed the dispatcher.

We can still have this readwrite():

def readwrite(obj, flags):  
    try:
        if flags & select.POLLIN:
            obj.handle_read_event()
        if flags & select.POLLOUT:
            obj.handle_write_event()
        if flags & select.POLLPRI:
            obj.handle_expt_event()
        if flags & (select.POLLHUP | select.POLLERR | select.POLLNVAL):
            obj.handle_close()
    ...

@vstinner
Copy link
Member

@giampaolo: "By relying on _fileno there's no risk of breaking compatibility."

I'm sorry, but I'm lost. There are many PR related to asyncore and many things were discussed. Why do you want to use _fileno? What is your use case? Are you trying to fix a bug?

Can we please wait until https://bugs.python.org/issue30931 is fixed? It would clarify the discussion.

@vstinner
Copy link
Member

The check should go in regardless of https://bugs.python.org/issue30931 because a double close() can still occur on read() followed write().

Hum. I guess that you describe a bug when poll2() is used, a FD is both ready to read and ready to write, the read handler closes the FD, and then the write handler tries to use the closed FD?

Why using using the same pattern used in poll() of the PR 2854, check "map.get(fd) is not obj" before calling each handler? For example, inline readwrite() in poll2() and duplicate the check?

@nirs
Copy link
Contributor

nirs commented Jul 26, 2017

Or we can add a check in handle_xxx_event for closed channel (self._fileno is None), and do nothing in this case, or raise, and handle the error in readwrite.

@giampaolo
Copy link
Contributor

@giampaolo: "By relying on _fileno there's no risk of breaking compatibility."
I'm sorry, but I'm lost. There are many PR related to asyncore and many things were discussed. Why do > you want to use _fileno? What is your use case? Are you trying to fix a bug?

Yes, I want to prevent close to be called twice during a single IO loop.
That can happen if a fd is both readable and writable, it gets closed on read(), then it gets closed again on write().

@vstinner
Copy link
Member

I added another fix to the poll2() race condition described by @giampaolo in my PR 2854, see e701582

@giampaolo
Copy link
Contributor

giampaolo commented Jul 26, 2017

Or we can add a check in handle_xxx_event for closed channel (self._fileno is None), and do nothing in this case, or raise, and handle the error in readwrite.

I would be scared to do that. Any change to asyncore, even minimal, should be very cautious. It's API is so broken that basically all subclasses out there rely on the long established logic of its internals, including logic which is clearly broken, like calling handle_*_event methods even when a dispatcher has already been closed.

@vstinner
Copy link
Member

"Yes, I want to prevent close to be called twice during a single IO loop. That can happen if a fd is both readable and writable, it gets closed on read(), then it gets closed again on write()."

Ok, you are right that my PR didn't prevent this bug, but only fixed two other race conditions. But I added a third fix in my PR ;-)

What matters for me is not really "prevent close to be called twice" but more generally the semantics. If I understood correctly, the constraint is:

asyncore must not call a handler if its dispatcher was closed

I believe that my PR now correctly implements this constraint in all cases. I wrote 3 different unit tests to test the 3 described bugs.

@vstinner
Copy link
Member

@nirs: "Or we can add a check in handle_xxx_event for closed channel (self._fileno is None), and do nothing in this case, or raise, and handle the error in readwrite."

I'm not sure that I understand your suggestion. handlexx_event() are written in third party code. Do you suggest to modify all projects on PyPI and private projects?

IMHO it's simpler to do the check just before calling an handler, no?

@giampaolo
Copy link
Contributor

For now I would start focusing on this PR and use _fileno instead of closing, merge it, backport it and then start looking at the other PRs.

@vstinner
Copy link
Member

@giampaolo: "I would be scared to do that. Any change to asyncore, even minimal, should be very cautious. It's API is so broken that basically all subclasses out there relies on the long established logic of its internals, including logic which is clearly broken, like calling handle_*_event methods even when a dispatcher has already been closed."

Same for me here, I'm very scared :-) But my PR #2854 clearly changed what you call a "clearly broken logic". Are you saying that my change is backward incompatible?

Honestly, if someone relies on the broken behaviour, I would suggest to copy the short asyncore.py file of an older Python version to really make sure that the behaviour never changes.

We cannot fix the 3 discussed race conditions without changing the exact semantics.

I would add that another issue is that the semantics is not currently well defined. For example, there was no test to check if read is called before write. My PollTests test case adds such "semantics" tests.

@nirs
Copy link
Contributor

nirs commented Jul 26, 2017

@Haypo it is harder to check before calling the handler, because of the way handlers call other handlers internally, see handle_read_event().

Regarding fixing hadnle_xxx_event() - they are undocumented, private implemetation defail of asyncore. Users of this class should implement only handle_xxx() method, specified in the docs. I don't know if people reimplemented them.

I think #2854 first commit is fine and can be merged without the new commit, and if we want to protect from double close we can do 2 line change in close().

Calling asyncore handle_xxx() after it was closed is an old issue. Here is one bug I reported 8 years ago:
http://bugs.python.org/issue6550

@vstinner
Copy link
Member

Nir:

Calling asyncore handle_xxx() after it was closed is an old issue. Here is one bug I reported 8 years ago: http://bugs.python.org/issue6550

Oh. I closed this one. I still agree with what I wrote in 2014 :-)

asyncore-fix-refused-4.patch catchs EBADF. To me, it looks like a bug: if you get a EBADF error, we already made something wrong before.

@giampaolo
Copy link
Contributor

Regarding fixing hadnle_xxx_event() - they are undocumented, private implemetation defail of asyncore. I don't know if people reimplemented them.

Unfortunately I know at least 2 code bases relying on such methods, one is pyftpdlib, another one is at my previous company (smartfile). This is exactly what I was talking about. In asyncore/chat everything is "public", even the non-documented stuff.

@vstinner
Copy link
Member

vstinner commented Jul 26, 2017 via email

@taleinat
Copy link
Contributor

taleinat commented Sep 6, 2018

@vstinner, @giampaolo, should this be closed?

@vstinner
Copy link
Member

vstinner commented Sep 6, 2018

Sadly, I agree to leave asyncore unchanged and so reject this PR. It's deprecated since Python 3.6. Just upgrade to asyncio, threads or something else. Backward compatibility matters more here.

@vstinner vstinner closed this Sep 6, 2018
@giampaolo
Copy link
Contributor

I agree with @vstinner. This should be rejected because basically any change to asyncore poses a backward compatibility risk due to its poor API.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants