Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

[1.0.X] Fixed #9479 -- Corrected an edge case in bulk queryset deleti…

…on that could cause an infinite loop when using MySQL InnoDB.

Merge of 10913-10914 from trunk.


git-svn-id: http://code.djangoproject.com/svn/django/branches/releases/1.0.X@10915 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 056796b74d871bbd4935542da8c9ad2a985dee75 1 parent 53b0436
Russell Keith-Magee authored June 03, 2009
18  django/db/models/query.py
@@ -37,11 +37,21 @@ class CollectedObjects(object):
37 37
 
38 38
     This is used for the database object deletion routines so that we can
39 39
     calculate the 'leaf' objects which should be deleted first.
  40
+
  41
+    previously_seen is an optional argument. It must be a CollectedObjects
  42
+    instance itself; any previously_seen collected object will be blocked from
  43
+    being added to this instance.
40 44
     """
41 45
 
42  
-    def __init__(self):
  46
+    def __init__(self, previously_seen=None):
43 47
         self.data = {}
44 48
         self.children = {}
  49
+        if previously_seen:
  50
+            self.blocked = previously_seen.blocked
  51
+            for cls, seen in previously_seen.data.items():
  52
+                self.blocked.setdefault(cls, SortedDict()).update(seen)
  53
+        else:
  54
+            self.blocked = {}
45 55
 
46 56
     def add(self, model, pk, obj, parent_model, nullable=False):
47 57
         """
@@ -58,6 +68,9 @@ def add(self, model, pk, obj, parent_model, nullable=False):
58 68
         Returns True if the item already existed in the structure and
59 69
         False otherwise.
60 70
         """
  71
+        if pk in self.blocked.get(model, {}):
  72
+            return True
  73
+
61 74
         d = self.data.setdefault(model, SortedDict())
62 75
         retval = pk in d
63 76
         d[pk] = obj
@@ -390,10 +403,11 @@ def delete(self):
390 403
 
391 404
         # Delete objects in chunks to prevent the list of related objects from
392 405
         # becoming too long.
  406
+        seen_objs = None
393 407
         while 1:
394 408
             # Collect all the objects to be deleted in this chunk, and all the
395 409
             # objects that are related to the objects that are to be deleted.
396  
-            seen_objs = CollectedObjects()
  410
+            seen_objs = CollectedObjects(seen_objs)
397 411
             for object in del_query[:CHUNK_SIZE]:
398 412
                 object._collect_sub_objects(seen_objs)
399 413
 
1  tests/regressiontests/delete_regress/__init__.py
... ...
@@ -0,0 +1 @@
  1
+
53  tests/regressiontests/delete_regress/models.py
... ...
@@ -0,0 +1,53 @@
  1
+from django.conf import settings
  2
+from django.db import models, backend, connection, transaction
  3
+from django.db.models import sql, query
  4
+from django.test import TestCase
  5
+
  6
+class Book(models.Model):
  7
+    pagecount = models.IntegerField()
  8
+
  9
+# Can't run this test under SQLite, because you can't
  10
+# get two connections to an in-memory database.
  11
+if settings.DATABASE_ENGINE != 'sqlite3':
  12
+    class DeleteLockingTest(TestCase):
  13
+        def setUp(self):
  14
+            # Create a second connection to the database
  15
+            self.conn2 = backend.DatabaseWrapper(**settings.DATABASE_OPTIONS)
  16
+
  17
+            # Put both DB connections into managed transaction mode
  18
+            transaction.enter_transaction_management()
  19
+            transaction.managed(True)
  20
+            # self.conn2._enter_transaction_management(True)
  21
+
  22
+        def tearDown(self):
  23
+            # Close down the second connection.
  24
+            transaction.leave_transaction_management()
  25
+            self.conn2.close()
  26
+
  27
+        def test_concurrent_delete(self):
  28
+            "Deletes on concurrent transactions don't collide and lock the database. Regression for #9479"
  29
+
  30
+            # Create some dummy data
  31
+            b1 = Book(id=1, pagecount=100)
  32
+            b2 = Book(id=2, pagecount=200)
  33
+            b3 = Book(id=3, pagecount=300)
  34
+            b1.save()
  35
+            b2.save()
  36
+            b3.save()
  37
+
  38
+            transaction.commit()
  39
+
  40
+            self.assertEquals(3, Book.objects.count())
  41
+
  42
+            # Delete something using connection 2.
  43
+            cursor2 = self.conn2.cursor()
  44
+            cursor2.execute('DELETE from delete_regress_book WHERE id=1')
  45
+            self.conn2._commit();
  46
+
  47
+            # Now perform a queryset delete that covers the object
  48
+            # deleted in connection 2. This causes an infinite loop
  49
+            # under MySQL InnoDB unless we keep track of already
  50
+            # deleted objects.
  51
+            Book.objects.filter(pagecount__lt=250).delete()
  52
+            transaction.commit()
  53
+            self.assertEquals(1, Book.objects.count())

0 notes on commit 056796b

Please sign in to comment.
Something went wrong with that request. Please try again.