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.