#Async Views

23 messages · Page 1 of 1 (latest)

oak pond
#

i have a simple view that register a user and then send an email nothing crazy, however, the process is a bit slow, i tried using async but everything collapsed for some reason.

#
async def create_and_email_user(user):
    await user.save()
    await signup_email(user)

async def create_new_user(request):
    if not request.user.is_authenticated:
        redirect('login')

    form = CustomUserForm()
    if request.method == 'POST':
        form = CustomUserForm(request.POST)
        if form.is_valid():
            if form.cleaned_data['password1'] == form.cleaned_data['password2']:
                user = form.save(commit=False)
                form.clean_daily_hours()
                user.username = user.username.lower()

                await async_to_sync(create_and_email_user)(user)

                messages.success(request, "User has been created successfully.")
                return redirect('users') 
            else:
                messages.error(request, "Passwords are different, please check again.")
        else:
            messages.error(request, "Invalid inputs, please check again!")

    context = {
        "form": form
    }
    return render(request, "project_manager/admin_control/users/create_user.html", context)
restive parcel
#
await signup_email(user)

How fast is it when you comment out this line?

#

This is your primary issue, sync or async, it's not going to matter. You're waiting for the email to be sent.

#

This involves resolving the mail servers's hostname, connecting, sending the email, which is generally slow.

oak pond
restive parcel
#

is the primary reason for moving to async, that the request is "slow" ?

oak pond
restive parcel
#

well sadly that will not help, calling send mail asynchronously will not improve the performance. You need to run that in the background instead.

restive parcel
#

The most obvious answer for this is, use celery. [or django-q, or any other]

oak pond
restive parcel
#

you need a broker, the simplest to install and use is generally redis.

silent hatch
#

As @restive parcel says. Celery is the best way in your case. Little bit easier way is CRON job which will fire every minutes and send emails to appropriate users + mark them as "email_sent = true". Bit this is workaround if you don't want (or you can't) run redis on your server.

oak pond
silent hatch
#

no problem with setting up everything in one server 🙂

oak pond
#

thank u guys
@restive parcel @silent hatch

oak pond
#

and big thank for this guy on YouTube:
@ajudmeister
it's not a marketing thing, this man is just so kind he replied to my comments and gave me a great solution using signals and it worked like a charm:

from django.dispatch import receiver
from django.db.models.signals import post_save
from django.core.mail import EmailMultiAlternatives
import threading


@receiver(post_save, sender=CustomUser)
def send_email_async(sender, instance, created, **kwargs):
    if created:
        post = instance
        post_email = post.email
        email_subject = 'Email Title'
        email_body = 'Email Body'

        # Use a separate thread to send the email asynchronously
        email_thread = threading.Thread(target=send_email, args=(email_subject, email_body, post_email))
        email_thread.start()

def send_email(subject, message, recipient):
    email = EmailMultiAlternatives(subject, '', to=[recipient])
    email.attach_alternative(message, 'text/html')
    email.send()

he is so underrated and he is building great things in django that worth taking a look, like a real-time chat app using channels and WebSockets with HTMX and redis and many applications.

restive parcel
#

I'm glad you're enjoying their content, I'm skeptical about the solution. AFAIK (unless corrected, I'll look it up later) the signals run on the same thread, so using a post_save signal is no different than adding that code after your "save" your CustomUser.

Additionally I dont believe you control the worker process, so it may be possible that the worker dies/restarts/etc before that thread completes its job.

Again, this is not to say that this solution doesn't work, threading is just not a usual solution to such problems.

oak pond
# restive parcel I'm glad you're enjoying their content, I'm skeptical about the solution. AFAIK ...

I am not familiar of how it works actually, but I could say now when I add the user/send email it's 3x faster, it doesn't take the time to add the user then send the email like before, I don't know how, but it definitely not like sending the email after "save".
although I get your point and you are correct as far as I know, but when using threading to send emails within the signal handler, a separate thread is indeed created: email_thread = threading.Thread
as it's now it sends the email asynchronously and does not block the main thread, allowing the main thread to continue excution without waiting for the email sending process to complete.
but again am not fimiliar with how it works exactly, I could be wrong, maybe it give the appearance of faster execution while running the signal handler itself synchronously within the main django thread.
glad you brought that out, I will look into this and maybe use celery after all.

restive parcel
#

What I meant was, you could remove the "signal" and just call the rest as is, it will function the same:

-@receiver(post_save, sender=CustomUser)
-def send_email_async(sender, instance, created, **kwargs):
+def send_email_async(instance):
    if created:
        post = instance
        post_email = post.email
        email_subject = 'Email Title'
        email_body = 'Email Body'

        # Use a separate thread to send the email asynchronously
        email_thread = threading.Thread(target=send_email, args=(email_subject, email_body, post_email))
        email_thread.start()

def send_email(subject, message, recipient):
    email = EmailMultiAlternatives(subject, '', to=[recipient])
    email.attach_alternative(message, 'text/html')
    email.send()

and just call send_email_async(instance) after your user save()

oak pond
#

ok, so now I can control the worker process without relyeing on django signals?