Channels

Channels are the medium through which messages are sent. A channel is defined as a dataclass, where the dataclass fields define the accepted notification payload. A channel must subclass one of the pgpubsub.channel.Channel or pgpubsub.channel.TriggerChannel classes, where the latter should be used when a postgres-trigger is used as the notifying event and the former when it is not. Channels must be declared in your app’s channels.py file.

The Channel class

For our first example, the data required to update the aforementioned post-reads-per-day cache is a date and a Post id. This payload defines the fields of our first channel dataclass, through which notifications will be sent to update the post-reads-per-day cache. Since we do not use a trigger here, our channel should inherit from pgpubsub.channel.Channel:

# channels.py
from dataclasses import dataclass
import datetime

from pgpubsub.channel import Channel


@dataclass
class PostReads(Channel):
    model_id: int
    date: datetime.date

Note the accepted dataclass field types for classes inheriting from Channel are iterables (lists, tuples, dicts, sets) of:

  • python primitive types

  • (naive) datetime.date objects

The TriggerChannel class

In our second example we wish to have a channel through which notifications sent whenever a postgres-trigger is invoked by the creation of an Author object. To achieve this, we define our channel like so ( also in our apps channels.py module):

from dataclasses import dataclass

from pgpubsub.channel import TriggerChannel
from pgpubsub.tests.models import Author


@dataclass
class AuthorTriggerChannel(TriggerChannel):
    model = Author

Note that the key difference between this and the previous example is that this channel inherits from TriggerChannel, which defines the payload for all trigger-based notifications:

@dataclass
class TriggerChannel(_Channel):
    model = NotImplementedError
    old: django.db.models.Model
    new: django.db.models.Model

Here the old and new parameters are the (unsaved) versions of what the trigger invoking instance looked like before and after the trigger was invoked. These objects are built by passing in the trigger notification payload through Django’s model deserializers.

In this example, old would refer to the state of our Author object pre-creation (and would hence be None) and new would refer to a copy of the newly created Author instance. This payload is inspired by the OLD and NEW values available in the postgres CREATE TRIGGER statement (https://www.postgresql.org/docs/9.1/sql-createtrigger.html). The only custom logic we need to define on a trigger channel is the model class-level attribute.

Model Migrations

Note that the payload captures the snapshot of the Author instance for some time. Later it will be deserialized (see more about this below in the Listeners section). It may happen that by that time the Author model is migrated in django and this requires careful handling to make sure the payload can still be deserialized and processed. Special handling is required when the migration is backward incompatible like making existing field mandatory.

Let’s look to the example how to do that and what tooling pgpubsub provides to facilitate that. Let’s says we want to add a new mandatory text field email to Author.

This is done in three steps (releases):

  1. New optional field is added. Application is modified so that new records always get a value in email field.

  2. Values are populated in the existing records.

  3. Fields is made mandatory.

Note that before release 2 is deployed and the migration that populates the field is applied modifications to some Author entities would produce payloads that do not have value in the email field.

When release 3 is deployed the application may assume that every Author has email. The problem is that the notifications produced before release 2 is deployed may be still not processed (for example the listener process was not run or there was an issue with the processing of some specific notification and it was skipped). In order to safely deploy release 3 the deployer need to know if there are any notifications that were created before django migrations of the release 2 were applied.

To facilitate this Notification entity stores db_version field which contains the latest migration identier for the django app the Author is defined in. The deployer may check if there are any notifications with the old db_version before deploying version that potentially breaks backward compatibility in terms of the data structure.

In this case deployer should check that there are no Notification entities with db_version before the version that was assigned to the migrations in release 2.