Какой самый простой способ заблокировать объект в Django

Я хочу вызвать ошибку, когда пользователь пытается удалить объект, когда некоторые другие пользователи активны в представлении update_object. Я чувствую, что для этого нужен какой-то мьютекс-подобный механизм блокировки. У вас есть какие-нибудь предложения?

6 ответов

Решение

Итак, есть несколько способов сделать то, что вы просите. Но многие из них не будут независимы от реализации: вы можете использовать блокировки или блокировки, но они действительно будут работать только на 100% -ных многопоточных серверах и, вероятно, вообще не будут реализованы в реализации fork / pre-fork.

Это более или менее означает, что реализация блокировки будет зависеть от вас. Две идеи:

  1. .lock файл в вашей файловой системе
  2. locked недвижимость в вашем модельном классе

В обоих случаях вы должны вручную установить объект блокировки при обновлении и проверить его при удалении. Попробуйте что-то вроде:

def safe_update(request,model,id):
    obj = model.objects.get(id)
    if obj.locked:
        raise SimultaneousUpdateError #Define this somewhere
    else:
        obj.lock()
        return update_object(request,model,id)

# In models file
class SomeModel(models.Model):
    locked = models.BooleanField(default = False)
    def lock(self):
        self.locked = True
        super(models.Model,self).save()
    def save(self):
        # overriding save because you want to use generic views
        # probably not the best idea to rework model code to accomodate view shortcuts
        # but I like to give examples.
        self.locked = False
        # THIS CREATES A DIFFERENT CRITICAL REGION!
        super(models.Model,self).save()

Это действительно неуклюжая реализация, которую вам придется очистить. Возможно, вас не устраивает тот факт, что была создана другая критическая область, но я не понимаю, как вы будете работать намного лучше, если будете использовать базу данных в качестве реализации, не усложняя реализацию. (Одним из вариантов будет сделать блокировки полностью отдельными объектами. Затем вы можете обновить их после вызова метода save(). Но я не чувствую необходимости кодировать это.) Если вы действительно хотите использовать блокировку на основе файлов система, которая также решит проблему. Если вы параноик по базе данных, это может быть для вас. Что-то вроде:

class FileLock(object):
    def __get__(self,obj):
        return os.access(obj.__class__+"_"+obj.id+".lock",os.F_OK)
    def __set__(self,obj,value):
        if not isinstance(value,bool):
            raise AttributeError
        if value:
            f = open(obj.__class__+"_"+obj.id+".lock")
            f.close()
        else:
            os.remove(obj.__class__+"_"+obj.id+".lock")
    def __delete__(self,obj):
        raise AttributeError

 class SomeModel(models.Model):
     locked = FileLock()
     def save(self):
         super(models.Model,self).save()
         self.locked = False

В любом случае, может быть, есть какой-то способ смешать и сопоставить эти предложения на свой вкус?

После добавления select_for_update существует простой способ получить блокировку для объекта, если ваша база данных поддерживает его. postgresql, oracle и mysql, по крайней мере, поддерживают его, согласно документации Django.

Пример кода:

import time

from django.contrib.auth import get_user_model
from django.db import transaction


User = get_user_model()

target_user_pk = User.objects.all()[0].pk


with transaction.atomic():
    print "Acquiring lock..."
    to_lock = User.objects.filter(pk=target_user_pk).select_for_update()
    # Important! Queryset evaluation required to actually acquire the lock.
    locked = to_lock[0]
    print locked

    while True:
        print "sleeping {}".format(time.time())
        time.sleep(5)

Поскольку ваша область ограничена удалениями, а не обновлениями, одним из вариантов будет переосмысление идеи "удалить" как действия "отменить публикацию". Например, возьмите следующую модель:

class MyManager(models.Manager):
    def get_query_set(self):
        super(MyManager, self).get_query_set().filter(published=True)

class MyModel(models.Model):
    objects = MyManager()
    published = models.BooleanField(default=True)
    ... your fields ...

    def my_delete(self):
        self.published = False
        super(MyModel, self).save()

    def save(self):
        self.published = True
        super(MyModel, self).save()

Таким образом, всякий раз, когда редактирование фиксируется, оно видимо всем пользователям... но другие по-прежнему могут удалять элементы. Одним из преимуществ этой техники является то, что вам не нужно иметь никакой дополнительной логики для блокировки элементов и представления пользователю другого пользовательского интерфейса. Недостатками являются дополнительное пространство, используемое в таблице базы данных, и редкие обстоятельства, когда удаленный элемент "волшебным образом" появляется снова.

(Это, вероятно, только отправная точка. Если вы выбрали этот путь, вы, вероятно, захотите внести изменения в эту идею в зависимости от вашего варианта использования.)

Я бы предложил простую блокировку чтения-записи, поскольку вы не хотите блокировать одновременный доступ пользователей к объекту (только от редактирования).

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

Во-первых, вам нужно знать, что есть два механизма блокировки: оптимистическая и пессимистическая блокировка. Чем они отличаются, объясняется здесь. Насколько я знаю, нет пакета, реализующего пессимистическую блокировку для Django. Для оптимистичной блокировки наиболее полнофункциональной библиотекой является django-concurrency.

В принятом ответе между проверками все еще могут быть условия гонки.obj.lockedи установим для него значение true. Это можно исправить, используя отдельную модель.

Существует также вероятность взаимоблокировок (если объект никогда не сохраняется). Это можно исправить с помощью контекстного менеджера.

Поэтому я считаю, что эта версия имеет некоторые преимущества:

      class SomeModel(models.Model):
    @contextlib.contextmanager
    def lock(self):
        # raises IntegrityError if already locked
        lock = SomeLock.objects.create(obj=self)
        try:
            yield
        finally:
            lock.delete()

class SomeLock(models.Model):
    obj = models.ForeignKey(SomeModel, on_delete=models.CASCADE, unique=true)
Другие вопросы по тегам