5

I'm performing some persistence actions with the model's object when receiving a post_save signal, which includes a call to save(). Obviously the save() call sends a post_save signal again and I'm landing in a signal recursion.

Is there a way to prevent Django to send a post_save signal on a specific save() call? Or can I detect in my signal callback if a call was "looped"?

That didn't work

I tried to modify the model's object by adding a attribute, but it seems like django.db.models.base.Model.save_base passes a "sanitized" object to the signal callback, which does not contain that attribute anymore:

def callback(sender, **kwargs):
    instance = kwargs['instance']
    if not hasattr(instance, 'no_signal'):
        # (...) Perform actions
        instance.no_signal = True
        instance.save()
post_save.connect(callback, dispatch_uid='post_save_callback')

Background

The complete situation is a bite more complex, than just receiving the signal, modifying the object and persisting it. In fact I have two identical Django instances (on different servers) which exchange created or modified objects (via ØMQ) and persist it in their own database. I don't want to use any kind of DB synchronization, since the application needs to be deployable easily and should even work with sqlite.

Since this should work for all models, regardless if they were modified/created via a view or the admin app, I'd prefer finding a way to use the post_save signal rather than introducing an own one, which needs to be triggered in various points in the project (including the User model).

Manuel Faux
  • 2,317
  • 5
  • 24
  • 35
  • 2
    Take a look: [Django post_save preventing recursion without overriding model save()](http://stackoverflow.com/questions/10840030/django-post-save-preventing-recursion-without-overriding-model-save). – alecxe May 20 '14 at 22:04
  • When I read the first sentence of that question (_There are many Stack Overflow posts about recursion using the post_save signal_) I was wondering how well these posts are hidden... – Manuel Faux May 20 '14 at 22:10
  • They are not necessarily easy to find, I probably just tend to live on here and see all the questions. – Yuji 'Tomita' Tomita May 20 '14 at 22:11
  • @Yuji'Tomita'Tomita: Did you finally really took the way of disconnecting and reconnecting the signal? Is there really a risk of loosing some signals? Is the signal callback disconnected in all threads simultaneously? If not (and this is what I assume), there should not be a risk of loosing any signals. – Manuel Faux May 20 '14 at 22:17
  • @ManuelFaux, I have not looked too deep into the issue; I took the solution and ran. This update to django fixing a reported thread safety issue with signal disconnect/connect implies it's thread safe. https://code.djangoproject.com/ticket/14533 – Yuji 'Tomita' Tomita May 20 '14 at 22:20
  • Thanks for the link to this other post, but unfortunately non of these recommendations is actually a solution for my case. * I cannot disconnect the signal (risk of loosing one). * I cannot use `update()`, since I'm creating objects. * I don't want to modify `save()` since it affects all my models. -- Maybe there is a solution which includes "detecting the recursion in the signal callback"? – Manuel Faux May 21 '14 at 07:14
  • *I cannot use update(), since I'm creating objects.* The object is created by the initial `save()`. By the time your `post_save` signal handler is called the object has already been created. So you should be able to use `update()`... – Kevin Christopher Henry May 23 '14 at 00:18
  • The point is, that I'm not saving in the handler itself, but in another method (maybe the example in the question was not the best). As I've written in the "Background" section, I persist the object on another Django instance (with identical code base) than the `post_save` was triggered. But if that persisting would trigger a `post_save` again, the receiving instance would send the object back to the sending instance and I would have a indirect `post_save` loop by sending the objects between the two Django instances. – Manuel Faux May 23 '14 at 06:47

2 Answers2

5

I solved the problem by calling save_base(raw=True) instead of save() and then checked the kwarg['raw'] in the signal handler:

def receive_signal(sender, **kwargs):
    instance, raw = kwargs['instance'], kwargs['raw']
    if not raw:
        # Send the instance to the other Django node

Actually, I'm using django.core.serializers.deserialize to deserialize my received objects and then perform a save() on the deserialized objects, which calls the save_base(raw=True) of the model.

See also: https://stackoverflow.com/a/22560210/145013

Community
  • 1
  • 1
Manuel Faux
  • 2,317
  • 5
  • 24
  • 35
1

First of all: if you deploy your django programs in a way that does no multithreading (you can do multiprocessing!) you can disconnect signals at will, and you'll not lose any singals from other threads.

If this is not an option you may also use some global thread-local state, that will contain primaty keys (and possibly types) of models that have signals suppressed.

jb.
  • 23,300
  • 18
  • 98
  • 136