This PR fixes the HTTP 500 timeout errors occurring when deleting SLA
policies that have large volumes of historical data.
The fix moves the deletion workflow to asynchronous background
processing using the existing `DeleteObjectJob`.
By offloading heavy cascaded deletions (applied SLAs, SLA events,
conversation nullifications) from the request cycle, the API can now
return immediately while the cleanup continues in the background
avoiding the `Rack::Timeout::RequestTimeoutException`. This ensures that
SLA policies can be deleted reliably, regardless of data size.
### Problem
Deleting an SLA policy via `DELETE
/api/v1/accounts/{account_id}/sla_policies/{id}` fails consistently with
`Rack::Timeout::RequestTimeoutException (15s)` for policies with large
amounts of related data.
Because the current implementation performs all dependent deletions
**synchronously**, Rails processes:
- `has_many :applied_slas, dependent: :destroy` (thousands)
- Each `AppliedSla#destroy` → triggers destruction of many `SlaEvent`
records
- `has_many :conversations, dependent: :nullify` (thousands)
This processing far exceeds the Rack timeout window and consistently
triggers HTTP 500 errors for users.
### Solution
This PR applies the same pattern used successfully in Inbox deletion.
**Move deletion to async background jobs**
- Uses `DeleteObjectJob` for centralized, reliable cleanup.
- Allows the DELETE API call to respond immediately.
**Chunk large datasets**
- Records are processed in **batches of 5,000** to reduce DB load and
avoid job timeouts.
177 lines
5.6 KiB
Ruby
177 lines
5.6 KiB
Ruby
# Generate Bulk Conversations
|
|
#
|
|
# This task creates bulk conversations with fake contacts and movie dialogue messages
|
|
# for testing purposes. Each conversation gets random messages between contacts and agents.
|
|
#
|
|
# Usage Examples:
|
|
# # Using arguments (may need escaping in some shells)
|
|
# bundle exec rake "conversations:generate_bulk[100,1,1]"
|
|
#
|
|
# # Using environment variables (recommended)
|
|
# COUNT=100 ACCOUNT_ID=1 INBOX_ID=1 bundle exec rake conversations:generate_bulk
|
|
#
|
|
# # Generate 50 conversations
|
|
# COUNT=50 ACCOUNT_ID=1 INBOX_ID=1 bundle exec rake conversations:generate_bulk
|
|
#
|
|
# Parameters:
|
|
# COUNT: Number of conversations to create (default: 10)
|
|
# ACCOUNT_ID: ID of the account (required)
|
|
# INBOX_ID: ID of the inbox that belongs to the account (required)
|
|
#
|
|
# What it creates:
|
|
# - Unique contacts with fake names, emails, phone numbers
|
|
# - Conversations with random status (open/resolved/pending)
|
|
# - 3-10 messages per conversation with movie quotes
|
|
# - Alternating incoming/outgoing message flow
|
|
#
|
|
# Notes:
|
|
# - Only runs in development environment
|
|
# - Creates realistic test data for conversation testing
|
|
# - Progress shown every 10 conversations
|
|
# - All contacts get unique email addresses to avoid conflicts
|
|
#
|
|
# rubocop:disable Metrics/BlockLength
|
|
namespace :conversations do
|
|
desc 'Generate bulk conversations with contacts and movie dialogue messages'
|
|
task :generate_bulk, [:count, :account_id, :inbox_id] => :environment do |_t, args|
|
|
unless Rails.env.development?
|
|
puts 'This task can only be run in the development environment.'
|
|
puts "Current environment: #{Rails.env}"
|
|
exit(1)
|
|
end
|
|
|
|
count = (args[:count] || ENV['COUNT'] || 10).to_i
|
|
account_id = args[:account_id] || ENV.fetch('ACCOUNT_ID', nil)
|
|
inbox_id = args[:inbox_id] || ENV.fetch('INBOX_ID', nil)
|
|
|
|
if account_id.blank?
|
|
puts 'Error: ACCOUNT_ID is required'
|
|
puts 'Usage: bundle exec rake conversations:generate_bulk[count,account_id,inbox_id]'
|
|
puts 'Or: COUNT=100 ACCOUNT_ID=1 INBOX_ID=1 bundle exec rake conversations:generate_bulk'
|
|
exit(1)
|
|
end
|
|
|
|
if inbox_id.blank?
|
|
puts 'Error: INBOX_ID is required'
|
|
puts 'Usage: bundle exec rake conversations:generate_bulk[count,account_id,inbox_id]'
|
|
puts 'Or: COUNT=100 ACCOUNT_ID=1 INBOX_ID=1 bundle exec rake conversations:generate_bulk'
|
|
exit(1)
|
|
end
|
|
|
|
account = Account.find_by(id: account_id)
|
|
inbox = Inbox.find_by(id: inbox_id)
|
|
|
|
unless account
|
|
puts "Error: Account with ID #{account_id} not found"
|
|
exit(1)
|
|
end
|
|
|
|
unless inbox
|
|
puts "Error: Inbox with ID #{inbox_id} not found"
|
|
exit(1)
|
|
end
|
|
|
|
unless inbox.account_id == account.id
|
|
puts "Error: Inbox #{inbox_id} does not belong to Account #{account_id}"
|
|
exit(1)
|
|
end
|
|
|
|
puts "Generating #{count} conversations for Account ##{account.id} in Inbox ##{inbox.id}..."
|
|
puts "Started at: #{Time.current}"
|
|
|
|
start_time = Time.current
|
|
created_count = 0
|
|
|
|
count.times do |i|
|
|
contact = create_contact(account)
|
|
contact_inbox = create_contact_inbox(contact, inbox)
|
|
conversation = create_conversation(contact_inbox)
|
|
add_messages(conversation)
|
|
|
|
created_count += 1
|
|
puts "Created conversation #{i + 1}/#{count} (ID: #{conversation.id})" if ((i + 1) % 10).zero?
|
|
rescue StandardError => e
|
|
puts "Error creating conversation #{i + 1}: #{e.message}"
|
|
puts e.backtrace.first(5).join("\n")
|
|
end
|
|
|
|
elapsed_time = Time.current - start_time
|
|
puts "\nCompleted!"
|
|
puts "Successfully created: #{created_count} conversations"
|
|
puts "Total time: #{elapsed_time.round(2)}s"
|
|
puts "Average time per conversation: #{(elapsed_time / created_count).round(3)}s" if created_count.positive?
|
|
end
|
|
|
|
def create_contact(account)
|
|
Contact.create!(
|
|
account: account,
|
|
name: Faker::Name.name,
|
|
email: "#{SecureRandom.uuid}@example.com",
|
|
phone_number: generate_e164_phone_number,
|
|
additional_attributes: {
|
|
source: 'bulk_generator',
|
|
company: Faker::Company.name,
|
|
city: Faker::Address.city
|
|
}
|
|
)
|
|
end
|
|
|
|
def generate_e164_phone_number
|
|
country_code = [1, 44, 61, 91, 81].sample
|
|
subscriber_number = rand(1_000_000..9_999_999_999).to_s
|
|
subscriber_number = subscriber_number[0...(15 - country_code.to_s.length)]
|
|
"+#{country_code}#{subscriber_number}"
|
|
end
|
|
|
|
def create_contact_inbox(contact, inbox)
|
|
ContactInboxBuilder.new(
|
|
contact: contact,
|
|
inbox: inbox
|
|
).perform
|
|
end
|
|
|
|
def create_conversation(contact_inbox)
|
|
ConversationBuilder.new(
|
|
params: ActionController::Parameters.new(
|
|
status: %w[open resolved pending].sample,
|
|
additional_attributes: {},
|
|
custom_attributes: {}
|
|
),
|
|
contact_inbox: contact_inbox
|
|
).perform
|
|
end
|
|
|
|
def add_messages(conversation)
|
|
num_messages = rand(3..10)
|
|
message_type = %w[incoming outgoing].sample
|
|
|
|
num_messages.times do
|
|
message_type = message_type == 'incoming' ? 'outgoing' : 'incoming'
|
|
create_message(conversation, message_type)
|
|
end
|
|
end
|
|
|
|
def create_message(conversation, message_type)
|
|
sender = if message_type == 'incoming'
|
|
conversation.contact
|
|
else
|
|
conversation.account.users.sample || conversation.account.administrators.first
|
|
end
|
|
|
|
conversation.messages.create!(
|
|
account: conversation.account,
|
|
inbox: conversation.inbox,
|
|
sender: sender,
|
|
message_type: message_type,
|
|
content: generate_movie_dialogue,
|
|
content_type: :text,
|
|
private: false
|
|
)
|
|
end
|
|
|
|
def generate_movie_dialogue
|
|
Faker::Movie.quote
|
|
end
|
|
end
|
|
# rubocop:enable Metrics/BlockLength
|