#24576: Bad cascading leads to non-deterministic IntegrityError
-------------------------------------+-------------------------------------
     Reporter:  glic3rinu            |                    Owner:  nobody
         Type:  Bug                  |                   Status:  new
    Component:  Database layer       |                  Version:  1.8
  (models, ORM)                      |
     Severity:  Normal               |               Resolution:
     Keywords:                       |             Triage Stage:  Accepted
    Has patch:  0                    |      Needs documentation:  0
  Needs tests:  0                    |  Patch needs improvement:  0
Easy pickings:  0                    |                    UI/UX:  0
-------------------------------------+-------------------------------------
Changes (by timgraham):

 * needs_better_patch:   => 0
 * needs_docs:   => 0
 * needs_tests:   => 0
 * stage:  Unreviewed => Accepted


Old description:

> I've spent all day with a bug on my application that seems to be a bug on
> Django's ORM delete on cascade resoultion order.
>
> Moreover because the order in which related objects are deleted by the
> ORM is non-deterministic (changes every time you start the interpreter)
> it makes even harder to track down.
>
> So far I've been able to reproduce the problem with a few models:
>

> {{{
> from django.db import models
> from django.contrib.contenttypes.fields import GenericForeignKey,
> GenericRelation
> from django.contrib.contenttypes.models import ContentType
> from django.db.models.signals import post_delete
> from django.dispatch import receiver
>
> class Resource(models.Model):
>     content_type = models.ForeignKey(ContentType)
>     object_id = models.PositiveIntegerField()
>     content_object = GenericForeignKey()
>
> class Account(models.Model):
>     name = models.CharField(max_length=10)
>     resources = GenericRelation(Resource)
>
> class Order(models.Model):
>     account = models.ForeignKey(Account)
>     content_type = models.ForeignKey(ContentType)
>     object_id = models.PositiveIntegerField()
>     content_object = GenericForeignKey()
>
> class MetricStorage(models.Model):
>     order = models.ForeignKey(Order)
>
> @receiver(post_delete, dispatch_uid="orders.cancel_orders")
> def cancel_orders(sender, **kwargs):
>     instance = kwargs['instance']
>     print('delete', sender, instance, instance.pk)
>     if sender == Resource:
>         related = instance.content_object
>         if related:
>             ct = ContentType.objects.get_for_model(related)
>             order = Order.objects.get(content_type=ct,
> object_id=related.pk)
>             order.metricstorage_set.create()
>             order.save()
>         else:
>             print('related is None')
> }}}
>

> And this test code
>

> {{{
> from test.models import Account, Resource, Order
>
> account = Account.objects.create(name='John')
> resource = Resource.objects.create(content_object=account)
> order = Order.objects.create(account=account, content_object=account)
> account.delete()
> }}}
>
> In order to properly test this you should make tries restarting the
> interpreter a handfull of times, and then you'll se two different
> results.
>
> This that I consider correct:
>
> {{{
> delete <class 'test.models.Order'> Order object 384
> delete <class 'test.models.Account'> Account object 124
> delete <class 'test.models.Resource'> Resource object 307
> related is None
> }}}
>
> And the IntegrityError which I consider to be a bug
>

> {{{
> delete <class 'test.models.Resource'> Resource object 311
> delete <class 'test.models.Order'> Order object 388
> delete <class 'test.models.Account'> Account object 128
> Traceback (most recent call last):
>   File "<console>", line 1, in <module>
>   File "/usr/local/lib/python3.4/dist-packages/django/db/models/base.py",
> line 872, in delete
>     collector.delete()
>   File "/usr/local/lib/python3.4/dist-
> packages/django/db/models/deletion.py", line 314, in delete
>     sender=model, instance=obj, using=self.using
>   File "/usr/local/lib/python3.4/dist-packages/django/db/transaction.py",
> line 232, in __exit__
>     connection.commit()
>   File "/usr/local/lib/python3.4/dist-
> packages/django/db/backends/base/base.py", line 173, in commit
>     self._commit()
>   File "/usr/local/lib/python3.4/dist-
> packages/django/db/backends/base/base.py", line 142, in _commit
>     return self.connection.commit()
>   File "/usr/local/lib/python3.4/dist-packages/django/db/utils.py", line
> 97, in __exit__
>     six.reraise(dj_exc_type, dj_exc_value, traceback)
>   File "/usr/local/lib/python3.4/dist-packages/django/utils/six.py", line
> 658, in reraise
>     raise value.with_traceback(tb)
>   File "/usr/local/lib/python3.4/dist-
> packages/django/db/backends/base/base.py", line 142, in _commit
>     return self.connection.commit()
> django.db.utils.IntegrityError: insert or update on table
> "test_metricstorage" violates foreign key constraint
> "test_metricstorage_order_id_730c757c66b8c627_fk_test_order_id"
> DETAIL:  Key (order_id)=(388) is not present in table "test_order".
> }}}
>

> Notice how the order in which related objects are deleted is differnet, I
> believe this to be the source of the problem.
>
> I've noticed this problem on 1.7 and now 1.8, not tested with master.

New description:

 I've spent all day with a bug on my application that seems to be a bug on
 Django's ORM delete on cascade resolution order.

 Moreover because the order in which related objects are deleted by the ORM
 is non-deterministic (changes every time you start the interpreter) it
 makes even harder to track down.

 So far I've been able to reproduce the problem with a few models:


 {{{
 from django.db import models
 from django.contrib.contenttypes.fields import GenericForeignKey,
 GenericRelation
 from django.contrib.contenttypes.models import ContentType
 from django.db.models.signals import post_delete
 from django.dispatch import receiver

 class Resource(models.Model):
     content_type = models.ForeignKey(ContentType)
     object_id = models.PositiveIntegerField()
     content_object = GenericForeignKey()

 class Account(models.Model):
     name = models.CharField(max_length=10)
     resources = GenericRelation(Resource)

 class Order(models.Model):
     account = models.ForeignKey(Account)
     content_type = models.ForeignKey(ContentType)
     object_id = models.PositiveIntegerField()
     content_object = GenericForeignKey()

 class MetricStorage(models.Model):
     order = models.ForeignKey(Order)

 @receiver(post_delete, dispatch_uid="orders.cancel_orders")
 def cancel_orders(sender, **kwargs):
     instance = kwargs['instance']
     print('delete', sender, instance, instance.pk)
     if sender == Resource:
         related = instance.content_object
         if related:
             ct = ContentType.objects.get_for_model(related)
             order = Order.objects.get(content_type=ct,
 object_id=related.pk)
             order.metricstorage_set.create()
             order.save()
         else:
             print('related is None')
 }}}


 And this test code


 {{{
 from test.models import Account, Resource, Order

 account = Account.objects.create(name='John')
 resource = Resource.objects.create(content_object=account)
 order = Order.objects.create(account=account, content_object=account)
 account.delete()
 }}}

 In order to properly test this you should make tries restarting the
 interpreter a handful of times, and then you'll see two different results.

 This that I consider correct:

 {{{
 delete <class 'test.models.Order'> Order object 384
 delete <class 'test.models.Account'> Account object 124
 delete <class 'test.models.Resource'> Resource object 307
 related is None
 }}}

 And the IntegrityError which I consider to be a bug


 {{{
 delete <class 'test.models.Resource'> Resource object 311
 delete <class 'test.models.Order'> Order object 388
 delete <class 'test.models.Account'> Account object 128
 Traceback (most recent call last):
   File "<console>", line 1, in <module>
   File "/usr/local/lib/python3.4/dist-packages/django/db/models/base.py",
 line 872, in delete
     collector.delete()
   File "/usr/local/lib/python3.4/dist-
 packages/django/db/models/deletion.py", line 314, in delete
     sender=model, instance=obj, using=self.using
   File "/usr/local/lib/python3.4/dist-packages/django/db/transaction.py",
 line 232, in __exit__
     connection.commit()
   File "/usr/local/lib/python3.4/dist-
 packages/django/db/backends/base/base.py", line 173, in commit
     self._commit()
   File "/usr/local/lib/python3.4/dist-
 packages/django/db/backends/base/base.py", line 142, in _commit
     return self.connection.commit()
   File "/usr/local/lib/python3.4/dist-packages/django/db/utils.py", line
 97, in __exit__
     six.reraise(dj_exc_type, dj_exc_value, traceback)
   File "/usr/local/lib/python3.4/dist-packages/django/utils/six.py", line
 658, in reraise
     raise value.with_traceback(tb)
   File "/usr/local/lib/python3.4/dist-
 packages/django/db/backends/base/base.py", line 142, in _commit
     return self.connection.commit()
 django.db.utils.IntegrityError: insert or update on table
 "test_metricstorage" violates foreign key constraint
 "test_metricstorage_order_id_730c757c66b8c627_fk_test_order_id"
 DETAIL:  Key (order_id)=(388) is not present in table "test_order".
 }}}


 Notice how the order in which related objects are deleted is different, I
 believe this to be the source of the problem.

 I've noticed this problem on 1.7 and now 1.8, not tested with master.

--

Comment:

 I could also reproduce on master with your example code.

--
Ticket URL: <https://code.djangoproject.com/ticket/24576#comment:1>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/067.eb666febf6b17de837ef92c8d47558a2%40djangoproject.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to