|
1 | 1 | from datetime import datetime, timedelta
|
| 2 | +from unittest.mock import patch |
2 | 3 |
|
3 | 4 | import pytest
|
4 | 5 | from celery import states, uuid
|
5 | 6 | from django.db import transaction
|
| 7 | +from django.db.utils import InterfaceError |
6 | 8 | from django.test import TransactionTestCase
|
7 | 9 |
|
| 10 | +from django_celery_results.backends import DatabaseBackend |
8 | 11 | from django_celery_results.models import GroupResult, TaskResult
|
9 | 12 | from django_celery_results.utils import now
|
10 | 13 |
|
@@ -93,6 +96,94 @@ class TransactionError(Exception):
|
93 | 96 | assert TaskResult.objects.get_task(m1.task_id).status != states.SUCCESS
|
94 | 97 | assert TaskResult.objects.get_task(m2.task_id).status == states.SUCCESS
|
95 | 98 |
|
| 99 | + def test_retry_store_result_fails(self): |
| 100 | + """ |
| 101 | + Test the retry logic for InterfaceErrors. |
| 102 | + When result_backend_always_retry is False, |
| 103 | + and an InterfaceError is raised during _store_result(), |
| 104 | + then the InterfaceError will be re-raised. |
| 105 | + """ |
| 106 | + m = self.create_task_result() |
| 107 | + assert set(TaskResult.objects.all()) == set( |
| 108 | + TaskResult.objects.using('secondary').all() |
| 109 | + ) |
| 110 | + |
| 111 | + always_retry = self.app.conf.get('result_backend_always_retry') |
| 112 | + self.app.conf.result_backend_always_retry = False |
| 113 | + backend = DatabaseBackend(self.app) |
| 114 | + |
| 115 | + with patch.object( |
| 116 | + backend, |
| 117 | + '_store_result', |
| 118 | + side_effect=[ |
| 119 | + InterfaceError('Connection closed') |
| 120 | + ] |
| 121 | + ) as patched_store_result: |
| 122 | + with patch.object( |
| 123 | + backend, |
| 124 | + 'exception_safe_to_retry', |
| 125 | + return_value=backend.exception_safe_to_retry |
| 126 | + ) as patched_safe_to_retry: |
| 127 | + # InterfaceError should be re-raised |
| 128 | + with pytest.raises(InterfaceError): |
| 129 | + backend.store_result( |
| 130 | + m.task_id, |
| 131 | + result=states.SUCCESS, |
| 132 | + state=states.SUCCESS |
| 133 | + ) |
| 134 | + assert patched_safe_to_retry.call_count == 0 |
| 135 | + assert patched_store_result.call_count == 1 |
| 136 | + |
| 137 | + self.app.conf.result_backend_always_retry = always_retry |
| 138 | + if always_retry is None: |
| 139 | + del self.app.conf.result_backend_always_retry |
| 140 | + |
| 141 | + def test_retry_store_result_succeeds(self): |
| 142 | + """ |
| 143 | + Test the retry logic for InterfaceErrors. |
| 144 | + When result_backend_always_retry is True, |
| 145 | + and an InterfaceError is raised during _store_result(), |
| 146 | + then the InterfaceError will be hidden, |
| 147 | + the connection to the database will be closed, |
| 148 | + and then automatically reopened for the next retry. |
| 149 | + """ |
| 150 | + m = self.create_task_result() |
| 151 | + assert set(TaskResult.objects.all()) == set( |
| 152 | + TaskResult.objects.using('secondary').all() |
| 153 | + ) |
| 154 | + |
| 155 | + always_retry = self.app.conf.get('result_backend_always_retry') |
| 156 | + self.app.conf.result_backend_always_retry = True |
| 157 | + backend = DatabaseBackend(self.app) |
| 158 | + |
| 159 | + with patch.object( |
| 160 | + backend, |
| 161 | + '_store_result', |
| 162 | + side_effect=[ |
| 163 | + InterfaceError('Connection closed'), |
| 164 | + backend._store_result |
| 165 | + ] |
| 166 | + ) as patched_store_result: |
| 167 | + with patch.object( |
| 168 | + backend, |
| 169 | + 'exception_safe_to_retry', |
| 170 | + return_value=backend.exception_safe_to_retry |
| 171 | + ) as patched_safe_to_retry: |
| 172 | + # InterfaceError should be hidden |
| 173 | + # And new connection opened |
| 174 | + # Then unpatched function called for retry |
| 175 | + backend.store_result( |
| 176 | + m.task_id, |
| 177 | + result=states.SUCCESS, |
| 178 | + state=states.SUCCESS |
| 179 | + ) |
| 180 | + assert patched_safe_to_retry.call_count == 1 |
| 181 | + assert patched_store_result.call_count == 2 |
| 182 | + |
| 183 | + self.app.conf.result_backend_always_retry = always_retry |
| 184 | + if always_retry is None: |
| 185 | + del self.app.conf.result_backend_always_retry |
| 186 | + |
96 | 187 | def create_group_result(self):
|
97 | 188 | id = uuid()
|
98 | 189 | taskmeta, created = GroupResult.objects.get_or_create(group_id=id)
|
|
0 commit comments