Flagging blocking functions not to be used with asyncio

Hi list, I am currently developing a Python library based on asyncio. Unfortunately, not all users of my library have much experience with asynchronous programming, so they often try to use blocking functions. I thought it would be a good idea if we could somehow flag blocking functions in the standard library, such that they issue a warning (or even raise an exception) if they are used in an asyncio context. For functions implemented in Python, a simple decorator should do the job. For functions implemented in C, things get a bit more complex. Thinking about it, I realized that currently the best indicator for a C function to block is that it releases the GIL. There are some false positives, like a read with O_NONBLOCK set, in which case we need a way to opt out, but in general it could be a good idea that releasing the GIL triggers a warning in an asyncio environment. Greetings Martin

On Fri, Oct 7, 2016 at 1:07 AM, Martin Teichmann <lkb.teichmann@gmail.com> wrote:
That implementation idea seems iffy -- it feels like it would be a lot of work to pull it off. Releasing the GIL is done at an extremely low level and it's not clear how you'd even raise an exception at that point. Maybe a simpler approach would be to write a linter that checks for a known list of common blocking functions, and anything that calls those automatically gets the same property? -- --Guido van Rossum (python.org/~guido)

On 2016-10-07 11:16 AM, Guido van Rossum wrote:
What if somebody uses logging module and logs to a file? I think this is something that linters can't infer (how logging is configured). One way to solve this would be to monkeypatch the io and os modules (gevent does that, so it's possible) to issue a warning when it's used in an asyncio context. This can be done as a module on PyPI. Another way would be to add some kind of IO tracing hooks to CPython. Yury

On Fri, Oct 7, 2016 at 6:52 PM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
How about something like this? http://www.tornadoweb.org/en/stable/ioloop.html#tornado.ioloop.IOLoop.set_bl... -- Giampaolo - http://grodola.blogspot.com

On 2016-10-07 1:31 PM, Giampaolo Rodola' wrote:
Yes, we already have a similar mechanism in asyncio -- loop.slow_callback_duration property that is used in debug mode. The thing it isn't really precise, as you can have a lot of relatively fast blocking calls that harm performance, but complete faster than slow_callback_duration. Yury

On Fri, Oct 7, 2016 at 9:52 AM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
And depending on your use case this may be acceptable.
Honestly before writing a lot of code here I'd like to hear more from Martin about the spread of mistakes he's observed among his users. -- --Guido van Rossum (python.org/~guido)

Hi,
Honestly before writing a lot of code here I'd like to hear more from Martin about the spread of mistakes he's observed among his users.
Over the weekend, I tried to classify the mistakes I found. Most of the times, it's something like "I'm just doing a quick lookup on the database, that shouldn't be a problem". For people coming from a threading background, this are indeed fast operations, they don't consider such calls as blocking. In the end, it all boils down to some read operation down in some non-asyncio code. This is why I got my idea to flag such calls. Unfortunately, I realized that it is nearly impossible to tell whether a read call is blocking or not. We would need to know whether the file descriptor we read from was created as non-blocking, or whether it was an actual file, and how fast the file storage is for this file (SSD: maybe fine, Network: to slow, magnetic disk: dunno). All of this is unfortunately not a Python issue, but an issue for the underlying operating system. So I guess I have to tell my users to program carefully and think about what they're reading from. No automatic detection of problems seems to be possible, at least not easily. Greetings Martin

On Mon, Oct 10, 2016 at 8:59 PM, Martin Teichmann <lkb.teichmann@gmail.com> wrote:
Probably not worth trying to categorize those reads by source. However, one important feature would be: coming from cache, or actually waiting for content? With pipes and sockets, this is a very significant difference, and if you've done a peek() or select() to find that there is content there, a read() should be perfectly legal, even in an asyncio world. ChrisA

On Mon, Oct 10, 2016 at 2:59 AM, Martin Teichmann <lkb.teichmann@gmail.com> wrote:
Yeah, it really doesn't help that a synchronous network query to a remote SSD-backed database can easily be lower latency than a synchronous local disk read to spinning media, yet the fact that we have async network APIs but no standard async disk APIs means that we would inevitably find ourselves warning about the former case while letting the latter one pass silently... -n -- Nathaniel J. Smith -- https://vorpus.org

On Fri, Oct 7, 2016 at 1:07 AM, Martin Teichmann <lkb.teichmann@gmail.com> wrote:
That implementation idea seems iffy -- it feels like it would be a lot of work to pull it off. Releasing the GIL is done at an extremely low level and it's not clear how you'd even raise an exception at that point. Maybe a simpler approach would be to write a linter that checks for a known list of common blocking functions, and anything that calls those automatically gets the same property? -- --Guido van Rossum (python.org/~guido)

On 2016-10-07 11:16 AM, Guido van Rossum wrote:
What if somebody uses logging module and logs to a file? I think this is something that linters can't infer (how logging is configured). One way to solve this would be to monkeypatch the io and os modules (gevent does that, so it's possible) to issue a warning when it's used in an asyncio context. This can be done as a module on PyPI. Another way would be to add some kind of IO tracing hooks to CPython. Yury

On Fri, Oct 7, 2016 at 6:52 PM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
How about something like this? http://www.tornadoweb.org/en/stable/ioloop.html#tornado.ioloop.IOLoop.set_bl... -- Giampaolo - http://grodola.blogspot.com

On 2016-10-07 1:31 PM, Giampaolo Rodola' wrote:
Yes, we already have a similar mechanism in asyncio -- loop.slow_callback_duration property that is used in debug mode. The thing it isn't really precise, as you can have a lot of relatively fast blocking calls that harm performance, but complete faster than slow_callback_duration. Yury

On Fri, Oct 7, 2016 at 9:52 AM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
And depending on your use case this may be acceptable.
Honestly before writing a lot of code here I'd like to hear more from Martin about the spread of mistakes he's observed among his users. -- --Guido van Rossum (python.org/~guido)

Hi,
Honestly before writing a lot of code here I'd like to hear more from Martin about the spread of mistakes he's observed among his users.
Over the weekend, I tried to classify the mistakes I found. Most of the times, it's something like "I'm just doing a quick lookup on the database, that shouldn't be a problem". For people coming from a threading background, this are indeed fast operations, they don't consider such calls as blocking. In the end, it all boils down to some read operation down in some non-asyncio code. This is why I got my idea to flag such calls. Unfortunately, I realized that it is nearly impossible to tell whether a read call is blocking or not. We would need to know whether the file descriptor we read from was created as non-blocking, or whether it was an actual file, and how fast the file storage is for this file (SSD: maybe fine, Network: to slow, magnetic disk: dunno). All of this is unfortunately not a Python issue, but an issue for the underlying operating system. So I guess I have to tell my users to program carefully and think about what they're reading from. No automatic detection of problems seems to be possible, at least not easily. Greetings Martin

On Mon, Oct 10, 2016 at 8:59 PM, Martin Teichmann <lkb.teichmann@gmail.com> wrote:
Probably not worth trying to categorize those reads by source. However, one important feature would be: coming from cache, or actually waiting for content? With pipes and sockets, this is a very significant difference, and if you've done a peek() or select() to find that there is content there, a read() should be perfectly legal, even in an asyncio world. ChrisA

On Mon, Oct 10, 2016 at 2:59 AM, Martin Teichmann <lkb.teichmann@gmail.com> wrote:
Yeah, it really doesn't help that a synchronous network query to a remote SSD-backed database can easily be lower latency than a synchronous local disk read to spinning media, yet the fact that we have async network APIs but no standard async disk APIs means that we would inevitably find ourselves warning about the former case while letting the latter one pass silently... -n -- Nathaniel J. Smith -- https://vorpus.org
participants (7)
-
Chris Angelico
-
Giampaolo Rodola'
-
Guido van Rossum
-
Martin Teichmann
-
Nathaniel Smith
-
Victor Stinner
-
Yury Selivanov