Подтвердить что ты не робот

Django: IntegrityError во многих случаях добавить()

Мы сталкиваемся с известной проблемой в django:

IntegrityError во многих случаях добавить()

Существует условие гонки, если несколько процессов/запросов пытаются добавить одну и ту же строку в ManyToManyRelation.

Как обойти это?

Envionment:

  • Django 1.9
  • Сервер Linux
  • Postgres 9.3 (при необходимости можно сделать обновление)

Подробнее

Как воспроизвести его:

my_user.groups.add(foo_group)

Выше сбой, если два запроса попытаются выполнить этот код сразу. Вот таблица базы данных и ограничение на отказ:

myapp_egs_d=> \d auth_user_groups
  id       | integer | not null default ...
  user_id  | integer | not null
  group_id | integer | not null
Indexes:
           "auth_user_groups_pkey" PRIMARY KEY, btree (id)
fails ==>  "auth_user_groups_user_id_group_id_key" UNIQUE CONSTRAINT,
                                            btree (user_id, group_id)

Окружающая среда

Так как это происходит только на производственных машинах, и все производственные машины в моем контексте запускают postgres, будет доступно только решение postgres.

4b9b3361

Ответ 1

Можно ли воспроизвести ошибку?

Да, давайте использовать знаменитые модели Publication и Article из Django docs. Затем создайте несколько потоков.

import threading
import random

def populate():

    for i in range(100):
        Article.objects.create(headline = 'headline{0}'.format(i))
        Publication.objects.create(title = 'title{0}'.format(i))

    print 'created objects'


class MyThread(threading.Thread):

    def run(self):
        for q in range(1,100):
            for i in range(1,5):
                pub = Publication.objects.all()[random.randint(1,2)]
                for j in range(1,5):
                    article = Article.objects.all()[random.randint(1,15)]
                    pub.article_set.add(article)

            print self.name


Article.objects.all().delete()
Publication.objects.all().delete()
populate()
thrd1 = MyThread()
thrd2 = MyThread()
thrd3 = MyThread()

thrd1.start()
thrd2.start()
thrd3.start()

Вы обязательно увидите уникальные нарушения ограничений типа, указанные в отчете об ошибке . Если вы их не видите, попробуйте увеличить количество потоков или итераций.

Есть ли работа вокруг?

Да. Используйте модели through и get_or_create. Вот модели .py, адаптированные из примера в django docs.

class Publication(models.Model):
    title = models.CharField(max_length=30)

    def __str__(self):              # __unicode__ on Python 2
        return self.title

    class Meta:
        ordering = ('title',)

class Article(models.Model):
    headline = models.CharField(max_length=100)
    publications = models.ManyToManyField(Publication, through='ArticlePublication')

    def __str__(self):              # __unicode__ on Python 2
        return self.headline

    class Meta:
        ordering = ('headline',)

class ArticlePublication(models.Model):
    article = models.ForeignKey('Article', on_delete=models.CASCADE)
    publication = models.ForeignKey('Publication', on_delete=models.CASCADE)
    class Meta:
        unique_together = ('article','publication')

Вот новый класс потоков, который является модификацией выше.

class MyThread2(threading.Thread):

    def run(self):
        for q in range(1,100):
            for i in range(1,5):
                pub = Publication.objects.all()[random.randint(1,2)]
                for j in range(1,5):
                    article = Article.objects.all()[random.randint(1,15)]
                    ap , c = ArticlePublication.objects.get_or_create(article=article, publication=pub)
            print 'Get  or create', self.name

Вы обнаружите, что исключение больше не отображается. Не стесняйтесь увеличивать количество итераций. Я только поднялся до 1000 с get_or_create, он не выбрал исключение. Однако add() обычно генерирует исключение из 20 итераций.

Почему это работает?

Потому что get_or_create является атомарным.

Этот метод является атомарным, предполагающим правильное использование, правильную базу данных конфигурации и правильного поведения базовой базы данных. Однако, если уникальность не применяется на уровне базы данных для kwargs, используемые в вызове get_or_create (см. уникальный или unique_together), этот метод склонен к состоянию гонки, что может привести к множественным строки с теми же параметрами, которые вставлены одновременно.

Update: Спасибо @louis за указание, что сквозная модель действительно может быть устранена. Thuse get_or_create в MyThread2 можно изменить как.

ap , c = article.publications.through.objects.get_or_create(
            article=article, publication=pub)

Ответ 2

Если вы готовы решить эту проблему в PostgreSQL, вы можете сделать следующее в psql:

-- Create a RULE and function to intercept all INSERT attempts to the table and perform a check whether row exists:

CREATE RULE auth_user_group_ins AS 
    ON INSERT TO auth_user_groups 
    WHERE (EXISTS (SELECT 1 
                   FROM auth_user_groups 
                   WHERE user_id=NEW.user_id AND group_id=NEW.group_id)) 
    DO INSTEAD NOTHING;

Затем он будет игнорировать дубликаты только новых вставок в таблице:

db=# TRUNCATE auth_user_groups;
TRUNCATE TABLE

db=# INSERT INTO auth_user_groups (user_id, group_id) VALUES (1,1);
INSERT 0 1   --  added

db=# INSERT INTO auth_user_groups (user_id, group_id) VALUES (1,1);
INSERT 0 0   -- no insert no error

db=# INSERT INTO auth_user_groups (user_id, group_id) VALUES (1,2);
INSERT 0 1   -- added

db=# SELECT * FROM auth_user_groups;  -- check
 id | user_id | group_id
----+---------+----------
 14 |       1 |        1
 16 |       1 |        2
(2 rows)

db=#

Ответ 3

Из того, что я вижу в представленном коде. Я считаю, что у вас есть ограничение на уникальность в парах (user_id, group_id) в группах. Таким образом, при запуске 2 раза один и тот же запрос будет терпеть неудачу, поскольку вы пытаетесь добавить 2 строки с теми же user_id и group_id, что первый из них будет выполняться, а второй вызовет исключение.