Django Dramatiq

repository·master·Indexed 18 days ago

https://github.com/bogdanp/django_dramatiq

A seamless integration of the Dramatiq task queue with Django. It provides task discovery, the rundramatiq management command for starting workers, and Django Admin integration for monitoring tasks via AdminMiddleware. Supports RabbitMQ and Redis brokers, result backends, and includes testing utilities for pytest and unittest.

Tokens
3.3K
Snippets
11
Records
14
Agent score
62%

What's inside django_dramatiq

  1. Declare and Auto-discover Tasks

    master

    Django Dramatiq automatically discovers tasks located in tasks modules within your installed apps (e.g., customers.tasks).

    To change the module names used for discovery, set DRAMATIQ_AUTODISCOVER_MODULES in your settings.

    To prevent specific modules from being processed as tasks, use DRAMATIQ_IGNORED_MODULES with support for wildcards.

    import dramatiq
    from django.core.mail import send_mail
    from .models import Customer
    
    @dramatiq.actor
    def email_customer(customer_id, subject, message):
        customer = Customer.get(pk=customer_id)
        send_mail(subject, message, "webmaster@example.com", [customer.email])
  2. Install Django Dramatiq

    master

    To install Django Dramatiq, you must install the package along with Dramatiq and a broker (RabbitMQ or Redis).

    For RabbitMQ:

    pip install django-dramatiq 'dramatiq[rabbitmq]'

    For Redis:

    pip install django-dramatiq 'dramatiq[redis]'

    To include watch support:

    pip install django-dramatiq 'dramatiq[rabbitmq, watch]'

    Important: Add django_dramatiq to your INSTALLED_APPS before any of your custom apps.

    INSTALLED_APPS = [
        "django_dramatiq",
    
        "myprojectapp1",
        "myprojectapp2",
        # etc...
    ]
  3. Test Dramatiq Tasks with unittest

    master

    Use django_dramatiq.test.DramatiqTestCase for standard unittest suites. This class inherits from django.test.TransactionTestCase and automatically sets up the broker and worker as attributes on the test case.

    from django.core import mail
    from django.test import override_settings
    from django_dramatiq.test import DramatiqTestCase
    
    
    class CustomerTestCase(DramatiqTestCase):
    
        @override_settings(EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend')
        def test_customers_can_be_emailed(self):
            customer = Customer(email="jim@gcpd.gov")
            # Assuming "send_welcome_email" enqueues an "email_customer" task
            customer.send_welcome_email()
    
            # Wait for all the tasks to be processed
            self.broker.join(customer.queue_name)
            self.worker.join()
    
            self.assertEqual(len(mail.outbox), 1)
            self.assertEqual(mail.outbox[0].subject, "Welcome Jim!")
  4. Run the basic Django Dramatiq example locally

    master

    To run the basic example application, you need Redis running. Use Docker Compose to start the infrastructure, then run the Django migrations and the development server.

    1. Start Redis using Docker Compose: cd examples/basic && docker compose up -d
    2. Run Django migrations: python manage.py migrate
    3. Start the Django server: python manage.py runserver
    cd examples/basic && docker compose up -d
    python manage.py migrate
    python manage.py runserver
  5. Test Dramatiq Tasks with pytest

    master

    For testing, use dramatiq.brokers.stub.StubBroker in your test settings.

    When using pytest, create fixtures for the broker and the worker. To ensure the test database state is consistent with the worker (which runs in a separate thread), use the @pytest.mark.django_db(transaction=True) decorator.

    import dramatiq
    import pytest
    
    @pytest.fixture
    def broker():
        broker = dramatiq.get_broker()
        broker.flush_all()
        return broker
    
    @pytest.fixture
    def worker(broker):
        worker = dramatiq.Worker(broker, worker_timeout=100)
        worker.start()
        yield worker
        worker.stop()
    
    def test_customers_can_be_emailed(transactional_db, broker, worker, mailoutbox):
        customer = Customer(email="jim@gcpd.gov")
        customer.send_welcome_email()
    
        # Wait for all the tasks to be processed
        broker.join("default")
        worker.join()
    
        assert len(mailoutbox) == 1
        assert mailoutbox[0].subject == "Welcome Jim!"
  6. Provide custom keyword arguments to Middleware

    master

    If a middleware requires dynamic arguments (e.g., dramatiq.middleware.GroupCallbacks), you can provide them by extending DjangoDramatiqConfig.

    Follow the naming convention: create a @classmethod named middleware_<middleware_name>_kwargs (where <middleware_name> is the lowercase name of the middleware).

    Then, replace the default django_dramatiq app config in INSTALLED_APPS with your custom config class.

    from django_dramatiq.apps import DjangoDramatiqConfig
    
    
    class CustomDjangoDramatiqConfig(DjangoDramatiqConfig):
        @classmethod
        def middleware_groupcallbacks_kwargs(cls):
            return {"rate_limiter_backend": cls.get_rate_limiter_backend()}
  7. Configure a Results Backend

    master

    To store task results, configure DRAMATIQ_RESULT_BACKEND in your settings. This allows you to specify the backend class, its connection options, and middleware options like result_ttl.

    DRAMATIQ_RESULT_BACKEND = {
        "BACKEND": "dramatiq.results.backends.redis.RedisBackend",
        "BACKEND_OPTIONS": {
            "url": "redis://localhost:6379",
        },
        "MIDDLEWARE_OPTIONS": {
            "result_ttl": 1000 * 60 * 10
        }
    }
  8. Configure the Dramatiq Broker and Middleware

    master

    Configure your broker in settings.py using the DRAMATIQ_BROKER dictionary. This dictionary defines the broker class, its options, and the middleware stack.

    When using AdminMiddleware, you can also specify which database to use for persisting Task objects via DRAMATIQ_TASKS_DATABASE (defaults to "default").

    DRAMATIQ_BROKER = {
        "BROKER": "dramatiq.brokers.rabbitmq.RabbitmqBroker", 
        "OPTIONS": {
            "url": "amqp://localhost:5672",
        },
        "MIDDLEWARE": [
            "dramatiq.middleware.prometheus.Prometheus",
            "dramatiq.middleware.AgeLimit",
            "dramatiq.middleware.TimeLimit",
            "dramatiq.middleware.Callbacks",
            "dramatiq.middleware.Retries",
            "django_dramatiq.middleware.DbConnectionsMiddleware",
            "django_dramatiq.middleware.AdminMiddleware",
        ]
    }
    
    DRAMATIQ_TASKS_DATABASE = "default"
  9. Configure task module discovery

    master

    The rundramatiq command automatically discovers task modules by scanning your Django apps. You can control this behavior using the following Django settings:

    • DRAMATIQ_AUTODISCOVER_MODULES: A tuple of module names to look for in each app (e.g., ('tasks', 'worker_tasks')). Defaults to ('tasks',).
    • DRAMATIQ_IGNORED_MODULES: A list of module names or patterns to ignore during discovery.

    The command always includes django_dramatiq.setup in the execution path to ensure the Django integration is initialized.

  10. Reference: Django Dramatiq Middleware

    master

    The following middleware are provided by django_dramatiq:

    • django_dramatiq.middleware.DbConnectionsMiddleware: Vital for closing expired database connections after each message is processed.
    • django_dramatiq.middleware.AdminMiddleware: Stores task metadata in a relational database and exposes it via the Django admin.
  11. Run Dramatiq Workers

    master

    Use the rundramatiq management command to start workers. This command automatically discovers task modules.

    Control Worker Scaling:

    • Environment Variables: DRAMATIQ_NPROCS (processes) and DRAMATIQ_NTHREADS (threads per process).
    • CLI Arguments: -p (processes) and -t (threads). CLI arguments take highest precedence.

    Commands:

    # Default execution
    python manage.py rundramatiq
    
    # Using environment variables
    export DRAMATIQ_NPROCS=2 DRAMATIQ_NTHREADS=2
    python manage.py rundramatiq
    
    # Using CLI arguments
    python manage.py rundramatiq -p 2 -t 2