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:
- Signals: Instances of the
Signalclass that act as dispatchers. - 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 usesweakrefto store receivers. If a receiver is a local function or a lambda, it may be garbage collected. Setweak=Falseto 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 viasync_to_async) and then executes all asynchronous receivers concurrently usingasyncio.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:
- Filtering: Only receivers connected to the specific
sender(or connected to all senders viaNone) are selected. - Weakref Cleanup: It checks if any weak references to receivers or senders have died. If
self._dead_receiversis flagged, it performs a cleanup of theself.receiverslist. - Caching: If
use_cachingis enabled, it retrieves the resolved list fromsender_receivers_cacheto 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.