diff --git a/app/jobs/internal/remove_orphan_conversations_job.rb b/app/jobs/internal/remove_orphan_conversations_job.rb new file mode 100644 index 000000000..e88611129 --- /dev/null +++ b/app/jobs/internal/remove_orphan_conversations_job.rb @@ -0,0 +1,11 @@ +# housekeeping +# remove conversations that do not have a contact_id +# orphan conversations without contact cannot be accessed or used + +class Internal::RemoveOrphanConversationsJob < ApplicationJob + queue_as :housekeeping + + def perform + Internal::RemoveOrphanConversationsService.new.perform + end +end diff --git a/app/services/internal/remove_orphan_conversations_service.rb b/app/services/internal/remove_orphan_conversations_service.rb new file mode 100644 index 000000000..c52d6082b --- /dev/null +++ b/app/services/internal/remove_orphan_conversations_service.rb @@ -0,0 +1,34 @@ +class Internal::RemoveOrphanConversationsService + def initialize(account: nil, days: 1) + @account = account + @days = days + end + + def perform + orphan_conversations = build_orphan_conversations_query + total_deleted = 0 + + Rails.logger.info '[RemoveOrphanConversationsService] Starting removal of orphan conversations' + + orphan_conversations.find_in_batches(batch_size: 1000) do |batch| + conversation_ids = batch.map(&:id) + Conversation.where(id: conversation_ids).destroy_all + total_deleted += batch.size + Rails.logger.info "[RemoveOrphanConversationsService] Deleted #{batch.size} orphan conversations (#{total_deleted} total)" + end + + Rails.logger.info "[RemoveOrphanConversationsService] Completed. Total deleted: #{total_deleted}" + total_deleted + end + + private + + def build_orphan_conversations_query + base = @account ? @account.conversations : Conversation.all + base = base.where('conversations.created_at > ?', @days.days.ago) + base = base.left_outer_joins(:contact, :inbox) + + # Find conversations whose associated contact or inbox record is missing + base.where(contacts: { id: nil }).or(base.where(inboxes: { id: nil })) + end +end diff --git a/config/schedule.yml b/config/schedule.yml index cae9b94c8..96e4cfc4f 100644 --- a/config/schedule.yml +++ b/config/schedule.yml @@ -66,3 +66,10 @@ remove_old_notification_job: cron: '30 22 * * *' class: 'Notification::RemoveOldNotificationJob' queue: purgable + +# executed every 12 hours +# to remove orphan conversations without contact +remove_orphan_conversations_job: + cron: '0 */12 * * *' + class: 'Internal::RemoveOrphanConversationsJob' + queue: housekeeping diff --git a/lib/tasks/ops/cleanup_orphan_conversations.rake b/lib/tasks/ops/cleanup_orphan_conversations.rake index f6574b396..20cb207d6 100644 --- a/lib/tasks/ops/cleanup_orphan_conversations.rake +++ b/lib/tasks/ops/cleanup_orphan_conversations.rake @@ -15,13 +15,13 @@ namespace :chatwoot do days_input = $stdin.gets.strip days = days_input.empty? ? 7 : days_input.to_i - # Build a common base relation with identical joins for OR compatibility + service = Internal::RemoveOrphanConversationsService.new(account: account, days: days) + + # Preview count using the same query logic base = account .conversations .where('conversations.created_at > ?', days.days.ago) .left_outer_joins(:contact, :inbox) - - # Find conversations whose associated contact or inbox record is missing conversations = base.where(contacts: { id: nil }).or(base.where(inboxes: { id: nil })) count = conversations.count @@ -31,8 +31,8 @@ namespace :chatwoot do print 'Do you want to delete these conversations? (y/N): ' confirm = $stdin.gets.strip.downcase if %w[y yes].include?(confirm) - conversations.destroy_all - puts 'Conversations deleted.' + total_deleted = service.perform + puts "#{total_deleted} conversations deleted." else puts 'No conversations were deleted.' end