Неожиданное поведение при одновременных запросах ModelViewset
У меня возникла проблема с конечной точкой ModelViewSet в Django Rest Framework с именем projects/
. У меня есть набор запросов (PATCH, DELETE, затем GET), которые вызывают неожиданное поведение. Временная шкала запросов и ответов выглядит следующим образом:
- Запрос PATCH в 14:45:09.420
- DELETE запрос в 14:45:12.724 3.Ответ DELETE 204 в 14:45:12.852
- PATCH 200 ответ в 14:45:13.263
- GET запрос в 14:45:13.279
- GET 200 ответ в 14:45:13.714 Все ответы свидетельствуют об успехе. Однако ответ GET, который следует за DELETE, включает якобы удаленную модель. Если я вызову конечную точку GET немного позже, удаленная модель больше не будет указана.
Такое поведение говорит о потенциальном состоянии гонки или проблеме с кэшированием, когда операция PATCH завершается после DELETE, или запрос GET возвращает кэшированный список, не отражающий удаление.
Код представления, сериализатора и модели довольно ванильный:
class ProjectViewSet(ModelViewSet):
parser_classes = (MultiPartParser, FormParser, JSONParser)
queryset = Project.objects.all()
serializer_class = ProjectSerializer
pagination_class = ProjectPagination
class ProjectSerializer(serializers.ModelSerializer):
creator = UserUUIDField(default=serializers.CurrentUserDefault())
image = serializers.ImageField(required=False)
class Meta:
model = Project
fields = "__all__"
class Project(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
creator = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
image = models.ForeignKey(
wand_image,
on_delete=models.DO_NOTHING,
null=True,
blank=True,
related_name="projects"
)
У одной модели есть внешняя ключевая ссылка на эту модель, но поведение on_delete заключается в установке ее в null.
Я запускаю это с Google Cloud Run, бессерверным бэкэнд-сервисом
Миксины Rest Framework не являются атомарными операциями, поэтому для операций UPDATE и DELETE необходимо использовать собственные миксины:
class AtomicDestroyModelMixin:
"""
Destroy a model instance.
"""
def destroy(self, request, *args, **kwargs):
try:
with transaction.atomic():
instance = self.get_object()
self.perform_destroy(instance)
except OperationalError:
raise DatabaseOperationException()
return Response(status=status.HTTP_204_NO_CONTENT)
def perform_destroy(self, instance):
instance.delete()
class AtomicUpdateModelMixin:
"""
Update a model instance.
"""
def update(self, request, *args, **kwargs):
try:
with transaction.atomic():
partial = kwargs.pop("partial", False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
if getattr(instance, "_prefetched_objects_cache", None):
instance._prefetched_objects_cache = {}
except OperationalError:
raise DatabaseOperationException()
return Response(serializer.data)
def perform_update(self, serializer):
serializer.save()
def partial_update(self, request, *args, **kwargs):
kwargs["partial"] = True
return self.update(request, *args, **kwargs)