From 0618e6e229130022a583883699d7a2421bd79a8a Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma Date: Thu, 30 Apr 2026 18:46:09 +0700 Subject: [PATCH] fix(voice): close lock-window gaps in accept, throttle, and call_attributes_changed? --- .../v1/accounts/whatsapp_calls_controller.rb | 24 +++++++++++++++---- .../app/models/enterprise/conversation.rb | 5 +++- .../app/services/whatsapp/call_service.rb | 12 ++++++---- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/enterprise/app/controllers/api/v1/accounts/whatsapp_calls_controller.rb b/enterprise/app/controllers/api/v1/accounts/whatsapp_calls_controller.rb index 077bdec83..14fd5cee3 100644 --- a/enterprise/app/controllers/api/v1/accounts/whatsapp_calls_controller.rb +++ b/enterprise/app/controllers/api/v1/accounts/whatsapp_calls_controller.rb @@ -113,14 +113,28 @@ class Api::V1::Accounts::WhatsappCallsController < Api::V1::Accounts::BaseContro end # 138006 = no call permission yet; send opt-in template (throttled) and surface state to FE. + # Lock the conversation so concurrent initiate requests can't both pass the throttle gate + # and double-send the opt-in template. def render_permission_request - return render json: { status: 'permission_pending' } if permission_request_throttled? + status = nil + @conversation.with_lock do + if permission_request_throttled? + status = 'permission_pending' + next + end - sent = provider_service.send_call_permission_request(@conversation.contact.phone_number.delete('+')) - return render_could_not_create_error(I18n.t('errors.whatsapp.calls.permission_request_failed')) unless sent + sent = provider_service.send_call_permission_request(@conversation.contact.phone_number.delete('+')) + if sent + record_permission_request_wamid(sent) + status = 'permission_requested' + else + status = 'failed' + end + end - record_permission_request_wamid(sent) - render json: { status: 'permission_requested' } + return render_could_not_create_error(I18n.t('errors.whatsapp.calls.permission_request_failed')) if status == 'failed' + + render json: { status: status } end def permission_request_throttled? diff --git a/enterprise/app/models/enterprise/conversation.rb b/enterprise/app/models/enterprise/conversation.rb index c5d7dde85..077e0e932 100644 --- a/enterprise/app/models/enterprise/conversation.rb +++ b/enterprise/app/models/enterprise/conversation.rb @@ -40,6 +40,9 @@ module Enterprise::Conversation def call_attributes_changed? return false if previous_changes['additional_attributes'].blank? - previous_changes['additional_attributes'][1].keys.intersect?(%w[call_status call_direction]) + # Compare before/after values for call keys — checking key presence alone + # rebroadcasts on any unrelated additional_attributes write once the keys exist. + before, after = previous_changes['additional_attributes'] + %w[call_status call_direction].any? { |key| (before || {})[key] != (after || {})[key] } end end diff --git a/enterprise/app/services/whatsapp/call_service.rb b/enterprise/app/services/whatsapp/call_service.rb index fee97bfa6..2683332a0 100644 --- a/enterprise/app/services/whatsapp/call_service.rb +++ b/enterprise/app/services/whatsapp/call_service.rb @@ -4,10 +4,14 @@ class Whatsapp::CallService def accept raise Voice::CallErrors::CallFailed, 'sdp_answer is required' if sdp_answer.blank? - call.with_lock { transition_to_in_progress! } - update_message_status('in_progress') - update_conversation_call_status(call.display_status) - broadcast(:accepted, accepted_by_agent_id: agent.id) + # All side effects under the lock so a concurrent terminate cannot finalize + # the call between status update and the message/conversation/broadcast writes. + call.with_lock do + transition_to_in_progress! + update_message_status('in_progress') + update_conversation_call_status(call.display_status) + broadcast(:accepted, accepted_by_agent_id: agent.id) + end call end