feat: add cron job to remove orphan conversations (#13335)

## Description

This PR includes cron job to delete the orphans

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Introduces a scheduled cleanup for conversations missing `contact` or
`inbox`.
> 
> - Adds `Internal::RemoveOrphanConversationsService` to batch-delete
orphan conversations (scoped by optional `account`, within a
configurable `days` window) with progress logging
> - New `Internal::RemoveOrphanConversationsJob` that invokes the
service; scheduled via `config/schedule.yml` to run every 12 hours on
`housekeeping` queue
> - Refactors rake task `chatwoot:ops:cleanup_orphan_conversations` to
use the service and report `total_deleted` after confirmation
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
59a24715cc59f048d08db3f588cde6fa036f3166. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
Tanmay Deep Sharma
2026-01-28 19:25:20 +05:30
committed by GitHub
parent aaeea6c9bf
commit d166ae73bc
4 changed files with 57 additions and 5 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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