Commit 704ee33f authored by Anssi Kääriäinen's avatar Anssi Kääriäinen
Browse files

Fixed #16679 -- Use caching to speed up signal sending

parent 507c0814
Loading
Loading
Loading
Loading
+9 −8
Original line number Diff line number Diff line
@@ -2,15 +2,16 @@ from django.dispatch import Signal

class_prepared = Signal(providing_args=["class"])

pre_init = Signal(providing_args=["instance", "args", "kwargs"])
post_init = Signal(providing_args=["instance"])
pre_init = Signal(providing_args=["instance", "args", "kwargs"], use_caching=True)
post_init = Signal(providing_args=["instance"], use_caching=True)

pre_save = Signal(providing_args=["instance", "raw", "using", "update_fields"])
post_save = Signal(providing_args=["instance", "raw", "created", "using", "update_fields"])
pre_save = Signal(providing_args=["instance", "raw", "using", "update_fields"],
                 use_caching=True)
post_save = Signal(providing_args=["instance", "raw", "created", "using", "update_fields"], use_caching=True)

pre_delete = Signal(providing_args=["instance", "using"])
post_delete = Signal(providing_args=["instance", "using"])
pre_delete = Signal(providing_args=["instance", "using"], use_caching=True)
post_delete = Signal(providing_args=["instance", "using"], use_caching=True)

post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive"])
post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive"], use_caching=True)

m2m_changed = Signal(providing_args=["action", "instance", "reverse", "model", "pk_set", "using"])
m2m_changed = Signal(providing_args=["action", "instance", "reverse", "model", "pk_set", "using"], use_caching=True)
+51 −22
Original line number Diff line number Diff line
@@ -10,6 +10,10 @@ def _make_id(target):
    if hasattr(target, '__func__'):
        return (id(target.__self__), id(target.__func__))
    return id(target)
NONE_ID = _make_id(None)

# A marker for caching
NO_RECEIVERS = object()

class Signal(object):
    """
@@ -20,8 +24,7 @@ class Signal(object):
        receivers
            { receriverkey (id) : weakref(receiver) }
    """

    def __init__(self, providing_args=None):
    def __init__(self, providing_args=None, use_caching=False):
        """
        Create a new signal.

@@ -33,6 +36,13 @@ class Signal(object):
            providing_args = []
        self.providing_args = set(providing_args)
        self.lock = threading.Lock()
        self.use_caching = use_caching
        # For convenience we create empty caches even if they are not used.
        # A note about caching: if use_caching is defined, then for each
        # distinct sender we cache the receivers that sender has in
        # 'sender_receivers_cache'. The cache is cleaned when .connect() or
        # .disconnect() is called and populated on send().
        self.sender_receivers_cache = {}

    def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
        """
@@ -106,6 +116,7 @@ class Signal(object):
                    break
            else:
                self.receivers.append((lookup_key, receiver))
            self.sender_receivers_cache = {}

    def disconnect(self, receiver=None, sender=None, weak=True, dispatch_uid=None):
        """
@@ -140,9 +151,10 @@ class Signal(object):
                if r_key == lookup_key:
                    del self.receivers[index]
                    break
            self.sender_receivers_cache = {}

    def has_listeners(self, sender=None):
        return bool(self._live_receivers(_make_id(sender)))
        return bool(self._live_receivers(sender))

    def send(self, sender, **named):
        """
@@ -163,10 +175,10 @@ class Signal(object):
        Returns a list of tuple pairs [(receiver, response), ... ].
        """
        responses = []
        if not self.receivers:
        if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
            return responses

        for receiver in self._live_receivers(_make_id(sender)):
        for receiver in self._live_receivers(sender):
            response = receiver(signal=self, sender=sender, **named)
            responses.append((receiver, response))
        return responses
@@ -195,12 +207,12 @@ class Signal(object):
        receiver.
        """
        responses = []
        if not self.receivers:
        if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
            return responses

        # Call each receiver with whatever arguments it can accept.
        # Return a list of tuple pairs [(receiver, response), ... ].
        for receiver in self._live_receivers(_make_id(sender)):
        for receiver in self._live_receivers(sender):
            try:
                response = receiver(signal=self, sender=sender, **named)
            except Exception as err:
@@ -209,26 +221,43 @@ class Signal(object):
                responses.append((receiver, response))
        return responses

    def _live_receivers(self, senderkey):
    def _live_receivers(self, sender):
        """
        Filter sequence of receivers to get resolved, live receivers.

        This checks for weak references and resolves them, then returning only
        live receivers.
        """
        none_senderkey = _make_id(None)
        receivers = None
        if self.use_caching:
            receivers = self.sender_receivers_cache.get(sender)
            # We could end up here with NO_RECEIVERS even if we do check this case in
            # .send() prior to calling _live_receivers() due to concurrent .send() call.
            if receivers is NO_RECEIVERS:
                return []
        if receivers is None:
            with self.lock:
                senderkey = _make_id(sender)
                receivers = []

                for (receiverkey, r_senderkey), receiver in self.receivers:
            if r_senderkey == none_senderkey or r_senderkey == senderkey:
                    if r_senderkey == NONE_ID or r_senderkey == senderkey:
                        receivers.append(receiver)
                if self.use_caching:
                    if not receivers:
                        self.sender_receivers_cache[sender] = NO_RECEIVERS
                    else:
                        # Note, we must cache the weakref versions.
                        self.sender_receivers_cache[sender] = receivers
        non_weak_receivers = []
        for receiver in receivers:
            if isinstance(receiver, WEAKREF_TYPES):
                # Dereference the weak reference.
                receiver = receiver()
                if receiver is not None:
                        receivers.append(receiver)
                    non_weak_receivers.append(receiver)
            else:
                    receivers.append(receiver)
        return receivers
                non_weak_receivers.append(receiver)
        return non_weak_receivers

    def _remove_receiver(self, receiver):
        """
@@ -247,7 +276,7 @@ class Signal(object):
                for idx, (r_key, _) in enumerate(reversed(self.receivers)):
                    if r_key == key:
                        del self.receivers[last_idx - idx]

            self.sender_receivers_cache = {}

def receiver(signal, **kwargs):
    """