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.
101 lines
3.9 KiB
Ruby
101 lines
3.9 KiB
Ruby
# Apply SLA Policy to Conversations
|
|
#
|
|
# This task applies an SLA policy to existing conversations that don't have one assigned.
|
|
# It processes conversations in batches and only affects conversations with sla_policy_id = nil.
|
|
#
|
|
# Usage Examples:
|
|
# # Using arguments (may need escaping in some shells)
|
|
# bundle exec rake "sla:apply_to_conversations[19,1,500]"
|
|
#
|
|
# # Using environment variables (recommended)
|
|
# SLA_POLICY_ID=19 ACCOUNT_ID=1 BATCH_SIZE=500 bundle exec rake sla:apply_to_conversations
|
|
#
|
|
# Parameters:
|
|
# SLA_POLICY_ID: ID of the SLA policy to apply (required)
|
|
# ACCOUNT_ID: ID of the account (required)
|
|
# BATCH_SIZE: Number of conversations to process (default: 1000)
|
|
#
|
|
# Notes:
|
|
# - Only runs in development environment
|
|
# - Processes conversations in order of newest first (id DESC)
|
|
# - Safe to run multiple times - skips conversations that already have SLA policies
|
|
# - Creates AppliedSla records automatically via Rails callbacks
|
|
# - SlaEvent records are created later by background jobs when violations occur
|
|
#
|
|
# rubocop:disable Metrics/BlockLength
|
|
namespace :sla do
|
|
desc 'Apply SLA policy to existing conversations'
|
|
task :apply_to_conversations, [:sla_policy_id, :account_id, :batch_size] => :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
|
|
|
|
sla_policy_id = args[:sla_policy_id] || ENV.fetch('SLA_POLICY_ID', nil)
|
|
account_id = args[:account_id] || ENV.fetch('ACCOUNT_ID', nil)
|
|
batch_size = (args[:batch_size] || ENV['BATCH_SIZE'] || 1000).to_i
|
|
|
|
if sla_policy_id.blank?
|
|
puts 'Error: SLA_POLICY_ID is required'
|
|
puts 'Usage: bundle exec rake sla:apply_to_conversations[sla_policy_id,account_id,batch_size]'
|
|
puts 'Or: SLA_POLICY_ID=1 ACCOUNT_ID=1 BATCH_SIZE=500 bundle exec rake sla:apply_to_conversations'
|
|
exit(1)
|
|
end
|
|
|
|
if account_id.blank?
|
|
puts 'Error: ACCOUNT_ID is required'
|
|
puts 'Usage: bundle exec rake sla:apply_to_conversations[sla_policy_id,account_id,batch_size]'
|
|
puts 'Or: SLA_POLICY_ID=1 ACCOUNT_ID=1 BATCH_SIZE=500 bundle exec rake sla:apply_to_conversations'
|
|
exit(1)
|
|
end
|
|
|
|
account = Account.find_by(id: account_id)
|
|
unless account
|
|
puts "Error: Account with ID #{account_id} not found"
|
|
exit(1)
|
|
end
|
|
|
|
sla_policy = account.sla_policies.find_by(id: sla_policy_id)
|
|
unless sla_policy
|
|
puts "Error: SLA Policy with ID #{sla_policy_id} not found for Account #{account_id}"
|
|
exit(1)
|
|
end
|
|
|
|
conversations = account.conversations.where(sla_policy_id: nil).order(id: :desc).limit(batch_size)
|
|
total_count = conversations.count
|
|
|
|
if total_count.zero?
|
|
puts 'No conversations found without SLA policy'
|
|
exit(0)
|
|
end
|
|
|
|
puts "Applying SLA Policy '#{sla_policy.name}' (ID: #{sla_policy_id}) to #{total_count} conversations in Account #{account_id}"
|
|
puts "Processing in batches of #{batch_size}"
|
|
puts "Started at: #{Time.current}"
|
|
|
|
start_time = Time.current
|
|
processed_count = 0
|
|
error_count = 0
|
|
|
|
conversations.find_in_batches(batch_size: batch_size) do |batch|
|
|
batch.each do |conversation|
|
|
conversation.update!(sla_policy_id: sla_policy_id)
|
|
processed_count += 1
|
|
puts "Processed #{processed_count}/#{total_count} conversations" if (processed_count % 100).zero?
|
|
rescue StandardError => e
|
|
error_count += 1
|
|
puts "Error applying SLA to conversation #{conversation.id}: #{e.message}"
|
|
end
|
|
end
|
|
|
|
elapsed_time = Time.current - start_time
|
|
puts "\nCompleted!"
|
|
puts "Successfully processed: #{processed_count} conversations"
|
|
puts "Errors encountered: #{error_count}" if error_count.positive?
|
|
puts "Total time: #{elapsed_time.round(2)}s"
|
|
puts "Average time per conversation: #{(elapsed_time / processed_count).round(3)}s" if processed_count.positive?
|
|
end
|
|
end
|
|
# rubocop:enable Metrics/BlockLength
|