django-pgpubsub

repository·master·Indexed 42 days ago

https://github.com/paulgilmartin/django-pgpubsub

A distributed task processing framework for Django version 2.2+ and PostgreSQL 11+ built on the Postgres LISTEN/NOTIFY protocol. It provides a lightweight, database-backed alternative to AMQP brokers like Celery, featuring trigger integration via django-pgtrigger, exactly-once processing using database locks, and atomic notifications tied to Postgres transactions.

Tokens
8.4K
Snippets
27
Records
36
Agent score
53%

What's inside django-pgpubsub

  1. What is a listener in pgpubsub

    master

    In pgpubsub, a listener is a function that processes notifications sent through a specific channel.

    To work correctly, listeners must follow these rules:

    1. They must be defined in your app's listeners.py file.
    2. They must be decorated using decorators from pgpubsub.listen (e.g., @pgpubsub.listener or @pgpubsub.post_insert_listener).
    3. The decorator associates the function with a specific channel, which also determines the function's expected signature.
    4. The function must be imported in your app's configuration class ready() method (similar to Django signals) to ensure it is registered.
  2. How notification recovery works with lock_notifications

    master

    Notification recovery relies on the lock_notifications setting being enabled on your channel. When enabled, pgpubsub stores a Notification object in the database instead of relying solely on the transient LISTEN/NOTIFY protocol.

    If listener processes are offline when a notification is sent, the payload remains in the database. You can then recover these notifications using:

    1. The pgpubsub.process_stored_notifications function, which fetches all stored Notifications and sends them to their respective channels.
    2. The --recover flag when running the listen management command, which triggers an automatic replay of missed notifications upon startup.
  3. How django-pgpubsub works

    master

    django-pgpubsub is a framework for building asynchronous and distributed message processing networks using a PostgreSQL database. It leverages the Postgres LISTEN/NOTIFY protocol to create a message queue at the database layer.

    Key Features:

    • Minimal Infrastructure: Uses your existing Postgres database; no need for external brokers like RabbitMQ.
    • Trigger Integration: Can use django-pgtrigger to emit notifications automatically from database-level events (e.g., table updates), making it more robust than Django's post_save signals which can miss bulk_create operations.
    • Exactly-once Processing: Can be configured to ensure each notification is processed exactly once by using database locks and storing notification copies.
    • Durability: Supports storing notifications in the database to allow replaying missed messages if listeners were offline.
    • Atomicity: Notifications are tied to the Postgres transaction; they are only sent if the transaction successfully commits.
  4. How lock_notifications works internally

    master

    When lock_notifications = True is enabled, pgpubsub changes the notification lifecycle to use database-backed locking:

    1. Storage: When a notification is sent (via pgpubsub.notify or a trigger), a pgpubsub.models.Notification object is inserted into the database containing the JSON payload. This is atomic with the Postgres notification.
    2. Locking: When a listener detects a notification, it attempts to fetch and lock the corresponding Notification object using select_for_update(skip_locked=True).
    3. Concurrency Control: The skip_locked=True flag ensures that if one process has already locked the notification, other processes will immediately skip it rather than waiting. This allows workers to continue polling for other notifications.
    4. Cleanup: Once the listener's callback successfully completes, the stored Notification is removed from the database.
  5. Handle backward incompatible model migrations

    master

    Because notification payloads capture a snapshot of a model that is later deserialized, backward incompatible Django migrations (e.g., making a field mandatory) can cause deserialization errors if notifications produced before the migration are still pending processing.

    Safe Migration Strategy

    To avoid breaking listeners, follow a multi-release deployment pattern:

    1. Add field as optional: Modify the app so new records get a value.
    2. Populate existing records: Run a migration to fill the field for old records.
    3. Make field mandatory: Only after ensuring all old notifications are processed.

    Using db_version for verification

    To ensure it is safe to deploy a breaking migration, check the Notification entity's db_version field. This field stores the latest migration identifier for the app.

    Deployment Check: Before deploying a version that breaks backward compatibility, verify that there are no Notification entities in the database with a db_version older than the migration that populates the required fields.

  6. Install django-pgpubsub

    master

    Install the library using pip.

    Prerequisites:

    • Django 2.2 or later.
    • PostgreSQL 11 or later.

    Setup Steps:

    1. Install via pip:
      pip install django-pgpubsub
    2. Add pgpubsub and pgtrigger to your INSTALLED_APPS in Django settings.
    3. Run python manage.py migrate to create the required Notification model table.
    pip install django-pgpubsub
  7. Start listening for notifications with the listen command

    master

    To start listener functions and have them listen for incoming notifications on their associated channels, use the listen management command.

    By default, pgpubsub provides automatic fault tolerance: if a listener process encounters a Python-level exception, the library automatically spins up a secondary process to continue listening before the initial process terminates. This prevents the need for manual restarts due to listener exceptions.

    ./manage.py listen
  8. Recover missed notifications using the --recover flag

    master

    By default, Postgres LISTEN/NOTIFY notifications are lost if no process is listening when they are sent. To prevent this, you can enable lock_notifications on a channel, which stores a Notification object in the database.

    To automatically replay any missed stored notifications when your listener processes start up, use the --recover option with the listen management command.

    python manage.py listen --recover
  9. Register listeners in Django AppConfig

    master

    To ensure your listeners are registered when Django starts, you must import the module containing your listeners within the ready() method of your application's AppConfig class.

    # tests/apps.py
    from django.apps import AppConfig
    
    class TestsConfig(AppConfig):
        name = 'tests'
    
        def ready(self):
            import pgpubsub.tests.listeners
  10. Register listeners in your Django AppConfig

    master

    To ensure your listeners are discovered and registered by pgpubsub, you must import your listeners.py module within the ready() method of your application's AppConfig class.

    from django.apps import AppConfig
    
    class TestsConfig(AppConfig):
        name = 'tests'
    
        def ready(self):
            import pgpubsub.tests.listeners
  11. Pass context to the trigger listener callback

    master

    By default, listener callbacks receive the old and new objects. To also receive the context dictionary used during the trigger, follow these steps:

    1. Set PGPUBSUB_PASS_CONTEXT_TO_LISTENERS = True in your Django settings.
    2. Add a context parameter (typed as Dict[str, Any]) to your listener function signature.
    # listeners.py
    import pgpubsub
    from typing import Dict, Any
    
    @pgpubsub.post_insert_listener(AuthorTriggerChannel)
    def create_first_post_for_author(
        old: Author, 
        new: Author, 
        context: Dict[str, Any]
    ):
        print(f'Creating first post for {new.name} with context={context}')
        # ... logic ...
    
    # django settings
    PGPUBSUB_PASS_CONTEXT_TO_LISTENERS = True