Utilizarea de țelină cu Django pentru prelucrarea sarcinilor de fundal

Aplicațiile Web încep de obicei simplu, dar pot deveni destul de complexe, iar majoritatea dintre ele depășesc rapid responsabilitatea de a răspunde numai la cererile HTTP.

Când se întâmplă acest lucru, trebuie făcut o distincție între ceea ce trebuie să se întâmple instantaneu (de obicei în ciclul de viață al cererii HTTP) și ce se poate întâmpla în cele din urmă. De ce este asta? Ei bine, pentru că atunci când aplicația dvs. devine supraîncărcată cu trafic, lucruri simple ca asta fac diferența. 

Operațiile dintr-o aplicație web pot fi clasificate ca operațiuni critice sau care necesită timp și sarcini de fundal, cele care se petrec în afara timpului de solicitare. Aceste hărți sunt cele descrise mai sus: 

  • trebuie să se întâmple instantaneu: operațiuni de cerere-timp
  • trebuie să se întâmple în cele din urmă: sarcini de bază

Operațiunile de timp de solicitare pot fi efectuate pe un singur ciclu de solicitare / răspuns, fără a vă face griji că operațiunea se va opri sau că utilizatorul ar putea avea o experiență defectuoasă. Exemplele obișnuite includ operațiile de baze de date CRUD (Creare, citire, actualizare, ștergere) și gestionarea utilizatorilor (rutine de conectare / deconectare).

Sarcinile de fundal sunt diferite, deoarece acestea sunt, de obicei, destul de consumatoare de timp și sunt predispuse la eșec, în mare parte datorită dependențelor externe. Unele scenarii comune printre aplicațiile web complexe includ:

  • trimiterea de e-mailuri de confirmare sau activitate
  • călătorind zilnic și căutând câteva informații din diverse surse și stocându-le
  • efectuarea analizei datelor
  • ștergerea resurselor care nu sunt necesare
  • exportând documente / fotografii în diferite formate

Obiectivele principale sunt principalul obiectiv al acestui tutorial. Cel mai obișnuit model de programare folosit pentru acest scenariu este Arhitectura consumatorilor de produse. 

În termeni simpli, această arhitectură poate fi descrisă astfel: 

  • Producătorii creează date sau sarcini.
  • Sarcinile sunt plasate într-o coadă care este denumită coada de sarcini. 
  • Consumatorii sunt responsabili pentru consumarea datelor sau executarea sarcinilor. 

În mod obișnuit, consumatorii preiau sarcini din coadă într-o primă clasă (FIFO) sau în funcție de prioritățile lor. Consumatorii sunt, de asemenea, numiți muncitori, și acesta este termenul pe care îl vom folosi în întregime, deoarece este în concordanță cu terminologia utilizată de tehnologiile discutate.

Ce fel de sarcini pot fi procesate în fundal? Sarcini care:

  • nu sunt esențiale pentru funcționalitatea de bază a aplicației web
  • nu pot fi executate în ciclul cererii / răspunsului deoarece acestea sunt lente (I / O intensive etc.)
  • depinde de resursele externe care ar putea să nu fie disponibile sau să nu se comporte așa cum era de așteptat
  • poate fi necesar să fie retrimis cel puțin o dată
  • trebuie să fie executate în timp util

Țelina este alegerea de facto pentru a face procesarea sarcinilor de fond în ecosistemul Python / Django. Are un API simplu și clar și se integrează frumos cu Django. Acesta susține diverse tehnologii pentru coada de sarcini și diverse paradigme pentru lucrători.

În acest tutorial, vom crea o aplicație web pentru jucării Django (care se ocupă de scenarii din lumea reală) care utilizează procesarea sarcinilor de fundal.

Setarea lucrurilor în sus

Presupunând că deja cunoașteți managementul pachetelor Python și mediile virtuale, să instalați Django:

$ pip instala Django

Am decis să construiesc o altă aplicație de blogging. Aplicația se va concentra pe simplitate. Un utilizator poate crea un cont simplu și fără prea multă agitație poate crea un post și îl poate publica platformei. 

Configurați quick_publisher Proiectul Django:

$ django-admin startproject rapid_publisher

Să începem aplicația:

$ cd rapid_publisher $ ./manage.py startapp main

Când încep un nou proiect Django, îmi place să creez un principal aplicație care conține, printre altele, un model de utilizator personalizat. De cele mai multe ori, mă întâlnesc cu limitări ale implicit Django Utilizator model. Având un obicei Utilizator modelul ne oferă beneficiul flexibilității.

# main / models.py din modelele de import django.db de la importul django.contrib.auth.models AbstractBaseUser, PermissionsMixin, BaseUserManager clasa UserAccountManager (BaseUserManager): use_in_migrations = True def _create_user (auto, email, parola, ** extra_fields): if nu e-mail: ridicați ValueError ("Adresa de e-mail trebuie furnizată") dacă nu parola: ridicați ValueError ("Parola trebuie furnizată") email = self.normalize_email (email) user.set_password (parola) user.save (folosind = auto._db) returnează user def create_user (auto, email = Niciuna, parola = Niciuna, ** extra_fields): return self._create_user (e-mail, parola, create_superuser (e-mail, parola, ** extra_fields): extra_fields ['is_staff'] = Adevărat extra_fields ['is_superuser'] = Adevărat întoarcere self._create_user (e-mail, parola, ** extra_fields) User (AbstractBaseUser, PermissionsMixin): REQUIRED_FIELDS = [] USERNAME_FIELD = 'email' obiecte = UserAccountManager () email = models.EmailField (e-mail), unic = Adevărat, gol = False, null = False) full_name = models.CharField (' , default = False) este_activ = models.BooleanField ('activ', default = True) def get_short_name (auto): return self.email def get_full_name (self)

Asigurați-vă că verificați documentația Django dacă nu sunteți familiarizat cu modul în care funcționează modelele personalizate ale utilizatorilor.

Acum trebuie să-i spunem lui Django să utilizeze acest model de utilizator în locul celui implicit. Adăugați această linie la quick_publisher / settings.py fişier:

AUTH_USER_MODEL = 'main.User' 

Trebuie, de asemenea, să adăugăm principal aplicație la INSTALLED_APPS listă în quick_publisher / settings.py fişier. Acum putem crea migrațiile, le putem aplica și putem crea un superuser pentru a putea intra în panoul de administrare Django:

$ ./manage.py makemigrations principal $ ./manage.py migrează $ ./manage.py createsuperuser

Să creați acum o aplicație separată Django responsabilă de postări:

$ ./manage.py startapp publicați

Să definim un model Post simplu în editor / models.py:

din modelele de import django.db din fusul orar de import django.utils de la django.contrib.auth importul get_user_model class Post (models.Model): author = models.ForeignKey (get_user_model ()) create = models.DateTimeField (' = timezone.now) title = models.CharField ('Titlu', max_length = 200) content = models.TextField ('Conținut') slug = models.SlugField ('Slug') def __str __ (self) "de% s '% (auto.titlu, auto.author)

Încărcarea Post modelul cu adminul Django se face în editor / admin.py fișier ca acesta:

de la importul django.contrib admin de la .models import Post @ admin.register (Post) class PostAdmin (admin.ModelAdmin): treci

În cele din urmă, hai să-l prindem editor aplicație cu proiectul nostru prin adăugarea acestuia la INSTALLED_APPS listă.

Putem rula acum serverul și să ne îndreptăm spre http: // localhost: 8000 / admin / și să creați primele noastre posturi, astfel încât să avem de jucat:

$ ./manage.py runserver

Am încredere că ți-ai făcut temele și că ai creat postările. 

Sa trecem peste. Următorul pas evident este de a crea o modalitate de a vizualiza postările publicate. 

# editor / views.py din importul django.http Http404 de la django.shortcuts importa renderul din importul .models Post post view_post (request, slug): try: post = Post.objects.get (slug = slug) cu excepția Post.DoesNotExist: ridicați Http404 ("Sondaj nu există") retur render (cerere, 'post.html', context = 'post': post)

Să asociem noua noastră viziune cu o adresă URL în: quick_publisher / urls.py

# rapid_publisher / urls.py de la django.conf.urls import url din django.contrib import admin din editor.views import view_post urlpatterns = [url (r '^ admin /', admin.site.urls), url (r '^ (? P[a-zA-Z0-9 \ "] +) ', view_post, name =" view_post ")]

În cele din urmă, să creăm șablonul care redă postarea în: editor / template-uri / post.html

       

post.title

post.content

Publicat de post.author.full_name pe post.created

Acum ne putem îndrepta către http: // localhost: 8000 / the-slug-of-the-post-ați creat / în browser. Nu este chiar un miracol al designului web, dar realizarea unor posturi de calitate este dincolo de sfera acestui tutorial.

Trimiterea de e-mailuri de confirmare

Iată scenariul clasic:

  • Creați un cont pe o platformă.
  • Furnizați o adresă de e-mail care să fie identificată în mod unic pe platformă.
  • Platforma verifică că sunteți într-adevăr proprietarul adresei de e-mail prin trimiterea unui e-mail cu un link de confirmare.
  • Până când efectuați verificarea, nu puteți să utilizați (pe deplin) platforma.

Să adăugăm un is_verified steag și verification_uuid pe Utilizator model:

# principale / models.py import uuid class Utilizator (AbstractBaseUser, PermissionsMixin): REQUIRED_FIELDS = [] USERNAME_FIELD = 'email' obiecte = UserAccountManager () email = models.EmailField False) full_name = models.CharField ('nume complet', blanc = True, null = True, max_length = 400) is_staff = models.BooleanField (' implicit = Adevărat) is_verified = models.BooleanField ('verificat', implicit = False) # Adăugați pavilonul 'is_verified' verification_uuid = models.UUIDField (default UUID) auto.email def get_full_name (auto): return self.email def __unicode __ (self): return self.email

Să utilizăm această ocazie pentru a adăuga modelul utilizatorului la admin:

din administrarea importului django.contrib admin din .models import User @ admin.register (User) clasa UserAdmin (admin.ModelAdmin): pass

Să transformăm modificările în baza de date:

$ ./manage.py makemigrations $ ./manage.py migrați

Acum trebuie să scriem o bucată de cod care trimite un e-mail când este creată o instanță de utilizator. Acestea sunt semnalele Django și este o ocazie perfectă pentru a atinge acest subiect. 

Semnalele sunt declanșate înainte / după apariția anumitor evenimente în aplicație. Putem defini funcții de apel invers care sunt declanșate automat când semnalele sunt declanșate. Pentru a face un trigger callback, trebuie mai întâi să îl conectăm la un semnal.

Vom crea un apel invers care va fi declanșat după ce a fost creat un model de utilizator. Vom adăuga acest cod după Utilizator definiția modelului în: principal / models.py

de la django.db.models semnale de import de la django.core.mail import send_mail def user_post_save (expeditor, instanță, semnal, * args, ** kwargs): dacă nu instance.is_verified: # Trimite e-mail de verificare send_mail ('Verificați contul QuickPublisher ',' Urmați acest link pentru a vă verifica contul: 'http: // localhost: 8000% s'% reverse ('verificare', kwargs = 'uuid': str (instance.verification_uuid)). dev ', [instance.email], fail_silently = False,) signals.post_save.connect (user_post_save, sender = Utilizator)

Ce am făcut aici este că am definit a user_post_save funcția și conectat la post_save semnal (unul care este declanșat după ce un model a fost salvat) trimis de către Utilizator model.

Django nu trimite doar e-mailuri pe cont propriu; trebuie să fie legat de un serviciu de e-mail. Din motive de simplitate, puteți adăuga acreditările dvs. Gmail quick_publisher / settings.py, sau puteți adăuga furnizorul dvs. de e-mail preferat. 

Iată cum arată configurația Gmail:

EMAIL_USE_TLS = Adevărat EMAIL_HOST = 'smtp.gmail.com' EMAIL_HOST_USER = '@ gmail.com 'EMAIL_HOST_PASSWORD =''EMAIL_PORT = 587

Pentru a testa lucrurile, accesați panoul de administrare și creați un utilizator nou cu o adresă de e-mail validă pe care o puteți verifica rapid. Dacă totul a mers bine, veți primi un e-mail cu un link de verificare. Rutina de verificare nu este încă pregătită. 

Iată cum să verificați contul:

# main / views.py din importul django.http Http404 de la django.shortcuts importa render, redirecționează din importul .models User def home (cerere): retur render (request, 'home.html') def verifică (request, uuid): încercați: user = User.objects.get (verification_uuid = uuid, is_verified = False) cu excepția User.DoesNotExist: ridicați Http404 ("Utilizatorul nu există sau este deja verificat") user.is_verified = 'Acasă')

Cuplați vederile în: quick_publisher / urls.py

# rapid_publisher / urls.py de la django.conf.urls import url din django.contrib import admin de la publisher.views import view_post de la main.views home import, verifica urlpatterns = [url (r '^ $', home, name = " acasă "), url (r '^ admin /', admin.site.urls), url (r '^ verifică /[a-z0-9 \ -] +) / ', verificați, name =' verify '), url (r' ^[a-zA-Z0-9 \ "] +) ', view_post, name =" view_post ")]

De asemenea, nu uitați să creați o home.html fișier în conformitate cu principal / template-uri / home.html. Acesta va fi oferit de către Acasă vedere.

Încercați să rulați întregul scenariu din nou. Dacă totul este bine, veți primi un e-mail cu o adresă URL de verificare validă. Dacă veți urma adresa URL și apoi veți verifica administratorul, puteți vedea cum a fost confirmat contul.

Trimiterea de e-mailuri asincron

Iată problema cu ceea ce am făcut până acum. S-ar putea să fi observat că crearea unui utilizator este cam încet. Asta pentru că Django trimite e-mailul de verificare în timpul solicitării. 

Acesta este modul în care funcționează: trimitem datele utilizatorului în aplicația Django. Aplicația creează o Utilizator și apoi creează o conexiune la Gmail (sau la alt serviciu pe care l-ați selectat). Django așteaptă răspunsul și numai atunci returnează un răspuns browserului nostru. 

Aici este locul în care Țelina intră. Mai întâi, asigurați-vă că este instalat:

$ pip instalați Țelina

Acum trebuie să creăm o aplicație pentru țelină în aplicația noastră Django:

# quick_publisher / celery.py import os din importul de țelină import Celery os.environ.setdefault ('DJANGO_SETTINGS_MODULE', 'quick_publisher.settings') app = Țelar ('quick_publisher') app.config_from_object ('django.conf: module de lucru de la toate configurările de aplicații Django înregistrate. app.autodiscover_tasks ()

Țelina este o coadă de sarcini. El primește sarcini de la aplicația noastră Django și le va rula în fundal. Țelina trebuie să fie asociată cu alte servicii care acționează ca brokeri. 

Brokerii intermediază trimiterea de mesaje între aplicația web și Celery. În acest tutorial, vom folosi Redis. Redis este ușor de instalat și putem începe cu ușurință fără prea multă agitație.

Puteți instala Redis urmând instrucțiunile de pe pagina Redis Quick Start. Va trebui să instalați biblioteca Redis Python, pip install redis, și pachetul necesar pentru utilizarea Redis și Țelină: pip instalare telina [redis].

Porniți serverul Redis într-o consolă separată, după cum urmează: $ redis-server

Să adăugăm configurările legate de Celery / Redis quick_publisher / settings.py:

Setările legate de REDIS 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 '

Înainte ca orice să poată fi rulat în Țelină, trebuie declarat ca o sarcină. 

Iată cum se face acest lucru:

# main / tasks.py de logare de import de la django.urls import inversă de la django.core.mail import send_mail de la django.contrib.auth import get_user_model de la quick_publisher.celery import app @ app.task def send_verification_email (user_id): UserModel = get_user_model ( ) încercați: user = UserModel.objects.get (pk = user_id) send_mail ('Verificați contul dvs. QuickPublisher', 'Urmați acest link pentru a vă verifica contul: http: // localhost: 8000% , kwargs = 'uuid': str (user.verification_uuid)), '[email protected]', [user.email], fail_silently = False, exceptând UserModel.DoesNotExist: logging.warning mail la utilizatorul non-existent '% s' "% user_id)

Ceea ce am făcut aici este acesta: am mutat funcția de trimitere a e-mailului de verificare într-un alt fișier numit tasks.py

Câteva note:

  • Numele fișierului este important. Țelina trece prin toate aplicațiile din INSTALLED_APPS și înregistrează sarcinile în tasks.py fișiere.
  • Observați cum am decorat-o Trimite emailul de verificare funcția cu @ app.task. Acest lucru spune lui Celery că aceasta este o sarcină care va fi rulată în coada de sarcini.
  • Observați cum ne așteptăm ca argument numele de utilizator mai degrabă decât a Utilizator obiect. Acest lucru se datorează faptului că s-ar putea să avem probleme în serializarea obiectelor complexe atunci când trimitem sarcinile către Țelina. Cel mai bine este să le păstrați simplu.

Revenind la principal / models.py, codul semnalului se transformă în:

din django.db.models semnale de import din main.tasks import send_verification_email def user_post_save (expeditor, instanță, semnal, * args, ** kwargs): dacă nu instance.is_verified: # Trimite e-mail de verificare send_verification_email.delay (instance.pk) semnale .post_save.connect (user_post_save, sender = Utilizator)

Observați cum îl numim .întârziere pe obiectul de activitate. Asta inseamna ca trimitem sarcina catre Celery si nu asteptam rezultatul. Dacă am fi folosit send_verification_email (instance.pk) în schimb, l-am fi trimis în continuare la Țelie, dar am fi așteptat ca sarcina să termine, ceea ce nu este ceea ce vrem.

Înainte de a începe să creați un utilizator nou, există o captură. Țelina este un serviciu și trebuie să-l pornim. Deschideți o nouă consolă, asigurați-vă că activați virtualenv-ul corespunzător și navigați la dosarul proiectului.

$ lucrătoare de telina -A quick_publisher --loglevel = debug --concurrency = 4

Aceasta începe cu patru lucrători din procesele de țelină. Da, acum puteți merge în sfârșit și puteți crea un alt utilizator. Observați cum nu există întârziere și asigurați-vă că urmăriți jurnalele din consola Celery și vedeți dacă sarcinile sunt executate corect. Ar trebui să arate ceva de genul:

[2017-04-28 15: 00: 09,190: DEBUG / MainProcess] Sarcina acceptată: main.tasks.send_verification_email [f1f41e1f-ca39-43d2-a37d-9de085dc99de] pid: 62065 [2017-04-28 15: 00: 11.740: INFO / PoolWorker-2] Task main.tasks.send_verification_email [f1f41e1f-ca39-43d2-a37d-9de085dc99de] a reușit în 2.5500912349671125s: Niciuna

Sarcinile periodice cu țelină

Iată un alt scenariu comun. Cele mai multe aplicații web mature trimit utilizatorii lor e-mailuri de ciclu de viață pentru a le menține angajați. Unele exemple comune de e-mailuri pe durata ciclului de viață:

  • rapoarte lunare
  • notificări de activitate (plăceri, solicitări de prietenie etc.)
  • mementouri pentru a realiza anumite acțiuni ("Nu uitați să vă activați contul")

Iată ce vom face în aplicația noastră. Vom număra de câte ori a fost vizionată fiecare postare și vom trimite un raport zilnic autorului. Odată în fiecare zi, vom trece prin toți utilizatorii, vom prelua postările și vom trimite un e-mail cu un tabel care conține postările și numărul de vizionări.

Să schimbăm Post astfel încât să putem acomoda scenariul privind numărul de vizualizări.

Clasa Post (modele.Model): autor = models.ForeignKey (User) creat = models.DateTimeField ('Creat data', default = timezone.now) title = models.CharField ('Titlu', max_length = .TextField ("Conținut") slug = models.SlugField ("Slug") view_count = models.IntegerField ("Count Count", default = 0) def __str __ (self) auto.title, auto.author)

Ca întotdeauna, atunci când schimbăm un model, trebuie să migrăm baza de date:

$ ./manage.py makemigrations $ ./manage.py migrați

Să modificăm de asemenea view_post Vizualizarea Django pentru a număra vizionările:

def_post (request, slug): încercați: post = Post.objects.get (slug = slug) cu excepția Post.DoesNotExist: ridicați Http404 ("Sondaj nu există") post.view_count + = 1 post.save (solicitare, 'post.html', context = 'post': post)

Ar fi util să afișați VIEW_COUNT în șablon. Adaugă asta 

A văzut post.view_count ori

 undeva înăuntru editor / template-uri / post.html fişier. Faceți câteva vizionări pe un post acum și vedeți cum crește numărul contorului.

Să creăm o sarcină de țelină. Deoarece este vorba despre posturi, o să o introduc editor / tasks.py:

de la importul django.template Template, Context din django.core.mail importul send_mail din django.contrib.auth importul get_user_model de la quick_publisher.celery importul de la import publisher.models Post REPORT_TEMPLATE = "" "Iată cum ați făcut până acum: % pentru post în postări% "post.title": vizualizat post.view_count ori | % endfor% "" "@ app.task def send_view_count_report (): pentru utilizator în get_user_model (). objects.all (): posts = Post.objects.filter (author = user) dacă nu postări: continue template = Template (REPORT_TEMPLATE) send_mail (' mesaje)), "de [email protected]", [user.email], fail_silently = False,)

De fiecare dată când efectuați modificări asupra sarcinilor de țelină, nu uitați să reporniți procesul de Țelină. Țelina trebuie să descopere și să reîncarce sarcinile. Înainte de a crea o sarcină periodică, trebuie să testați acest lucru în carcasa Django pentru a vă asigura că totul funcționează conform destinației:

$ ./manage.py shell În [1]: de la publisher.tasks importați send_view_count_report În [2]: send_view_count_report.delay ()

Sperăm că ați primit un raport minuțios în e-mail. 

Să creați acum o sarcină periodică. Deschide quick_publisher / celery.py și să înregistreze sarcinile periodice:

# quick_publisher / celery.py import os din telina Importul de telina din celery.schedules import crontab os.environ.setdefault ('DJANGO_SETTINGS_MODULE', 'quick_publisher.settings') app = : settings ') # Încărcați module de activitate din toate configurările de aplicații Django înregistrate. app.autodiscover_tasks () app.conf.beat_schedule = 'trimite-report-fiecare-un minut': 'task': 'publisher.tasks.send_view_count_report', 'schedule': crontab (minute = 0, oră = 0) 'dacă doriți să fie difuzat zilnic la miezul nopții,

Până acum, am creat un program care să conducă sarcina publisher.tasks.send_view_count_report fiecare minut indicat de crontab () notaţie. De asemenea, puteți specifica diferitele programe de tip Celery Crontab. 

Deschideți o altă consolă, activați mediul adecvat și porniți serviciul Celery Beat. 

$ țelină -Un rapid_publisher beat

Slujba serviciului Beat este de a împinge sarcini în țelină în conformitate cu programul. Luați în considerare faptul că programul face send_view_count_report sarcina execută fiecare minut în funcție de configurare. Este bine pentru testare, dar nu este recomandat pentru o aplicație web din lumea reală.

Efectuarea sarcinilor mai fiabile

Sarcini sunt adesea folosite pentru a efectua operațiuni nesigure, operațiuni care depind de resurse externe sau care pot eșua cu ușurință din diferite motive. Iată o orientare pentru a le face mai fiabile:

  • Faceți sarcinile idempotent. O sarcină idempotent este o sarcină care, dacă este oprită la jumătatea drumului, nu schimbă starea sistemului în nici un fel. Sarcina face fie modificări complete ale sistemului, fie nici una.
  • Reîncercați sarcinile. Dacă sarcina nu reușește, este o idee bună să o încercați din nou și din nou până când se execută cu succes. Puteți face acest lucru în țelină cu țelină Retry. Un alt lucru interesant de analizat este algoritmul Exponential Backoff. Acest lucru ar putea fi util când vă gândiți să limitați încărcarea inutilă a serverului de la sarcini retrimise.

concluzii

Sper că acest lucru a fost un tutorial interesant pentru dvs. și o bună introducere în utilizarea de țelină cu Django. 

Iată câteva concluzii pe care le putem trage:

  • Este o bună practică să țineți sarcini nesigure și consumatoare de timp în afara timpului de solicitare.
  • Activitățile pe termen lung trebuie executate în fundal de către procesele lucrătorilor (sau alte paradigme).
  • Activitățile de bază pot fi utilizate pentru diferite sarcini care nu sunt critice pentru funcționarea de bază a aplicației.
  • Țelina poate, de asemenea, să se ocupe de sarcini periodice folosind țelină beat serviciu.
  • Sarcinile pot fi mai fiabile dacă sunt făcute idempotent și retrimise (poate folosind backoff exponențial).
Cod