From f28081a72aa8cfe056179e4fb9056feb63e90c4e Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Wed, 12 Feb 2025 10:13:41 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(backend)=20race=20condition=20crea?= =?UTF-8?q?te=20doc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When 2 docs are created almost at the same time, the second one will fail because the first one. We get a unicity error on the path key already used ("impress_document_path_key"). To fix this issue, we will lock the table the time to create the document, the next query will wait for the lock to be released. --- CHANGELOG.md | 4 +++ src/backend/core/api/viewsets.py | 8 ++++- .../documents/test_api_documents_create.py | 30 +++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ffda055..d24ed8fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ and this project adheres to - ♻️Stop setting a default title on doc creation #634 +## Fixed + +- 🐛(backend) race condition create doc #633 + ## [2.2.0] - 2025-02-10 ## Added diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index e9616aff..9caafb63 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -12,8 +12,8 @@ from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.search import TrigramSimilarity from django.core.exceptions import ValidationError from django.core.files.storage import default_storage +from django.db import connection, transaction from django.db import models as db -from django.db import transaction from django.db.models.expressions import RawSQL from django.db.models.functions import Left, Length from django.http import Http404 @@ -573,6 +573,12 @@ class DocumentViewSet( @transaction.atomic def perform_create(self, serializer): """Set the current user as creator and owner of the newly created object.""" + with connection.cursor() as cursor: + cursor.execute( + f'LOCK TABLE "{models.Document._meta.db_table}" ' # noqa: SLF001 + "IN SHARE ROW EXCLUSIVE MODE;" + ) + obj = models.Document.add_root( creator=self.request.user, **serializer.validated_data, diff --git a/src/backend/core/tests/documents/test_api_documents_create.py b/src/backend/core/tests/documents/test_api_documents_create.py index 151724e0..5448ad81 100644 --- a/src/backend/core/tests/documents/test_api_documents_create.py +++ b/src/backend/core/tests/documents/test_api_documents_create.py @@ -2,6 +2,7 @@ Tests for Documents API endpoint in impress's core app: create """ +from concurrent.futures import ThreadPoolExecutor from uuid import uuid4 import pytest @@ -51,6 +52,35 @@ def test_api_documents_create_authenticated_success(): assert document.accesses.filter(role="owner", user=user).exists() +def test_api_documents_create_document_race_condition(): + """ + It should be possible to create several documents at the same time + without causing any race conditions or data integrity issues. + """ + + def create_document(title): + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + return client.post( + "/api/v1.0/documents/", + { + "title": title, + }, + format="json", + ) + + with ThreadPoolExecutor(max_workers=2) as executor: + future1 = executor.submit(create_document, "my document 1") + future2 = executor.submit(create_document, "my document 2") + + response1 = future1.result() + response2 = future2.result() + + assert response1.status_code == 201 + assert response2.status_code == 201 + + def test_api_documents_create_authenticated_title_null(): """It should be possible to create several documents with a null title.""" user = factories.UserFactory()