From f138be4c76dd4adb79273a1ef939349f9bd9824e Mon Sep 17 00:00:00 2001 From: Dheeraj Kumar Ketireddy Date: Wed, 10 Dec 2025 18:17:33 +0530 Subject: [PATCH] [WEB-4440] fix: Fix duplicate sequence when creating multiple workitems in rapid succession - Replace advisory lock with transaction-level lock in Issue model save method - Updated the save method in the Issue model to use a transaction-level advisory lock for better concurrency control. - Simplified the locking mechanism by removing the explicit unlock step, as the lock is automatically released at the end of the transaction. - Maintained existing functionality for sequence and sort order management while improving code clarity. --- apps/api/plane/db/models/issue.py | 54 ++++++++++++++----------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/apps/api/plane/db/models/issue.py b/apps/api/plane/db/models/issue.py index 05bb2fe5b91..07ebdf0a41e 100644 --- a/apps/api/plane/db/models/issue.py +++ b/apps/api/plane/db/models/issue.py @@ -207,39 +207,35 @@ def save(self, *args, **kwargs): if self._state.adding: with transaction.atomic(): - # Create a lock for this specific project using an advisory lock + # Create a lock for this specific project using a transaction-level advisory lock # This ensures only one transaction per project can execute this code at a time + # The lock is automatically released when the transaction ends lock_key = convert_uuid_to_integer(self.project.id) with connection.cursor() as cursor: - # Get an exclusive lock using the project ID as the lock key - cursor.execute("SELECT pg_advisory_lock(%s)", [lock_key]) - - try: - # Get the last sequence for the project - last_sequence = IssueSequence.objects.filter(project=self.project).aggregate( - largest=models.Max("sequence") - )["largest"] - self.sequence_id = last_sequence + 1 if last_sequence else 1 - # Strip the html tags using html parser - self.description_stripped = ( - None - if (self.description_html == "" or self.description_html is None) - else strip_tags(self.description_html) - ) - largest_sort_order = Issue.objects.filter(project=self.project, state=self.state).aggregate( - largest=models.Max("sort_order") - )["largest"] - if largest_sort_order is not None: - self.sort_order = largest_sort_order + 10000 - - super(Issue, self).save(*args, **kwargs) - - IssueSequence.objects.create(issue=self, sequence=self.sequence_id, project=self.project) - finally: - # Release the lock - with connection.cursor() as cursor: - cursor.execute("SELECT pg_advisory_unlock(%s)", [lock_key]) + # Get an exclusive transaction-level lock using the project ID as the lock key + cursor.execute("SELECT pg_advisory_xact_lock(%s)", [lock_key]) + + # Get the last sequence for the project + last_sequence = IssueSequence.objects.filter(project=self.project).aggregate( + largest=models.Max("sequence") + )["largest"] + self.sequence_id = last_sequence + 1 if last_sequence else 1 + # Strip the html tags using html parser + self.description_stripped = ( + None + if (self.description_html == "" or self.description_html is None) + else strip_tags(self.description_html) + ) + largest_sort_order = Issue.objects.filter(project=self.project, state=self.state).aggregate( + largest=models.Max("sort_order") + )["largest"] + if largest_sort_order is not None: + self.sort_order = largest_sort_order + 10000 + + super(Issue, self).save(*args, **kwargs) + + IssueSequence.objects.create(issue=self, sequence=self.sequence_id, project=self.project) else: # Strip the html tags using html parser self.description_stripped = (