|
1 | 1 | import threading |
2 | | -import time |
3 | 2 | from datetime import timedelta |
4 | 3 |
|
5 | 4 | from django.db import transaction |
6 | | -from django.utils import timezone |
7 | 5 | from django.test import TransactionTestCase |
| 6 | +from django.utils import timezone |
8 | 7 |
|
9 | 8 | from kpi.models.import_export_task import ( |
10 | 9 | ImportExportStatusChoices, |
11 | | - SubmissionExportTask |
| 10 | + SubmissionExportTask, |
12 | 11 | ) |
13 | 12 | from kpi.tasks import cleanup_anonymous_exports |
14 | 13 | from kpi.utils.object_permission import get_anonymous_user |
@@ -52,62 +51,52 @@ def test_processing_exports_are_not_deleted(self): |
52 | 51 | Test that exports with PROCESSING status are never deleted |
53 | 52 | """ |
54 | 53 | processing_export = self._create_export_task( |
55 | | - status=ImportExportStatusChoices.PROCESSING, |
56 | | - minutes_old=100 |
| 54 | + status=ImportExportStatusChoices.PROCESSING, minutes_old=100 |
57 | 55 | ) |
58 | 56 |
|
59 | 57 | cleanup_anonymous_exports() |
60 | 58 | self.assertTrue( |
61 | | - SubmissionExportTask.objects.filter( |
62 | | - uid=processing_export.uid |
63 | | - ).exists() |
| 59 | + SubmissionExportTask.objects.filter(uid=processing_export.uid).exists() |
64 | 60 | ) |
65 | 61 |
|
66 | 62 | def test_concurrency_locked_rows_are_skipped(self): |
67 | 63 | exports = [self._create_export_task(minutes_old=120) for _ in range(5)] |
68 | | - export_pks = [e.pk for e in exports] |
69 | | - locked_pks = export_pks[:3] |
70 | | - unlocked_pks = export_pks[3:] |
71 | | - |
72 | | - def lock_and_hold(): |
73 | | - """ |
74 | | - Acquire lock on first 3 exports and hold for 5 seconds, |
75 | | - this will block cleanup from acquiring the lock on these rows and |
76 | | - in the meantime other rows should be cleaned up |
77 | | - """ |
| 64 | + locked_export = exports[0] |
| 65 | + not_locked_exports = exports[1:] |
| 66 | + |
| 67 | + lock_acquired = threading.Event() |
| 68 | + release_lock = threading.Event() |
| 69 | + |
| 70 | + def lock_row(): |
78 | 71 | with transaction.atomic(): |
79 | | - # Acquire lock on first 3 exports |
80 | | - list( |
81 | | - SubmissionExportTask.objects |
82 | | - .select_for_update() |
83 | | - .filter(pk__in=locked_pks) |
| 72 | + # Lock the row |
| 73 | + SubmissionExportTask.objects.select_for_update().get( |
| 74 | + pk=locked_export.pk |
84 | 75 | ) |
85 | 76 |
|
86 | | - # Hold lock for 5 seconds, |
87 | | - # during this time cleanup should run |
88 | | - time.sleep(5) |
| 77 | + # Signal that the lock is active |
| 78 | + lock_acquired.set() |
| 79 | + |
| 80 | + # Wait until the main thread signals we can release the lock |
| 81 | + release_lock.wait() |
89 | 82 |
|
90 | | - # Start thread that will hold the lock |
91 | | - lock_thread = threading.Thread(target=lock_and_hold, daemon=True) |
92 | | - lock_thread.start() |
| 83 | + t = threading.Thread(target=lock_row) |
| 84 | + t.start() |
93 | 85 |
|
94 | | - # Give thread time to acquire lock |
95 | | - time.sleep(1) |
| 86 | + # Wait for the row-level lock to actually be acquired |
| 87 | + lock_acquired.wait() |
96 | 88 |
|
97 | | - # Run cleanup while lock is held |
| 89 | + # Now cleanup should try select_for_update(nowait=True), causing DatabaseError |
98 | 90 | cleanup_anonymous_exports() |
99 | 91 |
|
100 | | - # Wait for thread to finish |
101 | | - lock_thread.join(timeout=10) |
| 92 | + # Let the locking thread finish its transaction |
| 93 | + release_lock.set() |
| 94 | + t.join() |
102 | 95 |
|
103 | | - # Verify locked rows were not deleted |
104 | | - remaining_locked = SubmissionExportTask.objects.filter( |
105 | | - pk__in=locked_pks |
106 | | - ).count() |
107 | | - self.assertEqual(remaining_locked, 3) |
| 96 | + # Verify the locked row was not deleted |
| 97 | + assert SubmissionExportTask.objects.filter(pk=locked_export.pk).exists() |
108 | 98 |
|
109 | 99 | # Verify unlocked rows were deleted |
110 | | - remaining_unlocked = SubmissionExportTask.objects.filter( |
111 | | - pk__in=unlocked_pks |
112 | | - ).count() |
113 | | - self.assertEqual(remaining_unlocked, 0) |
| 100 | + assert not SubmissionExportTask.objects.filter( |
| 101 | + pk__in=[e.pk for e in not_locked_exports] |
| 102 | + ).exists() |
0 commit comments