Använda selleri med Django för bakgrundsuppgiftsbehandling

Webapplikationer börjar vanligtvis enkelt men kan bli ganska komplexa, och de flesta av dem överskrider snabbt ansvaret för att endast svara på HTTP-förfrågningar.

När det händer måste man skilja mellan vad som måste hända omedelbart (vanligtvis i HTTP-begäran livscykel) och vad som kan hända så småningom. Varför är det så? Tja, för när din ansökan blir överbelastad med trafik, gör det enkla saker som detta. 

Verksamheten i en webbapplikation kan klassificeras som kritisk eller förfrågad tid och bakgrundsuppgifter, de som händer utanför begäranstid. Dessa kartor till de som beskrivs ovan: 

  • måste hända genast: request-time-operationer
  • måste ske så småningom: bakgrundsuppgifter

Request-time-operationer kan göras på en enda begäran / svarscykel utan att oroa sig för att operationen kommer att gå ut eller att användaren kan ha en dålig upplevelse. Vanliga exempel inkluderar databasfunktionerna CRUD (Skapa, läs, uppdatera, ta bort) och användarhantering (inloggning / utloggning).

Bakgrundsuppgifter är olika, eftersom de oftast är ganska tidskrävande och är benägna att misslyckas, främst på grund av externa beroenden. Några vanliga scenarier bland komplexa webbapplikationer är:

  • skicka bekräftelse eller aktivitet e-post
  • Daglig krypning och skrapning av information från olika källor och lagring av dem
  • utföra dataanalys
  • radering av onödiga resurser
  • exporterande dokument / foton i olika format

Bakgrundsuppgifter är huvudfokus för denna handledning. Det vanligaste programmeringsmönstret som används för detta scenario är producentens konsumentarkitektur. 

I enkla termer kan denna arkitektur beskrivas så här: 

  • Producenter skapar data eller uppgifter.
  • Uppgifter läggs i en kö som kallas uppgiftskö. 
  • Konsumenterna är ansvariga för att konsumera uppgifterna eller utföra uppgifterna. 

Vanligtvis hämtar konsumenterna uppgifter från kön i ett första-i-först-ut-format (FIFO) eller enligt deras prioriteringar. Konsumenterna kallas också som arbetare, och det är termen som vi kommer att använda hela, eftersom det är förenligt med terminologin som används av den diskuterade tekniken.

Vilken typ av uppgifter kan behandlas i bakgrunden? Uppgifter som:

  • är inte nödvändiga för webbprogrammets grundläggande funktionalitet
  • kan inte köras i begäran / svarcykeln eftersom de är långsamma (I / O-intensiva etc.)
  • beror på externa resurser som kanske inte är tillgängliga eller inte uppträder som förväntat
  • kan behöva försökas minst en gång
  • måste utföras på ett schema

Selleri är de facto valet för att göra bakgrundsuppgift bearbetning i Python / Django ekosystemet. Den har ett enkelt och tydligt API, och det integrerar vackert med Django. Den stöder olika tekniker för uppgiftskön och olika paradigmer för arbetarna.

I den här handledningen kommer vi att skapa en Django-leksaks webbapplikation (hantera verkliga scenarier) som använder bakgrundsuppgiftsbehandling.

Ställa in saker upp

Om du antar att du redan är bekant med Python-pakethantering och virtuella miljöer, låt oss installera Django:

$ pip installera Django

Jag har bestämt mig för att bygga ytterligare en blogga applikation. Programmets fokus ligger på enkelhet. En användare kan helt enkelt skapa ett konto och utan för mycket krångel kan skapa ett inlägg och publicera det på plattformen. 

Ställ in quick_publisher Django-projektet:

$ django-admin startprojekt quick_publisher

Låt oss börja appen:

$ cd quick_publisher $ ./manage.py startapp main

När jag börjar ett nytt Django-projekt tycker jag om att skapa en huvud applikation som bland annat innehåller en anpassad användarmodell. Ofta än inte, stöter jag på begränsningar av standard Django Användare modell. Har en egen Användare modellen ger oss fördelen av flexibilitet.

# main / models.py från django.db importmodeller från django.contrib.auth.models importera AbstractBaseUser, PermissionsMixin, BaseUserManager klass UserAccountManager (BaseUserManager): use_in_migrations = Sann def_create_user (själv, email, lösenord, ** extra_fields): om inte e-post: hämta ValueError ('E-postadress måste anges') om inte lösenord: höja ValueError ('Lösenord måste anges') email = self.normalize_email (email) user = self.model (email = email, ** extra_fields) user.set_password (password) user.save (using = self._db) returnera användaren def create_user (själv, email = Ingen, lösenord = Ingen, ** extra_fields): returnera self._create_user (email, password, ** extra_fields) def Create_superuser (self, email, password, ** extra_fields): extra_fields ['is_staff'] = Sann extra_fields ['is_superuser'] = Sann retur själv._create_user (email, password, ** extra_fields) klass Användare (AbstractBaseUser, PermissionsMixin): REQUIRED_FIELDS = [] USERNAME_FIELD = 'email' objects = UserAccountManager () email = models.EmailField ('email', unikt = True, blank = False, null = False) full_name = models.CharField ('fullständigt namn', blank = Sann, null = True, max_length = 400) is_staff = models.BooleanField , default = False) is_active = models.BooleanField ('aktiv', default = True) def get_short_name (self): return self.email def get_full_name (själv): returnera self.email def __unicode __ (själv): returnera self.email

Var noga med att kolla in Django-dokumentationen om du inte är bekant med hur anpassade användarmodeller fungerar.

Nu måste vi berätta för Django att använda den här användarmodellen istället för standarden. Lägg till den här raden i quick_publisher / settings.py fil:

AUTH_USER_MODEL = 'main.User' 

Vi måste också lägga till huvud ansökan till INSTALLED_APPS lista i quick_publisher / settings.py fil. Vi kan nu skapa migreringar, tillämpa dem och skapa en superanvändare för att kunna logga in på Django admin panel:

$ ./manage.py makemigrations huvud $ ./manage.py migrera $ ./manage.py createsuperuser

Låt oss nu skapa en separat Django-applikation som ansvarar för inlägg:

$ ./manage.py startapp publicera

Låt oss definiera en enkel Post-modell i utgivare / models.py:

från django.db importmodeller från django.utils importera tidszon från django.contrib.auth importera get_user_model klass Post (models.Model): author = models.ForeignKey (get_user_model ()) created = models.DateTimeField ('Skapad Datum', standard = timezone.now) title = models.CharField ('Titel', max_length = 200) content = models.TextField ('Innehåll') slug = models.SlugField ('Slug') def __str __ (själv): returnera " "av% s '% (self.title, self.author)

Hooking the Posta Modellen med Django admin är klar i utgivare / admin.py filen så här:

från django.contrib import admin från .models import Post @ admin.register (Post) class PostAdmin (admin.ModelAdmin): passera

Slutligen, låt oss haka på utgivare ansökan med vårt projekt genom att lägga till det till INSTALLED_APPS lista.

Vi kan nu köra servern och gå vidare till http: // localhost: 8000 / admin / och skapa våra första inlägg så att vi har något att leka med:

$ ./manage.py körserver

Jag litar på att du har gjort dina läxor och du har skapat inläggen. 

Låt oss gå vidare. Nästa uppenbara steg är att skapa ett sätt att se de publicerade inläggen. 

# publisher / views.py från django.http import Http404 från django.shortcuts import render från .models import Post def view_post (begäran, slug): försök: post = Post.objects.get (slug = slug) utom Post.DoesNotExist: höja Http404 ("Poll existerar inte") returnera render (begäran, "post.html", context = 'post': post)

Låt oss associera vår nya vy med en webbadress i: quick_publisher / urls.py

# quick_publisher / urls.py från django.conf.urls importera url från django.contrib import admin från publisher.views import view_post urlpatterns = [url (r '^ admin /', admin.site.urls), url (r '^ (? P[a-zA-Z0-9 \ -] +) ', view_post, name = "view_post")]

Slutligen, låt oss skapa mallen som gör inlägget: utgivare / mallar / post.html

       

Post titel

post.content

Publicerad av post.author.full_name på post.created

Vi kan nu gå vidare till http: // localhost: 8000 / the-slug-of-the-post-you-skapade / i webbläsaren. Det är inte precis ett mirakel av webbdesign, men att göra snygga inlägg ligger utanför ramen för denna handledning.

Skickar bekräftelsemail

Här är det klassiska scenariot:

  • Du skapar ett konto på en plattform.
  • Du anger en e-postadress som ska identifieras unikt på plattformen.
  • Plattformen kontrollerar att du verkligen är ägaren till e-postadressen genom att skicka ett mail med en bekräftelseskod.
  • Innan du utför verifieringen kan du inte (helt) använda plattformen.

Låt oss lägga till en is_verified flagga och verification_uuid på Användare modell:

# main / models.py import uuid class Användare (AbstractBaseUser, PermissionsMixin): REQUIRED_FIELDS = [] USERNAME_FIELD = 'email'objekt = UserAccountManager () email = models.EmailField (' email ', unique = True, blank = False, null = Falskt) full_name = models.CharField ('fullständigt namn', blank = Sann, null = True, max_length = 400) is_staff = models.BooleanField ('personalstatus', default = False) is_active = models.BooleanField ('aktiv' default = True) is_verified = models.BooleanField ('verifierad', default = False) # Lägg till 'is_verified' flagg verification_uuid = models.UUIDField ('Unik verifiering UUID', default = uuid.uuid4) def get_short_name self.email def get_full_name (själv): returnera self.email def __unicode __ (själv): returnera self.email

Låt oss använda detta tillfälle för att lägga till användarmodellen till admin:

från django.contrib import admin från .models import Användare @ admin.register (User) class UserAdmin (admin.ModelAdmin): passera

Låt oss göra ändringarna reflekterade i databasen:

$ ./manage.py makemigrations $ ./manage.py migrera

Vi behöver nu skriva en kod som skickar ett mail när en användarinstans skapas. Det här är vad Django signaler är för, och det här är ett perfekt tillfälle att röra vid detta ämne. 

Signaler avfyras före / efter att vissa händelser inträffar i ansökan. Vi kan definiera återuppringningsfunktioner som utlöses automatiskt när signalerna avfyras. För att göra en återkallningsutlösare måste vi först ansluta den till en signal.

Vi ska skapa en återuppringning som kommer att utlösas efter att en användarmodell har skapats. Vi lägger till den här koden efter Användare modelldefinition i: main / models.py

från django.db.models importera signaler från django.core.mail import send_mail def user_post_save (avsändare, instans, signal, * args, ** kwargs): om inte instance.is_verified: # Skicka verifierings-email send_mail ('Verifiera ditt QuickPublisher-konto ',' Följ denna länk för att verifiera ditt konto: 'http: // localhost: 8000% s'% reverse ('verifiera', kwargs = 'uuid': str (instance.verification_uuid)), 'från @ quickpublisher. dev ', [instance.email], fail_silently = False,) signals.post_save.connect (user_post_save, avsändare = användare)

Vad vi har gjort här är vi har definierat a user_post_save funktionen och ansluten den till post_save signal (en som utlöses efter att en modell har sparats) skickad av Användare modell.

Django skickar inte bara e-postmeddelanden ut på egen hand. Det måste vara knutet till en e-posttjänst. För enkelhets skull kan du lägga till dina Gmail-uppgifter i quick_publisher / settings.py, eller du kan lägga till din favoritleverantör av e-post. 

Så här ser konfigurationen av Gmail ut:

EMAIL_USE_TLS = True EMAIL_HOST = 'smtp.gmail.com' EMAIL_HOST_USER = '@ gmail.com 'EMAIL_HOST_PASSWORD ='"EMAIL_PORT = 587

För att testa saker, gå in i adminpanelen och skapa en ny användare med en giltig e-postadress som du snabbt kan kolla. Om allt gick bra får du ett mail med en verifieringslänk. Verifieringsrutinen är inte klar än. 

Så här kontrollerar du kontot:

# main / views.py från django.http import Http404 från django.shortcuts import render, omdirigera från .models import Användare def home (request): returnera render (request, 'home.html') def verifiera (request, uuid): försök: user = User.objects.get (verification_uuid = uuid, is_verified = False) utom User.DoesNotExist: höja Http404 ("Användaren existerar inte eller är redan verifierad") user.is_verified = Sann user.save () returnera omdirigering () 'Hem')

Haka upp synpunkterna i: quick_publisher / urls.py

# quick_publisher / urls.py från django.conf.urls importera url från django.contrib import admin från publisher.views import view_post från main.views importera hem, verifiera urlpatterns = [url (r '^ $', hem, namn = " hem "), url (r '^ admin /', admin.site.urls), url (r '^ verifiera / (p[a-z0-9 \ -] +) / ', verifiera, namn = "verifiera"), url (r' ^ (P[a-zA-Z0-9 \ -] +) ', view_post, name = "view_post")]

Kom också ihåg att skapa en home.html fil i main / mallar / home.html. Det kommer att göras av Hem se.

Försök att köra hela scenariot igen. Om allt är bra får du ett mail med en giltig verifieringsadress. Om du följer webbadressen och sedan checkar in admin kan du se hur kontot har verifierats.

Sänder e-post asynkront

Här är problemet med vad vi gjort hittills. Du kanske har märkt att det är lite långsamt att skapa en användare. Det beror på att Django skickar verifierings-e-postmeddelandet inuti förfrågningstiden. 

Så här fungerar det: Vi skickar användardata till Django-programmet. Applikationen skapar en Användare modell och skapar sedan en anslutning till Gmail (eller en annan tjänst du valt). Django väntar på svaret, och då returnerar det bara ett svar till vår webbläsare. 

Här är var Selleri kommer in. Först, se till att det är installerat:

$ pip installera Selleri

Vi behöver nu skapa ett Selleri-program i vår Django-applikation:

# quick_publisher / celery.py importera os från selleri import Selleri os.environ.setdefault ('DJANGO_SETTINGS_MODULE', 'quick_publisher.settings') app = Selleri ('quick_publisher') app.config_from_object ('django.conf: settings') # Ladda uppgiftsmoduler från alla registrerade Django app konfigs. app.autodiscover_tasks ()

Selleri är en uppgiftskö. Den tar emot uppgifter från vår Django-applikation, och den kommer att köra dem i bakgrunden. Selleri måste vara parat med andra tjänster som fungerar som mäklare. 

Mäklare mellan sändningen av meddelanden mellan webbapplikationen och Selleri. I den här handledningen använder vi Redis. Redis är lätt att installera, och vi kan enkelt komma igång med det utan för mycket väsen.

Du kan installera Redis genom att följa anvisningarna på Redis Quick Start-sida. Du måste installera Redis Python-biblioteket, pip installera redis, och bunten som behövs för att använda Redis och selleri: pip installera selleri [redis].

Starta Redis-servern i en separat konsol så här: $ redis-server

Låt oss lägga till Celery / Redis relaterade configs till quick_publisher / settings.py:

# REDIS relaterade inställningar REDIS_HOST = 'localhost' REDIS_PORT = '6379' BROKER_URL = 'redis: //' + REDIS_HOST + ':' + REDIS_PORT + '/ 0' BROKER_TRANSPORT_OPTIONS = 'visibility_timeout': 3600 CELERY_RESULT_BACKEND = 'redis: / / '+ REDIS_HOST +': '+ REDIS_PORT +' / 0 '

Innan allt kan köras i Selleri, måste det förklaras som en uppgift. 

Så här gör du det här:

# main / tasks.py import loggning från django.urls importera omvänd från django.core.mail import send_mail från django.contrib.auth import get_user_model från quick_publisher.celery import app @ app.task def send_verification_email (user_id): UserModel = get_user_model ( ) försök: user = UserModel.objects.get (pk = user_id) send_mail ("Verifiera ditt QuickPublisher-konto", "Följ den här länken för att verifiera ditt konto:" http: // localhost: 8000% s '% reverse , kwargs = 'uuid': str (user.verification_uuid)), '[email protected]', [user.email], fail_silently = False,) förutom UserModel.DoesNotExist: logging.warning ("Försökte skicka verifiering email till icke-befintlig användare '% s' "% user_id)

Vad vi har gjort här är det här: Vi flyttade verifieringsadressens funktionalitet i en annan fil som heter tasks.py

Några anteckningar:

  • Namnet på filen är viktigt. Selleri går igenom alla appar i INSTALLED_APPS och registrerar uppgifterna i tasks.py filer.
  • Lägg märke till hur vi dekorerade Skicka bekräftelsemail fungera med @ app.task. Detta berättar Selleri Detta är en uppgift som kommer att köras i uppgiftskön.
  • Lägg märke till hur vi förväntar oss som argument användar ID snarare än a Användare objekt. Detta beror på att vi kan ha problem med att serialisera komplexa objekt när vi skickar uppgifterna till Selleri. Det är bäst att hålla dem enkla.

Kommer tillbaka till main / models.py, signalkoden blir till:

från django.db.models importera signaler från main.tasks importera send_verification_email def user_post_save (avsändare, instans, signal, * args, ** kwargs): om inte instance.is_verified: # Skicka verifierings-email send_verification_email.delay (instance.pk) signaler .post_save.connect (user_post_save, avsändare = användare)

Lägg märke till hur vi kallar .fördröjning metod på uppgiftsobjektet. Det betyder att vi skickar upp uppgiften till Selleri och vi väntar inte på resultatet. Om vi ​​använde send_verification_email (instance.pk) Istället skulle vi fortfarande skicka det till Selleri, men skulle vänta på uppgiften att slutföra, vilket inte är vad vi vill ha.

Innan du börjar skapa en ny användare finns det en fångst. Selleri är en tjänst, och vi måste börja det. Öppna en ny konsol, se till att du aktiverar lämplig virtualenv och navigera till projektmappen.

$ selleriarbetare -A quick_publisher --loglevel = debug --concurrency = 4

Det börjar fyra Celery processarbetare. Ja, nu kan du äntligen gå och skapa en annan användare. Lägg märke till hur det inte finns någon fördröjning, och se till att loggarna i selleri-konsolen visas och se om uppgifterna är korrekt utförda. Det här borde se ut så här:

[2017-04-28 15: 00: 09,190: DEBUG / MainProcess] Uppgift accepterat: main.tasks.send_verification_email [f1f41e1f-ca39-43d2-a37d-9de085dc99de] pid: 62065 [2017-04-28 15: 00: 11,740: INFO / PoolWorker-2] Uppgift main.tasks.send_verification_email [f1f41e1f-ca39-43d2-a37d-9de085dc99de] lyckades i 2.5500912349671125s: Ingen

Periodiska uppgifter med selleri

Här är ett annat vanligt scenario. De flesta mogna webbapplikationer skickar sina användare livscykel e-postmeddelanden för att hålla dem förlovade. Några vanliga exempel på livscykel e-postmeddelanden:

  • månadsrapporter
  • aktivitetsanmälningar (gillar, vänskapsförfrågningar, etc.)
  • påminnelser för att utföra vissa åtgärder ("Glöm inte att aktivera ditt konto")

Det här är vad vi ska göra i vår app. Vi ska räkna hur många gånger varje inlägg har blivit sedd och skicka en daglig rapport till författaren. En gång varje dag kommer vi att gå igenom alla användare, hämta sina inlägg och skicka ett mail med ett bord som innehåller inläggen och visa antalet.

Låt oss ändra Posta modell så att vi kan ta emot scenariot.

klassen Post (models.Model): author = models.ForeignKey (User) created = models.DateTimeField ('Skapad datum', default = timezone.now) title = models.CharField ('Titel', max_length = 200) content = models .TextField ('Innehåll') slug = models.SlugField ('Slug') view_count = models.IntegerField ("Visa antal", standard = 0) def __str __ (själv): returnera "% s" av% s '% self.title, self.author)

Som alltid, när vi ändrar en modell, behöver vi migrera databasen:

$ ./manage.py makemigrations $ ./manage.py migrera

Låt oss också ändra view_post Django visa för att räkna vyer:

def view_post (request, slug): försök: post = Post.objects.get (slug = slug) utom Post.DoesNotExist: höja Http404 ("Poll existerar inte") post.view_count + = 1 post.save () return render (förfrågan, "post.html", context = 'post': post)

Det skulle vara användbart att visa antal visningar i mallen. Lägg till detta 

Visade post.view_count gånger

 någonstans inne i utgivare / mallar / post.html fil. Gör några synpunkter på ett inlägg nu och se hur räknaren ökar.

Låt oss skapa en Selleriuppgift. Eftersom det handlar om inlägg, kommer jag att lägga in det utgivare / tasks.py:

från django.template import Mall, Context from django.core.mail import send_mail från django.contrib.auth import get_user_model från quick_publisher.celery import app från publisher.models import Post REPORT_TEMPLATE = "" "Så här har du gjort nu: % för inlägg i inlägg% "post.title": visade post.view_count gånger | % endfor% "" "@ app.task def send_view_count_report (): för användare i get_user_model (). objects.all (): posts = Post.objects.filter (författare = användare) om inte inlägg: fortsätt mall = Mall (REPORT_TEMPLATE) send_mail ('Din QuickPublisher Activity', template.render (context = Context ('posts' inlägg)), "[email protected]", [user.email], fail_silently = False,)

Varje gång du ändrar Celery-uppgifterna, kom ihåg att starta om Celery-processen. Selleri måste upptäcka och ladda om uppgifter. Innan du skapar en periodisk uppgift borde vi testa det här ut i Django-skalet för att se till att allt fungerar som tänkt:

$ ./manage.py skal i [1]: från publisher.tasks importera send_view_count_report I [2]: send_view_count_report.delay ()

Förhoppningsvis fick du en snygg liten rapport i din email. 

Låt oss nu skapa en periodisk uppgift. Öppna quick_publisher / celery.py och registrera de periodiska uppgifterna:

# quick_publisher / celery.py import os från selleri import Selleri från selleri.scheman importerar crontab os.environ.setdefault ('DJANGO_SETTINGS_MODULE', 'quick_publisher.settings') app = Selleri ('quick_publisher') app.config_from_object ('django.conf : inställningar ") # Ladda upp uppgiftsmoduler från alla registrerade Django app konfigs. app.autodiscover_tasks () app.conf.beat_schedule = 'send-report-every-single-minute': 'uppgift': 'publisher.tasks.send_view_count_report', 'schema': crontab (), # ändra till 'crontab (minut = 0, timme = 0) 'om du vill att den ska köra dagligen vid midnatt,

Hittills skapade vi ett schema som skulle utföra uppgiften publisher.tasks.send_view_count_report varje minut som anges av crontab () notation. Du kan också ange olika Celery Crontab scheman. 

Öppna en annan konsol, aktivera lämplig miljö och starta Celery Beat-tjänsten. 

$ selleri -A quick_publisher beat

Beat tjänstens jobb är att driva uppgifter i Selleri enligt schemat. Tänk på att schemat gör det send_view_count_report Uppgiftskörning varje minut enligt inställningen. Det är bra att testa men inte rekommenderas för en verklig webapplikation.

Att göra uppgifter mer pålitliga

Uppgifter används ofta för att utföra opålitliga operationer, verksamheter som är beroende av externa resurser eller som lätt kan misslyckas på grund av olika skäl. Här är en riktlinje för att göra dem mer tillförlitliga:

  • Gör uppgifter idempotent. En idempotent uppgift är en uppgift som, om den slutar halvvägs, inte förändrar systemets tillstånd på något sätt. Uppgiften gör också fullständiga ändringar i systemet eller ingen alls.
  • Försök igen med uppgifterna. Om uppgiften misslyckas är det en bra idé att prova det igen och igen tills det körs framgångsrikt. Du kan göra detta i Selleri med Selleri Retry. En annan intressant sak att titta på är Exponential Backoff-algoritmen. Detta kan vara till nytta när du tänker på att begränsa onödig belastning på servern från omprövade uppgifter.

Slutsatser

Jag hoppas att det här har varit en intressant handledning för dig och en bra introduktion till Celery med Django. 

Här är några slutsatser vi kan dra:

  • Det är bra att hålla opålitliga och tidskrävande uppgifter utanför begäranstid.
  • Långlöpande uppgifter ska utföras i bakgrunden genom arbetstagarprocesser (eller andra paradigmer).
  • Bakgrundsuppgifter kan användas för olika uppgifter som inte är kritiska för applikationens grundläggande funktion.
  • Selleri kan också hantera periodiska uppgifter med hjälp av selleri beat service.
  • Uppgifter kan vara mer tillförlitliga om de gjordes idempotenta och försökte igen (kanske med hjälp av exponentiell backoff).