From 677888bcde1c2071d05fbecc53d73178a31a27dd Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Tue, 7 Nov 2023 09:54:30 +0530 Subject: [PATCH] feat: Add job to remove stale contact inboxes (#8096) --- .env.example | 6 +++ .../remove_stale_contact_inboxes_job.rb | 11 +++++ app/models/contact_inbox.rb | 7 +++ .../remove_stale_contact_inboxes_service.rb | 43 +++++++++++++++++++ config/schedule.yml | 7 +++ ...move_stale_contact_inboxes_service_spec.rb | 32 ++++++++++++++ 6 files changed, 106 insertions(+) create mode 100644 app/jobs/internal/remove_stale_contact_inboxes_job.rb create mode 100644 app/services/internal/remove_stale_contact_inboxes_service.rb create mode 100644 spec/services/internal/remove_stale_contact_inboxes_service_spec.rb diff --git a/.env.example b/.env.example index 27d41685e7b2..d76338e288f9 100644 --- a/.env.example +++ b/.env.example @@ -253,3 +253,9 @@ AZURE_APP_SECRET= # Sentiment analysis model file path SENTIMENT_FILE_PATH= + + +# Housekeeping/Performance related configurations +# Set to true if you want to remove stale contact inboxes +# contact_inboxes with no conversation older than 90 days will be removed +# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false diff --git a/app/jobs/internal/remove_stale_contact_inboxes_job.rb b/app/jobs/internal/remove_stale_contact_inboxes_job.rb new file mode 100644 index 000000000000..e96e5f0683ea --- /dev/null +++ b/app/jobs/internal/remove_stale_contact_inboxes_job.rb @@ -0,0 +1,11 @@ +# housekeeping +# remove contact inboxes that does not have any conversations +# and are older than 3 months + +class Internal::RemoveStaleContactInboxesJob < ApplicationJob + queue_as :scheduled_jobs + + def perform + Internal::RemoveStaleContactInboxesService.new.perform + end +end diff --git a/app/models/contact_inbox.rb b/app/models/contact_inbox.rb index 5e592af1a8ea..6d034880e2d4 100644 --- a/app/models/contact_inbox.rb +++ b/app/models/contact_inbox.rb @@ -33,6 +33,13 @@ class ContactInbox < ApplicationRecord has_many :conversations, dependent: :destroy_async + # contact_inboxes that are not associated with any conversation + scope :stale_without_conversations, lambda { |time_period| + left_joins(:conversations) + .where('contact_inboxes.created_at < ?', time_period) + .where(conversations: { contact_id: nil }) + } + def webhook_data { id: id, diff --git a/app/services/internal/remove_stale_contact_inboxes_service.rb b/app/services/internal/remove_stale_contact_inboxes_service.rb new file mode 100644 index 000000000000..b619a7011110 --- /dev/null +++ b/app/services/internal/remove_stale_contact_inboxes_service.rb @@ -0,0 +1,43 @@ +class Internal::RemoveStaleContactInboxesService + def perform + return unless remove_stale_contact_inbox_job_enabled? + + time_period = 90.days.ago + contact_inboxes_to_delete = stale_contact_inboxes(time_period) + + log_stale_contact_inboxes_deletion(contact_inboxes_to_delete, time_period) + + # Since the number of records to delete is very high, + # delete_all would be faster than destroy_all since it operates at database level + # and avoid loading all the records in memory + # Transaction and batching is used to avoid deadlock and memory issues + ContactInbox.transaction do + contact_inboxes_to_delete + .find_in_batches(batch_size: 10_000) do |group| + ContactInbox.where(id: group.map(&:id)).delete_all + end + end + end + + private + + def remove_stale_contact_inbox_job_enabled? + job_status = ENV.fetch('REMOVE_STALE_CONTACT_INBOX_JOB_STATUS', false) + return false unless ActiveModel::Type::Boolean.new.cast(job_status) + + true + end + + def stale_contact_inboxes(time_period) + ContactInbox.stale_without_conversations(time_period) + end + + def log_stale_contact_inboxes_deletion(contact_inboxes, time_period) + count = contact_inboxes.count + Rails.logger.info "Deleting #{count} stale contact inboxes older than #{time_period}" + + # Log the SQL query without executing it + sql_query = contact_inboxes.to_sql + Rails.logger.info("SQL Query: #{sql_query}") + end +end diff --git a/config/schedule.yml b/config/schedule.yml index 5f8a0f97f0c5..7a0d2faeba7e 100644 --- a/config/schedule.yml +++ b/config/schedule.yml @@ -19,3 +19,10 @@ trigger_imap_email_inboxes_job: cron: '*/1 * * * *' class: 'Inboxes::FetchImapEmailInboxesJob' queue: scheduled_jobs + +# executed daily at 2230 UTC +# which is our lowest traffic time +remove_stale_contact_inboxes_job.rb: + cron: '30 22 * * *' + class: 'Internal::RemoveStaleContactInboxesJob' + queue: scheduled_jobs diff --git a/spec/services/internal/remove_stale_contact_inboxes_service_spec.rb b/spec/services/internal/remove_stale_contact_inboxes_service_spec.rb new file mode 100644 index 000000000000..da873b5416ab --- /dev/null +++ b/spec/services/internal/remove_stale_contact_inboxes_service_spec.rb @@ -0,0 +1,32 @@ +# spec/services/remove_stale_contact_inboxes_service_spec.rb + +require 'rails_helper' + +RSpec.describe Internal::RemoveStaleContactInboxesService do + describe '#perform' do + it 'does not delete stale contact inboxes if REMOVE_STALE_CONTACT_INBOX_JOB_STATUS is false' do + # default value of REMOVE_STALE_CONTACT_INBOX_JOB_STATUS is false + create(:contact_inbox, created_at: 3.days.ago) + create(:contact_inbox, created_at: 91.days.ago) + create(:contact_inbox, created_at: 92.days.ago) + create(:contact_inbox, created_at: 93.days.ago) + create(:contact_inbox, created_at: 94.days.ago) + + service = described_class.new + expect { service.perform }.not_to change(ContactInbox, :count) + end + + it 'deletes stale contact inboxes' do + with_modified_env REMOVE_STALE_CONTACT_INBOX_JOB_STATUS: 'true' do + create(:contact_inbox, created_at: 3.days.ago) + create(:contact_inbox, created_at: 91.days.ago) + create(:contact_inbox, created_at: 92.days.ago) + create(:contact_inbox, created_at: 93.days.ago) + create(:contact_inbox, created_at: 94.days.ago) + + service = described_class.new + expect { service.perform }.to change(ContactInbox, :count).by(-4) + end + end + end +end