Skip to content
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

Callback is never called when set_result() is called on a Future #3

Open
talljosh opened this issue Apr 11, 2020 · 3 comments · May be fixed by #9
Open

Callback is never called when set_result() is called on a Future #3

talljosh opened this issue Apr 11, 2020 · 3 comments · May be fixed by #9

Comments

@talljosh
Copy link

When code triggered by a Gtk signal calls set_result() on an asyncio Future object, it seems the callbacks on the Future object are not called unless something else wakes up the main loop.

I've put together the following minimal example of this bug.

Expected behaviour: clicking the button should print, 'button clicked' and 'future finished', then the main loop should end.
Actual behaviour: clicking the button prints 'button clicked', then the main loop continues forever.
Tested with: Python 3.6.9, Gtk 3.22.30 and asyncio-glib 0.1.

If you uncomment the heartbeat line, so that there's a regular timed call happening, then the Future's callback is called and the main loop ends. But with that line commented, the loop does not end.

import asyncio
import asyncio_glib
from gi.repository import Gtk

asyncio.set_event_loop_policy(asyncio_glib.GLibEventLoopPolicy())


class Demo:
    def __init__(self):
        self.future = asyncio.get_event_loop().create_future()

        self.window = Gtk.Window()
        button = Gtk.Button.new_with_label('Click here')
        button.connect('clicked', self.button_clicked)
        self.window.add(button)
        self.window.show_all()

    def button_clicked(self, widget):
        print('button clicked')
        self.window.close()
        self.future.set_result(None)


async def main():
    # Uncomment the following line and everything works.
    # asyncio.get_event_loop().call_later(1, heartbeat)
    demo = Demo()
    await demo.future
    print('future finished')


def heartbeat():
    asyncio.get_event_loop().call_later(1, heartbeat)
    print('tick')


if __name__ == '__main__':
    asyncio.get_event_loop().run_until_complete(main())
@jhenstridge
Copy link
Owner

This is essentially the problem I wrote about here:

https://blogs.gnome.org/jamesh/2019/10/07/gasyncresult-with-python-asyncio/

The basic problem is that GLib callbacks are outside of asyncio's view of what's happening on the thread. The work-around I used in that blog article was to treat these callbacks as if they were happening off-thread, and use call_soon_threadsafe as a way to schedule asyncio work. It's not pretty, but it gets the job done.

Part of the problem is that there's a lot more entry points that require passing control back to asyncio than just call_soon.

@talljosh
Copy link
Author

What kind of entry points are we talking about? To me the obvious non-I/O ones are call_soon and call_later/call_at. Are there other categories of entry point that suffer from this issue?

The problem I see with your call_soon_threadsafe workaround is that it requires the user to know that they're using one of these unsupported entry points, and they're not obviously documented (at least, they're not mentioned in README.md). But it seems to me if we can document the entry points, why not just fix them in a similar manner to what I've submitted for call_soon?

But as I said above, I'm not entirely sure what entry points we're talking about, so maybe that's not feasible.

@ydirson
Copy link

ydirson commented Apr 15, 2020

I ran into the same issue - thanks for pointing out to the call_soon_threadsafe solution, that works file (although as you say it's far from pretty). It would be nice to explain this in the README so people don't have to dive into the tickets to find out how to get it working :)

Also, as this sort of problem does not appear in gbulb, it would be worth mentioning explicitly in the differences between the two tools.

mnauw added a commit to mnauw/asyncio-glib that referenced this issue Aug 22, 2020
This combines variations of fixes that have already been proposed.

Fixes #1
Fixes jhenstridge#3
benzea pushed a commit to benzea/asyncio-glib that referenced this issue Nov 8, 2020
As it is right now, any python code that runs may modify the timeout
that would be passed into the select() call. As such, we can only run a
single mainloop iteration before the select() call needs to be
restarted.

Also, we cannot use g_source_set_ready_time, because GLib will always
do a full mainloop iteration afterwards to ensure that all sources had a
chance to dispatch.
This is easy to work around though as we can use the prepare callback to
pass the required timeout from python into the GLib main loop code.

Note that with this we end up iterating the main context but we never
actually run a GLib mainloop.

Fixes: jhenstridge#3
@benzea benzea linked a pull request Nov 8, 2020 that will close this issue
benzea pushed a commit to benzea/asyncio-glib that referenced this issue Nov 8, 2020
As it is right now, any python code that runs may modify the timeout
that would be passed into the select() call. As such, we can only run a
single mainloop iteration before the select() call needs to be
restarted.

Also, we cannot use g_source_set_ready_time, because GLib will always
do a full mainloop iteration afterwards to ensure that all sources had a
chance to dispatch.
This is easy to work around though as we can use the prepare callback to
pass the required timeout from python into the GLib main loop code.

Note that with this we end up iterating the main context but we never
actually run a GLib mainloop.

Fixes: jhenstridge#3
benzea pushed a commit to benzea/asyncio-glib that referenced this issue Mar 17, 2021
As it is right now, any python code that runs may modify the timeout
that would be passed into the select() call. As such, we can only run a
single mainloop iteration before the select() call needs to be
restarted.

Also, we cannot use g_source_set_ready_time, because GLib will always
do a full mainloop iteration afterwards to ensure that all sources had a
chance to dispatch.
This is easy to work around though as we can use the prepare callback to
pass the required timeout from python into the GLib main loop code.

Note that with this we end up iterating the main context but we never
actually run a GLib mainloop.

Fixes: jhenstridge#3
benzea pushed a commit to benzea/asyncio-glib that referenced this issue Apr 9, 2021
As it is right now, any python code that runs may modify the timeout
that would be passed into the select() call. As such, we can only run a
single mainloop iteration before the select() call needs to be
restarted.

Also, we cannot use g_source_set_ready_time, because GLib will always
do a full mainloop iteration afterwards to ensure that all sources had a
chance to dispatch.
This is easy to work around though as we can use the prepare callback to
pass the required timeout from python into the GLib main loop code.

Note that with this we end up iterating the main context but we never
actually run a GLib mainloop.

Fixes: jhenstridge#3
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 a pull request may close this issue.

3 participants