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

Asynchronous version of notify() #396

Closed
YoRyan opened this issue Jun 5, 2021 · 7 comments
Closed

Asynchronous version of notify() #396

YoRyan opened this issue Jun 5, 2021 · 7 comments

Comments

@YoRyan
Copy link
Contributor

YoRyan commented Jun 5, 2021

💡 The Idea
Great project! But I have to say, if you're calling Apprise from a program that is already structured around asyncio (e.g. my new thingy), it's both inefficient and redundant to have to spin up another event loop to call the non-async notify() method.

It would be much better to have direct access to the asynchronous function that Apprise already constructs. Then, you could just await it within your existing event loop.

🔨 Breaking Feature

@caronc
Copy link
Owner

caronc commented Jun 8, 2021

It's a great idea! I just have a few comments on this:

  • Backwards support for Python v2.7 is part of the reason it's not the default to just be fully asyncio
  • There was an issue with the Home Assistant Integration in the past because Apprise wraps requests which isn't truly asyncio friendly. You can see the full details (and merge request here). During which time there was some customization made to how the asyncio is handled under the hood. I'm not sure if this will break or play a factor into your suggestion.

So are you looking for something like:

import apprise

apobj = apprise.AsyncApprise()
apobj.add('url1')
apobj.add('url2')

await apobj.notify()

Or something along those lines?

Thoughts?

@YoRyan
Copy link
Contributor Author

YoRyan commented Jun 8, 2021

I'm envisioning a new method for the existing class:

apobj = apprise.Apprise()

[...]

await apobj.async_notify()

Note that you can't just change a synchronous function to an asynchronous function and expect existing code to work; writing a function that is both synchronous and asynchronous, in addition to being nontrivial, is considered bad practice.

Thanks for the background information; it was very helpful. I've poked around the code to gain an understanding of the execution flow. Currently, for Python 3, it is as follows:

  1. py3compat.asyncio.notify() spools up an event loop to run...
  2. ...a list of coroutines created by Apprise.notify(), each of which...
  3. ...creates a ThreadPoolExecutor to...
  4. ...call the synchronous NotifyBase.notify() method within the event loop launched by py3compat.asyncio.notify().

Prior to #304 there was no ThreadPoolExecutor step, so NotifyBase.notify() was actually blocking the event loop. This should have negated any advantage of using asyncio.

IMO, the right way to do this would be:

  1. (If using the synchronous version of Apprise.notify(), set up a new event loop and enter async mode.)
  2. Use loop.run_in_executor() to create a coroutine for each call to NotifyBase.notify(), which, for the moment, will stay synchronous. (The first version of Run synchronous code asynchronously #304 did this; I'm not sure why @raman325 switched to ThreadPoolExecutor?)
  3. Use asyncio.gather() to await these coroutines simultaneously.

In the long run, dropping support for Python 2 and converting the rest of Apprise to async would simplify things massively, and allow us to reap the full performance benefits of asyncio.

@raman325
Copy link
Contributor

raman325 commented Jun 8, 2021

I can't explain why I did that because it has been a long time and I spent a lot of time working my way through the code but quickly forgot it. The problem we were trying to solve is that the function was blocking the event loop (as you said) and we didn't want it to do that when it was running in an asynchronous context

@YoRyan
Copy link
Contributor Author

YoRyan commented Jun 8, 2021

I ask because, if I'm not mistaken, there should be no difference between using the event loop's existing executor and creating a new ThreadPoolExecutor instance. 1548ade

@YoRyan
Copy link
Contributor Author

YoRyan commented Jun 19, 2021

@caronc, I've taken a shot at implementing my proposal. Let me know what you think!

@caronc
Copy link
Owner

caronc commented Jun 19, 2021

@YoRyan I added some comments; overall it's amazing how much you've done. 🚀 ⭐ I just had a few questions/concerns (but maybe it's more of just educating me on your thought process). My comments were made to your PR.

@caronc
Copy link
Owner

caronc commented Jul 1, 2021

Great work @YoRyan; your fantastic contribution has been merged. 👍

I really appreciate it! ❤️ 🙏

I am going to close this issue now but feel free to open a new one anytime you want. I also won't complain if you do all of the heavy lifting yourself either in a PR like you did here. 😉

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

No branches or pull requests

3 participants