I am applying asynchronous task handling using Celery on my Django Project.
My project's logic:,
from frontend side, there is a table with rows each having an upload button. User clicks on it and a payload is sent to backend containing a url that contains a file.
File is recieved in django views. And saved into database, table Run. Immediately object is saved a post_save signal is triggered to run a celery task.
The task to be performed is, fetch a list of runs with specific status. For each run, perform a task of downloading the file.
I would like to perform this asynchronously in case there is more than one run. Keeping in mind user can click upload for more than one row from frontend.
I am setting up RabbitMQ as my broker. I have rabbitMQ installed and running. I have set the CELERY_BROKER_URL='amqp://localhost' too in settings.py. I am a little lost on what I should do next in my configurations, could I get some guidance. I think I need to configure celery worker on my tasks.
Below is my code so far :
views.py #view that saves to database
class RunsUploadView(APIView):
serializer_class = RunsURLSerializer
def post(self, request, *args, **kwargs):
crawler_name = self.request.data.get('crawler')
run_id = self.kwargs.get("run_id")
run_url = self.request.data.get("run_url")
run = Run()
run.name = f"{crawler_name}_{run_id}"
run.run = run_id
run.url = run_url
run.save()
return Response(model_to_dict(run))
models.py # run is saved to table Run then a post_save signal is triggered.
from django.db import models
class Run(models.Model):
UPLOAD_STATUS = (
("Pending", "pending"),
("Running", "running"),
("Success", "success"),
("Failed", "failed"),
)
name = models.CharField(max_length=100)
run = models.CharField(max_length=100, unique=True)
url = models.URLField(max_length=1000)
status = models.CharField(
max_length=50, choices=UPLOAD_STATUS, default="Pending")
started_at = models.DateTimeField(null=True)
done_at = models.DateTimeField(null=True)
signals.py #handling the post_save logic after save()
from django.db.models.signals import post_save
from django.dispatch import receiver
from main.models import Run
from main.tasks import DownloadRun
#receiver(post_save, sender=Run)
def download_file(sender, **kwargs):
pending_runs = Run.objects.filter(status='Pending') #all pending runs collected, I would need to handle the runs asynchronously.
for run in pending_runs:
run.status = "Started"
run.save()
DownloadRun(run)
Tasks.py #using a class because I am going to update with more functions.
class DownloadRun:
def __init__(self, run):
run_object = model_to_dict(run)
self.run_url = run_object["url"]
self.download_run()
def download_run(self, dest_folder="runs"):
""Run file is downloaded from url""
I figured out the way forward. I did not configure celery well.
tasks.py
from celery import shared_task #import shared_task decorator from celery
class DownloadRun:
def __init__(self, run):
run_object = model_to_dict(run)
self.run_url = run_object["url"]
self.download_run()
def download_run(self, dest_folder="runs"):
""Run file is downloaded from url""
#shared_task
def celery_task(run_id):
DownloadRun(run_id)
signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from main.models import Run
from main.tasks import celery_task #import celery_task from tasks.py
#receiver(post_save, sender=Run)
def download_file(sender, **kwargs):
pending_runs = Run.objects.filter(status='Pending')
for run in pending_runs:
run.status = "Started"
run.save()
celery_task.delay(run.run) #call celery delay() to invoke the task (pass the unique key as parameter, could be id, in my case I chose the run)
Related
I am using django-simple-history to save history of data. I want to save an extra field value to each history model before it is saved. I found the reference code in documentation mentioned above but cant use it. Please help.
from django.dispatch import receiver
from simple_history.signals import (
pre_create_historical_record,
post_create_historical_record
)
#receiver(pre_create_historical_record)
def pre_create_historical_record_callback(sender, **kwargs):
print("Sent before saving historical record")
#receiver(post_create_historical_record)
def post_create_historical_record_callback(sender, **kwargs):
print("Sent after saving historical record")
apps.py file
from django.apps import AppConfig
class LogAppConfig(AppConfig):
name = 'log_app'
def ready(self):
import log_app.signals
signals.py file
from simple_history.signals import (pre_create_historical_record, post_create_historical_record)
#receiver(pre_create_historical_record)
def pre_create_historical_record_callback(sender, **kwargs):
print("signal is running")
history_instance = kwargs['history_instance']
I'm struggling with the following.
I'm trying to create a custom signal that will trigger when the current time will be equal to the value of my model's notify_on DateTimeField.
Something like this:
class Notification(models.Model):
...
notify_on = models.DateTimeField()
def send_email(*args, **kwargs):
# send email
signals.when_its_time.connect(send_email, sender=User)
After I've read through all docs and I found no information on how to implement such a signal.
Any ideas?
UPDATE:
Less naive approach with ability to discard irrelevant tasks: https://stackoverflow.com/a/55337663/9631956
Ok, thanks to comments by #SergeyPugach I've done the following:
Added a post_save signal that calls a function that adds a task to the celery. apply_async let's you pass eta - estimated time of arrival which can accept DateTimeField directly, that's very convenient.
# models.py
from django.db.models import signals
from django.db import models
from .tasks import send_notification
class Notification(models.Model):
...
notify_on = models.DateTimeField()
def notification_post_save(instance, *args, **kwargs):
send_notification.apply_async((instance,), eta=instance.notify_on)
signals.post_save.connect(notification_post_save, sender=Notification)
And the actual task in the tasks.py
import logging
from user_api.celery import app
from django.core.mail import send_mail
from django.template.loader import render_to_string
#app.task
def send_notification(self, instance):
try:
mail_subject = 'Your notification.'
message = render_to_string('notify.html', {
'title': instance.title,
'content': instance.content
})
send_mail(mail_subject, message, recipient_list=[instance.user.email], from_email=None)
except instance.DoesNotExist:
logging.warning("Notification does not exist anymore")
I will not get into details of setting up celery, there's plenty of information out there.
Now I will try to figure out how to update the task after it's notification instance was updated, but that's a completely different story.
In django's documentation there is two interesting signals that may help you on this task: pre_save and post_save.
It depends on your needs, but let's say you want to check if your model's notify_on is equal to the current date after saving your model (actually after calling the save() or create() method). If it's your case you can do:
from datetime import datetime
from django.contrib.auth.models import User
from django.db import models
from django.dispatch import receiver
from django.db.models.signals import post_save
class Notification(models.Model):
...
# Every notification is related to a user
# It depends on your model, but i guess you're doing something similar
user = models.ForeignKey(User, related_name='notify', on_delete=models.DO_NOTHING)
notify_on = models.DateTimeField()
...
def send_email(self, *args, **kwargs):
"""A model method to send email notification"""
...
#receiver(post_save, sender=User)
def create_notification(sender, instance, created, **kwargs):
# check if the user instance is created
if created:
obj = Notification.objects.create(user=instance, date=datetime.now().date())
if obj.notify_on == datetime.now().date():
obj.send_email()
And you should know, that django signals won't work by they own only if there is an action that triggers them. What this mean is that Django signals won't loop over your model's instances and perform an operation, but django signals will trigger when your application is performing an action on the model connected to a signal.
Bonus: To perform a loop over your instances and process an action regulary you may need an asyncworker with a Queue database (mostly, Celery with Redis or RabbitMQ).
In my model, I have a status field with a default value of 'Processing'. In the Django admin interface, after user clicks 'Save' button, the form inputs are passed to a celery task that just sleeps for 30 seconds.
After that 30 seconds, how do I:
determine if the celery task was successful?
update the model's status field from 'Processing' to the actual status (ex: Completed, Failed?
models.py
from django.db import models
class Scorecard(models.Model):
name = models.CharField(max_length=100, unique=True)
status = models.CharField(max_length=20, default='Processing')
def __str__(self):
return self.name
admin.py
from django.contrib import admin
from scorecards.models import Scorecard
from scorecards.tasks import get_report
class ScorecardAdmin(admin.ModelAdmin):
list_display = ['name', 'status']
def save_model(self, request, obj, form, change):
if form.is_valid():
data = form.cleaned_data
name = data['name']
get_report.delay(name)
super().save_model(request, obj, form, change)
admin.site.register(Scorecard, ScorecardAdmin)
tasks.py
from __future__ import absolute_import, unicode_literals
from celery import shared_task
from time import sleep
#shared_task
def get_report(name):
sleep(30)
Real-time status of the celery task updating the status field every x time intervals would be nice, but for now I'm just really curious how to do this at all.
I didn't figure out real-time status (yet) but did manage to change the status once a task is completed.
These were the main parts below. The critical reason to understand why this works is that I'm starting a celery worker in --pool=solo like so:
celery -A scorecard worker --pool=solo -l info
This is a single-threaded execution pool (which is fine for my current purposes) but it means it will process the first task get_report(name), and when that's done, process the set_task_status(id) where it checks the status of the result and sets the status field to be whatever the actual status is.
models.py
class Scorecard(models.Model):
....
task_id = models.CharField(max_length=50)
admin.py
class ScorecardAdmin(admin.ModelAdmin):
...
result = get_report.delay(name)
set_task_status.delay(result.task_id)
...
tasks.py
#shared_task
def get_report(name):
sleep(30)
#shared_task
def set_task_status(id):
instance = Scorecard.objects.get(task_id=id)
task_status = AsyncResult(id)
instance.status = task_status.status
instance.save()
This is what I've figured out so far.
Sitting over a day on it. Really can't understand why this signal is not triggered when a user is activated, no error log, no exception in the admin on activation. Can anybody help? The following code should result in a log message in the apache error.log when a user, right?
import logging
from django.dispatch import receiver
from registration.signals import user_activated
#receiver(user_activated)
def registered_callback(sender, **kwargs):
logger = logging.getLogger("user-activated")
logger.error("activated here")
same with user_registered
First of all im using django 1.8.3 .You should register your signal first. As far as i know, there are some methods to do that but this is what im doing;
Create signals.py in your app write your signal there;
from django.db.models.signals import post_save
from django.dispatch import receiver
#receiver(post_save, sender=your_model,dispatch_uid="yourmodel_save_receiver")
def post_save_yourmodel(sender, instance, **kwargs):
if instance.profile_status:
print "active"
else:
print "not active"
Then you should create apps.py. This file contains configuration information to your model.
from django.apps import AppConfig
class yourmodel_config(AppConfig):
name = 'yourmodel_config'
verbose_name = 'your_model config'
def ready(self):
import yourmodel.signals
With this whenever your app is ready, your signals will be imported
Finally open your __init__.py and add the following.
default_app_config = 'yourmodel.apps.yourmodel_config'
With this you are defining application configuration for your model.This example when ever yourmodel is saved, signal checks for profile_status attribute and prints output depending on the value(true or false) to your console. You can also add created parameter to your model to know that if instance of the model is created. created will return True if a new record was created. def post_save_yourmodel(sender, instance, created, **kwargs):. Otherwise this signal will be triggered whenever your model is saved with yourmodel.save().
Consider that is a post_save example.You can find list of the model signals from here.
I'm trying to setup a signal so that when a valid form is saved, a function is ran to carry out a related task.
My app structure is as follows;
- events
- helpers
- __init__.py
- status.py
- models
- signals
- __init__.py
- event.py
- __init__.py
- event.py
- status.py
- views
- __init__.py
- event.py
I believe signals need to be imported as early as possible, before models, so at the top of models/__init__.py I've got from .signals import *.
# views/event.py
class AddEventView(CreateView):
"""
View for adding an Event.
"""
model = Event
form_class = EventForm
success_url = reverse_lazy('events:all_events')
def form_valid(self, form):
self.object = form.save()
signals.event_status.send(
sender=None, request=self.request, event=self.object, status=None
) # Should the sender be self.object?
return super(AddEventView, self).form_valid(form)
# signals/event.py
from django.dispatch import Signal
event_status = Signal(providing_args=["request", "event", "status"])
# helpers/status.py
from ..models import Status, StatusHistory
from ..models.signals import event_status
def create_status(sender, **kwargs):
"""
Create a status for a given event.
"""
event = kwargs['event']
status = kwargs['status']
creator = User.objects.get(pk=event.creator)
try:
current_status = StatusHistory.objects.filter(
event=event).order_by('timestamp')[0]
except IndexError:
# Not sure what we're doing here yet.
pass
if not status:
status = Status.objects.get(description=_("Submitted"))
statushistory = StatusHistory.create(
event=event,
event_status=status,
user=creator
)
statushistory.save()
event_status.connect(create_status)
I'm running the debug server in Pycharm with a break point in the create_status() function & it's never getting hit.
Have I implemented this wrong?
I've used signals in some of my projects and I allways import the signals in the __init__.py of my Django APP (Same folder as settings.py, views.py, urls.py...)
__init__.py:
import signals
signals.py:
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from my_project.models import *
#receiver(post_save, sender=Modelname) # Called after an object is saved
def create_modelname(sender, **kwargs):
obj = kwargs['instance'] # I get the object being saved here
# ... Here I do whatever I want
#receiver(pre_delete, sender=Modelname) # Called before an object is deleted
def delete_modelname(sender, **kwargs):
obj = kwargs['instance']
# ... Do whatever you need
Remember this 2 imports:
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
Remember to import the signals
To import the signals you need to add import signals in your __init__.py of your project
Using this code, this functions are called automatically by Django when an object of the class Modelname is created or deleted.
The receiver for created object is called after the object is created, and the receiver for deleted object is called before the object is deleted.
I think maybe you just need to import your helpers/status.py eg in models/__init__.py
otherwise your event_status signal gets defined ok but the signal handler create_status never gets connected by Django
if you only have one handler for that signal it might make sense to put it in the same module as the signal definition
I found one case that signal is not working.
Here are cases that signal(pre_save, post_save) won't happen.
Model.objects.filter(pk=pk).update(key=value)
Bulk model functions won't happen signals.