Race Conditions with ModelViewset Requests

30 Views Asked by At

I'm experiencing an issue with a Django Rest Framework ModelViewSet endpoint named projects/. I have a set of requests (PATCH, DELETE, then GET) that are causing unexpected behavior. The timeline of requests and responses is as follows:

  1. PATCH request at 14:45:09.420
  2. DELETE request at 14:45:12.724 3.DELETE 204 response at 14:45:12.852
  3. PATCH 200 response at 14:45:13.263
  4. GET request at 14:45:13.279
  5. GET 200 response at 14:45:13.714 All responses indicate success. However, the GET response, which follows a DELETE, includes the supposedly deleted model. If I call the GET endpoint a bit later, the deleted model is no longer listed.

This behavior suggests a potential race condition or a caching issue, where the PATCH operation completes after the DELETE, or the GET request returns a cached list not reflecting the deletion.

The view, serializer and model code are all pretty vanilla:

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"
    )

One model has a foreign key reference to this model, but the on_delete behavior is to set it to null.

I'm running this with Google Cloud Run, a serverless backend service

1

There are 1 best solutions below

0
merhoo On

The Rest Framework mixins are not atomic operations so its necessary to use custom mixins for UPDATE and DELETE operations:

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)