Skip to main content

Signals and Dispatching

The signal dispatcher in this codebase, centered around the Signal class in django/dispatch/dispatcher.py, provides a mechanism for decoupled applications to get notified when specific actions occur. This allows components to communicate without having direct dependencies on each other.

Core Concepts​

The dispatch system consists of two main components:

  1. Signals: Instances of the Signal class that act as dispatchers.
  2. Receivers: Callables (functions or methods) that are registered to be executed when a signal is sent.

Defining a Signal​

Signals are typically defined at the module level. For example, in django/core/signals.py, core framework signals are defined as:

from django.dispatch import Signal

request_started = Signal()
request_finished = Signal()
got_request_exception = Signal()

When instantiating a Signal, you can enable caching by passing use_caching=True. This optimizes performance by caching the list of receivers for specific senders in self.sender_receivers_cache.

Connecting Receivers​

A receiver must be a callable that accepts keyword arguments (**kwargs). This requirement, enforced in Signal.connect when DEBUG is enabled, ensures that receivers remain compatible if new arguments are added to the signal in the future.

Using the @receiver Decorator​

The most common way to connect a receiver is using the receiver decorator found in django/dispatch/dispatcher.py. It allows you to specify one or more signals and optional filters like the sender.

from django.dispatch import receiver
from django.core.signals import request_finished

@receiver(request_finished)
def my_callback(sender, **kwargs):
print("Request finished!")

Manual Connection​

You can also use the connect() method directly. This is useful for dynamic connections or when you need more control over connection parameters:

from django.dispatch import Signal

pizza_done = Signal()

def notify_customer(sender, **kwargs):
# Logic here
pass

pizza_done.connect(notify_customer, dispatch_uid="unique_notification_id")

Connection Parameters​

  • sender: You can restrict a receiver to only trigger when a specific object sends the signal.
  • weak: By default (weak=True), the dispatcher uses weakref to store receivers. If a receiver is a local function or a lambda, it may be garbage collected. Set weak=False to maintain a strong reference.
  • dispatch_uid: A unique identifier (usually a string) that prevents the same receiver from being registered multiple times. This is particularly useful in environments where code might be imported more than once.

Sending Signals​

The Signal class provides several methods for dispatching events, supporting both synchronous and asynchronous execution.

Synchronous Dispatch​

The send() method executes all connected receivers synchronously. It returns a list of tuple pairs [(receiver, response), ... ].

responses = pizza_done.send(sender="Kitchen", toppings=["cheese"])

If a receiver raises an exception, it propagates immediately, potentially preventing subsequent receivers from being called. To handle errors gracefully, use send_robust(), which catches Exception subclasses and returns the error object in the response list instead of propagating it.

Asynchronous Dispatch​

For applications using asyncio, the asend() and asend_robust() methods allow for non-blocking dispatch.

  • Execution Order: asend() first groups and executes all synchronous receivers (wrapped via sync_to_async) and then executes all asynchronous receivers concurrently using asyncio.TaskGroup.
  • Context: These methods must be awaited.

Example from tests/signals/tests.py:

async def test_asend(self):
signal = Signal()
signal.connect(sync_handler)
signal.connect(async_handler)

# Triggers both handlers; async_handler runs concurrently
result = await signal.asend(sender=self.__class__)

Internal Dispatch Logic​

The Signal class manages its receivers in self.receivers, which is a list of tuples containing the receiver reference, the sender reference, and an is_async flag.

Live Receiver Resolution​

When a signal is sent, the dispatcher calls _live_receivers(sender) to determine which callables should be executed. This process involves:

  1. Filtering: Only receivers connected to the specific sender (or connected to all senders via None) are selected.
  2. Weakref Cleanup: It checks if any weak references to receivers or senders have died. If self._dead_receivers is flagged, it performs a cleanup of the self.receivers list.
  3. Caching: If use_caching is enabled, it retrieves the resolved list from sender_receivers_cache to avoid repeated filtering logic.

Thread Safety​

All modifications to the receiver list (connect, disconnect) and the cleanup of dead receivers are protected by self.lock (a threading.Lock), ensuring that the dispatcher is thread-safe during registration and resolution.