Projet

Général

Profil

0001-utils-use-an-exclusive-lock-on-model-s-table-in-safe.patch

Benjamin Dauvergne, 14 janvier 2022 18:01

Télécharger (3,19 ko)

Voir les différences:

Subject: [PATCH] utils: use an exclusive lock on model's table in
 safe_get_or_create (#60658)

 src/authentic2/utils/models.py | 42 ++++++++--------------------------
 tests/test_utils_models.py     |  4 +---
 2 files changed, 10 insertions(+), 36 deletions(-)
src/authentic2/utils/models.py
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import math
18
import random
19
import time
20

  
21 17
from django.conf import settings
22
from django.db import connection
23

  
24

  
25
def poisson_random(frequency):
26
    '''Generate random numbers following a poisson distribution'''
27
    return -math.log(1.0 - random.random()) / frequency
28

  
29

  
30
SAFE_GET_OR_CREATE_RETRIES = 3
31

  
32

  
33
class ConcurrencyError(Exception):
34
    pass
18
from django.db import connection, transaction
35 19

  
36 20

  
37 21
def safe_get_or_create(model, defaults=None, **kwargs):
......
39 23
        getattr(settings, 'TESTING', False) or not connection.in_atomic_block
40 24
    ), 'safe_get_or_create cannot be used in inside a transaction'
41 25

  
42
    defaults = defaults or {}
43
    exception = None
44
    for dummy in range(SAFE_GET_OR_CREATE_RETRIES):
45
        try:
46
            instance, created = model.objects.get_or_create(defaults=defaults, **kwargs)
47
        except model.MultipleObjectsReturned as e:
48
            exception = e
49
            time.sleep(max(poisson_random(1), 0.5))
50
            continue
51

  
52
        if created and model.objects.filter(**kwargs).exclude(pk=instance.pk).exists():
53
            instance.delete()
54
            time.sleep(max(poisson_random(1), 0.5))
55
            continue
56
        return instance, created
57
    raise exception
26
    try:
27
        return model.objects.get(**kwargs), False
28
    except model.DoesNotExist:
29
        pass
30
    with transaction.atomic():
31
        with connection.cursor() as cur:
32
            cur.execute('LOCK TABLE "%s" IN EXCLUSIVE MODE' % model._meta.db_table)
33
        return model.objects.get_or_create(defaults=defaults, **kwargs)
tests/test_utils_models.py
17 17

  
18 18
import threading
19 19

  
20
from django.core.exceptions import MultipleObjectsReturned
21 20
from django.db import connection
22 21

  
23 22
from authentic2.custom_user.models import User
......
48 47
        threads[-1].start()
49 48
    for thread in threads:
50 49
        thread.join()
50
    assert not exceptions
51 51
    assert len(users) == 1
52 52
    assert User.objects.count() == 1
53
    assert all(isinstance(exception, MultipleObjectsReturned) for exception in exceptions)
54
    assert len(exceptions) < (0.5 * concurrency)  # 50% of failure is 'ok-ish' with a lot of concurrency
55 53
    users[0].delete()
56
-