Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 52 additions & 25 deletions care/emr/api/viewsets/scheduling/booking.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,52 @@ def available_users(self, request, *args, **kwargs):
}
)

@classmethod
def generate_token_handler(
cls,
booking: TokenBooking,
category: TokenCategory,
queue: TokenQueue | None = None,
note: str | None = None,
):
if booking.token:
raise ValidationError("Token already generated")

if queue is None:
# slot may start at 1:00 IST (19:30 UTC of previous date), hence
# making it tz naive and adding 1 second to ensure correct date extraction
token_date = timezone.make_naive(
booking.token_slot.start_datetime + timedelta(seconds=1)
).date()
filters = {
"facility": booking.token_slot.resource.facility,
"resource": booking.token_slot.resource,
"date": token_date,
}
queue_exists = TokenQueue.objects.filter(**filters).exists()
filters["system_generated"] = True
queue = TokenQueue.objects.filter(**filters).first()
if not queue:
filters["name"] = "System Generated"
if not queue_exists:
filters["is_primary"] = True
queue = TokenQueue.objects.create(**filters)
Comment on lines +297 to +315
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Race condition in queue creation logic.

The queue existence check (line 308) and queue creation (line 315) are not atomic and occur outside the lock. Under concurrent requests, two threads could both see queue_exists = False and each create a system-generated queue with is_primary = True, resulting in duplicate queues.

Consider using get_or_create to atomically handle this, or extend the lock scope to include queue resolution.

🔒 Proposed fix using get_or_create
         if queue is None:
             # slot may start at 1:00 IST (19:30 UTC of previous date), hence
             # making it tz naive and adding 1 second to ensure correct date extraction
             token_date = timezone.make_naive(
                 booking.token_slot.start_datetime + timedelta(seconds=1)
             ).date()
             filters = {
                 "facility": booking.token_slot.resource.facility,
                 "resource": booking.token_slot.resource,
                 "date": token_date,
             }
-            queue_exists = TokenQueue.objects.filter(**filters).exists()
-            filters["system_generated"] = True
-            queue = TokenQueue.objects.filter(**filters).first()
-            if not queue:
-                filters["name"] = "System Generated"
-                if not queue_exists:
-                    filters["is_primary"] = True
-                queue = TokenQueue.objects.create(**filters)
+            with transaction.atomic():
+                queue_exists = TokenQueue.objects.filter(**filters).exists()
+                queue, created = TokenQueue.objects.get_or_create(
+                    **filters,
+                    system_generated=True,
+                    defaults={
+                        "name": "System Generated",
+                        "is_primary": not queue_exists,
+                    },
+                )
🤖 Prompt for AI Agents
In `@care/emr/api/viewsets/scheduling/booking.py` around lines 297 - 315, The code
checks TokenQueue.exists() and then creates a system-generated queue outside the
lock, causing a race where two requests can both create primary queues; replace
the separate exists()/create() flow with an atomic
TokenQueue.objects.get_or_create(...) call using the same filters (facility,
resource, date, system_generated=True, name="System Generated") and supply
defaults including is_primary=True only when creating, or wrap the get_or_create
inside the existing lock/transaction.atomic to ensure only one primary queue is
created; update the block that references booking.token_slot,
TokenQueue.objects.filter(...).exists(), .first(), and .create(...) to use
get_or_create with appropriate defaults.

with Lock(f"booking:token:{queue.id}"), transaction.atomic():
number = Token.objects.filter(queue=queue, category=category).count() + 1
token = Token.objects.create(
facility=booking.token_slot.resource.facility,
queue=queue,
category=category,
number=number,
status=TokenStatusOptions.CREATED.value,
note=note,
booking=booking,
patient=booking.patient,
)
booking.token = token
booking.save(update_fields=["token"])
return token

@extend_schema(
request=TokenGenerationSpec,
)
Expand All @@ -291,8 +337,6 @@ def generate_token(self, request, *args, **kwargs):
booking = self.get_object()
self.authorize_update({}, booking)
request_data = TokenGenerationSpec(**request.data)
if booking.token:
raise ValidationError("Token already generated")
# slot may start at 1:00 IST (19:30 UTC of previous date), hence
# making it tz naive and adding 1 second to ensure correct date extraction
token_date = timezone.make_naive(
Expand All @@ -310,36 +354,19 @@ def generate_token(self, request, *args, **kwargs):
if not queue:
raise ValidationError("Queue not found")
else:
queue_exists = TokenQueue.objects.filter(**filters).exists()
filters["system_generated"] = True
queue = TokenQueue.objects.filter(**filters).first()
if not queue:
filters["name"] = "System Generated"
if not queue_exists:
filters["is_primary"] = True
queue = TokenQueue.objects.create(**filters)
queue = None

category = TokenCategory.objects.filter(
facility=booking.token_slot.resource.facility,
resource_type=booking.token_slot.resource.resource_type,
external_id=request_data.category,
).first()
if not category:
raise ValidationError("Category not found")
note = request_data.note
with Lock(f"booking:token:{queue.id}"), transaction.atomic():
number = Token.objects.filter(queue=queue, category=category).count() + 1
token = Token.objects.create(
facility=booking.token_slot.resource.facility,
queue=queue,
category=category,
number=number,
status=TokenStatusOptions.CREATED.value,
note=note,
booking=booking,
patient=booking.patient,
)
booking.token = token
booking.save(update_fields=["token"])

token = self.generate_token_handler(
booking, category, queue=queue, note=request_data.note
)
return Response(TokenReadSpec.serialize(token).to_json())


Expand Down