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

IMPROVEMENT: Support for asynchronous sending #296

Open
Jakobovski opened this issue Feb 18, 2017 · 17 comments
Open

IMPROVEMENT: Support for asynchronous sending #296

Jakobovski opened this issue Feb 18, 2017 · 17 comments
Labels
status: help wanted requesting help from the community type: community enhancement feature request not on Twilio's roadmap

Comments

@Jakobovski
Copy link

Thanks for the great API and product!

It would be awesome if there was support for asynchronous api calls.

Use Case:
When charging a user's credit card over my REST API I need to call the stripe API ( a few times), then call sendgrid API to send user confirmation message, then call sendgrid again to notify my sales team of a new sale. This takes a bit of time to wait for all the responses and causes my API it be rather slow.

Cheers.

@thinkingserious thinkingserious added status: help wanted requesting help from the community type: community enhancement feature request not on Twilio's roadmap labels Feb 20, 2017
@thinkingserious
Copy link
Contributor

Hello @Jakobovski,

Thanks for taking the time to not only add the request, but for the additional details! And thanks for the kind words as well!

We will add this to our backlog. For it to rise in importance, we would require additional votes or a pull request.

With Best Regards,

Elmer

@jussih
Copy link

jussih commented Oct 3, 2017

This is something I wouldn't expect in a library, but rather something that should be implemented in the project that uses the library. The de facto solution with Python is to write an asynchronous task to do the heavy lifting using Celery.

@thinkingserious
Copy link
Contributor

Hi @jussih,

Would you mind creating documentation that demonstrates how this can be done? You would add your PR here and we would give you hacktoberfest credit at "difficulty: medium" for that. If not, no worries. We appreciate your feedback in any case :)

With Best Regards,

Elmer

@LiYChristopher
Copy link
Contributor

LiYChristopher commented Oct 4, 2017

Celery is a great tool for async or parallel processing. But if one is using Python 3.5+, the built-in asyncio library can be used to send email in a non-blocking manner. I created a gist to illustrate roughly how this might work.

SendGrid v3 Mail Send - Async Example

asyncio helps us execute mail sending in a separate context, allowing us to continue execution of business logic without waiting for all our emails to send first.

@tr11
Copy link

tr11 commented Oct 16, 2017

I've been doing something like this for a while, but resorted to updating the http-client library instead of making any changes to this library. You can take a look at https://github.com/tr11/python-http-client, which uses aiohttp instead of the requests library to do the heavy lifting.

@thinkingserious
Copy link
Contributor

@LiYChristopher,

Do you mind adding that example to this repo's USE_CASES.md?

That's awesome @tr11!

Do you mind making a PR for hacktoberfest on that library to demonstrate your example? You would create a USE_CASES.md based on the format of the one found in this repo.

With Best Regards,

Elmer

@LiYChristopher
Copy link
Contributor

@thinkingserious

I have a PR open - #363 that adds this example to the USE_CASES.md file. If there are any other changes I need to make beyond contained in the change log, please let me know. Thanks!

@mbernier mbernier removed difficulty: medium fix is medium in difficulty difficulty: very hard fix is very hard in difficulty labels Oct 27, 2017
@thinkingserious thinkingserious added status: work in progress Twilio or the community is in the process of implementing difficulty: medium fix is medium in difficulty and removed status: help wanted requesting help from the community labels Feb 27, 2018
@dizlv
Copy link

dizlv commented Apr 10, 2018

@LiYChristopher are you sure your code actually works? Client post will block scheduler, or I'm missing something?

response = sg.client.mail.send.post(request_body=email.get())

@stalkerg
Copy link

Hello.
I suppose we can use the same approach what I used for https://github.com/stalkerg/pywebpush - sendgrid API will return just URL and data to send, and all sending mechanisms should be outside.
Like

sg = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY'))
url, data = sg.prepare_send(message)
response = await your_async_method_for_send(url, data)
response = sg.process_response(response)

because we have many different libs (for me it's Tornado) to make an async request we should provide an easy way to integration with any of it.

@ejm
Copy link

ejm commented Sep 29, 2019

If this is still on the table, I'd love to take it on as a Hacktoberfest project two years later following the idea of @stalkerg here. Would the method name prepare_send be okay? I wouldn't imagine a process_response method would be necessary as send just returns the response untouched.

I think send should keep the default client (for backwards compatibility) but prepare_send would just be another option for people who are using async or their own HTTP clients.

@Taywee
Copy link

Taywee commented Nov 20, 2020

This has been open for a while, and nobody has mentioned it, but you can run blocking network requests in parallel through the default executor. It's not an obvious solution (mentioned only in a corner of the asyncio documentation and referring to a method that doesn't directly refer to being run in a background thread, but the example code demonstrates it), and it's not perfect (because it's not actually using async IO, but executing parallel through threads behind the scenes) but this will work when run from within any async function, and will be properly parallelized without blocking or exposing thread safety issues:

mail = {
    # Mail body here
}
loop = asyncio.get_running_loop()  # or get_event_loop if you need to use Python 3.6
response = await loop.run_in_executor(
    None,
    functools.partial(
        api_client.client.mail.send.post,
        request_body=mail))

Like I said, not the most ideal, but for my uses, it allows backgrounding parallel IO to keep my application responsive and snappy.

The previous answer posted by @LiYChristopher will not work. Each request will still fully block the scheduler. I only mention it because it has a handful of positive reactions despite not functioning the way it looks like it does.

edit: I should mention that this is thread-safe in respect to asyncio, but it does involve threads running in parallel, so if sendgrid's API client is not thread-safe for any reason, this will actually be vulnerable to thread safety issues. I don't think this will be the case in cPython due to the GIL, but I can't assert that for certain. I have had exactly one 503 error from SendGrid while interacting in this way, but I don't think it's related to this parallelism and I haven't been able to replicate it.

@stalkerg
Copy link

@Taywee, after all, I started to communicate with REST API directly it's much easier than fighting with the library in the async environment.

@Taywee
Copy link

Taywee commented Nov 23, 2020

@stalkerg
That's fine. The SendGrid API is pretty easy to work with directly. This library is synchronous, though, so the only way to get parallel IO with it currently is through a separate thread or process, whether using traditional threading, an asynchronous executor (including the default ThreadPoolExecutor that the default Python event loop makes available), or some other way of offloading the parallel work to a thread and waiting for it.

@thinkingserious
Copy link
Contributor

I prefer @stalkerg's solution, mainly because there are several popular async libs and it would be great to better support them all. Then we can add usage examples in the most popular async libs in the docs.

Thank you to everyone on this thread (and linked threads) for the thoughtful conversation. And thank you @Taywee for the PR and explanations with sample code!

This issue has been added to our internal backlog to be prioritized. Pull requests and +1s on the issue summary will help it move up the backlog.

@thinkingserious thinkingserious added status: help wanted requesting help from the community and removed difficulty: medium fix is medium in difficulty status: work in progress Twilio or the community is in the process of implementing labels Dec 1, 2020
@stalkerg
Copy link

stalkerg commented Dec 1, 2020

@thinkingserious this also should fix this issue #409 ;)

@stalkerg
Copy link

Interesting, this approach even formalized!
https://sans-io.readthedocs.io/
you definitely should move into this side.

@quantology
Copy link

quantology commented Dec 11, 2021

Since there aren't any working code examples posted above, I figured I'd share what I just cooked up:

import aiohttp
from sendgrid import SendGridAPIClient

async def send_async(client, message):
    if not isinstance(message, dict):
        message = message.get()
    async with aiohttp.ClientSession() as session:
        async with session.post(f"{client.host}/v3/mail/send", 
                                headers=client._default_headers,
                                json=message) as resp:
            resp.raise_for_status()
            return await resp.text()
SendGridAPIClient.send_async = send_async

Later, I call with: response = await sg_client.send_async(message).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: help wanted requesting help from the community type: community enhancement feature request not on Twilio's roadmap
Projects
None yet
Development

No branches or pull requests