cucumberian / tutorial_celery

celery

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Asynchronous Tasks With Django and Celery

  1. https://habr.com/ru/companies/otus/articles/503380/ (https://github.com/testdrivenio/django-celery)
  2. https://realpython.com/asynchronous-tasks-with-django-and-celery/

Celery это отдельная очередь задач, которая может собирать, получать, планировать и выполнять задачи вне основной программы. Чтобы получить и отдавать готовые задачи celery нужен брокер сообщений для коммуникации. Обычно вместе с Celery используется Redis и RabbitMQ.

  • Celery workers - это рабочие процессы, котрые выполняют задачи независимо вне основной программы.
  • Celery beat - планировщик, который определяет когда запускать задачи.

Установка

pip3 install django
...
pip3 install celery
pip3 install redis

Теперь можно запустить worker командой celery worker. Но получим сообщение об ошибке, что celery не может работать с брокером сообщений. Celery будет безуспешно пытаться подключиться к локальному хосту по протоколу amqp - advanced message queuing protocol (https://en.wikipedia.org/wiki/Advanced_Message_Queuing_Protocol).

Redis

Установим redis-server

sudo apt update
sudo apt install redis

Конечно можно ставить отдельно в виде докер-контейнера.

Можно запустить redis-server

redis-server

Проверить работает ли redis

ps aux | grep redis

Остановить

sudo service redis-server stop

Пингануть

redis-cli ping

Запустить клиент

redis-cli

Установим питоновский клиент

pip install redis
pip install celery
pip install flower

Т.е. нам нужен редис сервер, как отдельное приложение и пакет для питоновских программ для работы с ним.

Добавление celery у джанго проекту

Создадим файл celery.py в папке корневого приложенияЮ рядом c settings.py.

# django_celery/celery.py
import os
from celery import Celery

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_celery.settings")

app = Celery("django_celery")
# app = Celery("hello", backend="redis://localhost:6379", broker="pyamqp://quest@127.0.0.1:6379/")
app.config_from_object("django.conf.settings", namespace="CELERY")
app.autodiscover_tasks()


@app.task
def add(x, y):
    return x + y

Здесь мы устанавливаем переменную окружения, чтобы получить модуль джанго project_name.settings.py через переменную окружения DJANGO_SETTINGS_MODULE.

Затем создаём экземпляр приложения Celery и передаём внутрь имя нашего приложения (главного модуля).

Далее мы задаем путь до файла настроек и имя неймспейса с настройками celery. В файле конфигурации settings.py все настройки начинающиеся с CELERY_ будут прочтены этим приложением. При желании можно определить и другой файл конфигурации.

Через автодисковер мы говорим приложению celery искать задачи в каждом приложении джанго.

Далее добавляем настройки для celery в settings.py:

# settings.py

# Celery settings
CELERY_BROKER_URL = "redis://localhost:6379/0"
CELERY_RESULT_BACKEND = "redis://localhost:6379/0"

Данные строки дают инстансу celery достаточно информации, чтобы понять куда отправлять сообщения и куда записывать результат. Заметим, что начинаются эти строки на имя CELERY_, где название CELERY задается как namespace в файле celery.py в строке app.config_from_object("django.conf:settings", namespace="CELERY").

Добавим celery.app в загрузку модуля через файл main_app/__init__.py:

# __init__.py

from .celery import app as celery_app

__all__ = ("celery_app", )

запуск приложения celery_app при запуске джанго будет гарантировать нам, что декоратор @shared_task будет использовать его корректно.

Теперь можно протестировать приложение. Напомним что наша связка с celery-django будет состоять из трёх модулей:

  • producer - приложение джанго
  • message-broker - сервер редис
  • consumer - приложение celery_app в джанго

Запуск

  • Запускаем сервер redis redis-server если еще не запущен как сервис или в докере
  • запускаем джанго python manage.py runserver
  • запускаем воркер python -m celery -A django_celery worker --loglevel=INFO При запуске воркера передаём celery имя нашего джанго модуля в котором есть инстанс Celery.
    • -A = --app=
    • -l = --loglevel=
    • -b = --broker= Можно явно указать инстанс Celery:
    python -m celery --app=django_celery:celery_app worker --loglevel=INFO
  • запускаем flower на порту 5555
python -m celery --broker=redis://127.0.0.1:6379/0 flower -A django_celery --port=5555

или если подтягиваем настройки для фловера из аппы

python -m celery -A django_celery flower --port=5555

Использование

Для использование надо в тексте программы добавить задачу для воркера

task = add.delay(1, 2)

, где add - функция задекорированная @celery_app.task - специальным декоратором от инстанса Celery, который сы создали в celery.py.

Получить статус задачи и результат выполнения:

print(task.result)
print(task.status)

В случае если работа ведётся с базой данных, например создаваться объект, сохраняется и потом добавляется задача, то может случиться, что воркер получит задачу и начнёт её выполнять, а значения в базе данных ещё не будет. Тогда лучше запускать воркер после транзакции в базу данных, например так:

from django.db import transaction

transaction.on_commit(lambda: some_celery_task.delay(obj.id))

По самому celery. Уже много раз обсуждалось и везде предупреждают, но повторю — не используйте в качестве аргументов для тасков сложные объекты, например модели django. Передавайте лучше id и уже в таске получайте объект из БД. Ещё один важный момент, который может смутить начинающего разработчика на django — вьюхи, как правило, выполняются в транзакции. Это может привести к тому, сохранив новый объект и сразу отправь его id в таск вы можете получить object not found. Чтобы такого избежать, нужно использовать конструкцию типа transaction.on_commit(lambda: some_celery_task.delay(obj.id))

Так же можно смотреть текущие задачи и их статусы с помощью

python -m celery -A worker events

Django

Вот пример использования в django:

# для запуска после транзакции
from django.db import transaction
# для декорирования задачи
from django_cel.celery import app as celery_app

class SimpleView(View):
    def post(self, request):
        value = request.POST.get("value")
        if value:
            simple = Simple(value=value)
            simple.save()
            # отдаём задачу воркеру после выполнения транзакции,
            # когда объект уже будет создан
            transaction.on_commit(lambda: simple_task.delay(simple.id))
            return JsonResponse({"id": simple.id})
        return JsonResponse({"error": "Value is required"})

# регистрируем задачу для воркера
@celery_app.task
def simple_task(simple_id):
    print(f"Simple task started with id {simple_id}")
    simple = Simple.objects.get(id=simple_id)
    simple.result = len(Simple.objects.all())
    simple.is_completed = True
    simple.save()
    print(f"Simple task finished with id {simple_id}")
    return simple_id

Если celery не может найти задачу (ошибка Not registered), то просто перезапустите celery.

Конструкции celery

from celery import Celery
from celery import shared_task
...
celery_app = Celery(
    "project-name",
    broker="redis://localhost:6379/0",  # можно задать потом
    backend="redis://localhost:6379/0", # можно задать потом    
)
celery_app.autodiscover_tasks(force=True)
...

# регистрация таски
@celery_app.task
def add(x, y):
    return x + y

# регистрация shared_task
@shared_task
def sub(x, y):
    return x - y

task_sub = sub.delay(2, 1)

task = add.delay(1, 2)
while not task.ready():
    pass
print(task.get())

@shared_task vs @app.task

https://docs.celeryq.dev/en/stable/userguide/tasks.html При использовании shared_task нет необходимости икспортировать экземпляр Celery. Также можно использовать @app.task(shared=True). В случае если есть несколько экземпляров Celery

app1 = Celery()

app2 = Celery()

@app1.task
def test():
    pass

, то таска test будет зарегистрирована в обоих иснтансах Celery, но имя test будет относиться только к app1. Однако @shared_task позволяет использовать таск в обоих инстансах.

bind=True

Такие задачи первым аргументом принимают себя и позволяют повторять себя при необходимости повторов.

logger = get_task_logger(__name__)

@app.task(bind=True)
def add(self, x, y):
    logger.info(self.request.id)

Bound tasks are needed for retries (using app.Task.retry()), for accessing information about the current task request, and for any additional functionality you add to custom task base classes.

delay

task.delay() - это метод который является псевдонимом более мощного метода .apply_async(), у которого есть опции выполнения.

@shared_task
def add_task(x, y):
    return x + y

add_task.apply_async(
    args=[1, 2]
)

Хотя для многих простых случаев использование delay является предпочтительнее, использование метода apply_async иногда оправдано, например со счётчиками или повторами.

tasks.py

Можно задать выполняемые задачи в файле tasks.py.

#tasks.py
from celery import shared_task


@shared_task
def hello():
    print("Hello Celery")

@shared_task
def add(x, y):
    return x + y

docker-compose

version: "3"

services:
  redis:
    image: redis:7.2.4-alpine3.19
    # ports:
    #   - 6379:6379
  
  postgres:
    image: postgres:13.14-alpine3.19
    restart: always
    # ports:
    #   - "5444:5432"
    env_file: .env
    environment:
      - POSTGRES_USER=$POSTGRES_USER
      - POSTGRES_PASSWORD=$POSTGRES_PASSWORD
      - POSTGRES_DB=$POSTGRES_DB
    volumes:
      - postgresql_volume:/var/lib/postgresql/data

  django:
    build:
      context: celery2
      dockerfile: Dockerfile
    command: sh -c "python manage.py makemigrations && python manage.py migrate --noinput && python manage.py runserver 0.0.0.0:8000"
    ports:
      - 8080:8000
    env_file: .env
    environment:
      - DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY}
      - CELERY_BROKER_URL=redis://redis:6379/0
      - CELERY_RESULT_BACKEND=redis://redis:6379/0
      - POSTGRES_HOST=postgres
      - POSTGRES_PORT=5432
    depends_on:
      - redis
      - postgres

    
  worker:
    build:
      context: celery2
      dockerfile: Dockerfile
    command: python -m celery -A celery2:celery_app worker
    env_file: .env
    environment:
      - DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY}
      - CELERY_BROKER_URL=redis://redis:6379/0
      - CELERY_RESULT_BACKEND=redis://redis:6379/0
      - POSTGRES_HOST=postgres
      - POSTGRES_PORT=5432
    depends_on:
      - redis
      - postgres
  
  flower:
    build:
      context: celery2
      dockerfile: Dockerfile
    command: python -m celery -A celery2 flower --port=5555
    ports:
      - "5555:5555"
    env_file: .env
    environment:
      - DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY}
      - CELERY_BROKER_URL=redis://redis:6379/0
      - CELERY_RESULT_BACKEND=redis://redis:6379/0
      - POSTGRES_HOST=postgres
      - POSTGRES_PORT=5432
    depends_on:
      - redis
      - postgres



volumes:
  postgresql_volume:
    name: postgresql_volume

Чтобы изменить количество запущенных контейнеров с воркерами можно воспользоваться командой

docker-compose up -d --build --scale worker=3

Celery для произвольного проекта

Задача - запустить асинхронный расчет хэша от строки. Т.к. задача асинхронная, то выполнять её можно в отдельном процессе. А в главной программе мы будем асинхронно ожидать выполнения этого процесса через asyncio.sleep.

  1. Создаем экземпляр celery приложения в файле celery_app.py
import time
import hashlib
from celery import Celery

celery_app = Celery(
    "tasks", broker="redis://localhost:6379/0", backend="redis://localhost:6379/0"
)

celery_app.conf.broker_url = Config.CELERY_BROKER_URL
celery_app.conf.result_backend = Config.CELERY_RESULT_BACKEND
celery_app.conf.update(result_expires=3600)

@celery_app.task
def calc_hash(string: str) -> str:
    time.sleep(10)
    hash_str = hashlib.sha256(string.encode()).hexdigest()
    return hash_str
  1. Используем эту задачу в коде:
import asyncio
from celery_app import celery_hash

async def calc_hash(string: str) -> str:
  """
  Асинхронный расчёт хэша в отдельном процессе Celery воркера
  """
  task = celery_hash.delay(string=string)
  while not task.ready():
    asyncio.sleep(0.1)
  return task.result
  1. Запускаем воркера
celery -A celery_app worker --loglevel=INFO
  1. Запускаем наше приложение

About

celery


Languages

Language:Python 88.1%Language:HTML 10.3%Language:Dockerfile 1.6%