From b3077b2b26196a823c5237cfe89392235891849f Mon Sep 17 00:00:00 2001 From: Rodribm10 Date: Sun, 19 Apr 2026 01:05:02 -0300 Subject: [PATCH] feat(captain-memory): add AgingJob with TTL + LRU cap, weekly cron --- config/schedule.yml | 6 +++ .../captain/contact_memories/aging_job.rb | 47 +++++++++++++++++++ .../contact_memories/aging_job_spec.rb | 26 ++++++++++ 3 files changed, 79 insertions(+) create mode 100644 enterprise/app/jobs/captain/contact_memories/aging_job.rb create mode 100644 spec/enterprise/jobs/captain/contact_memories/aging_job_spec.rb diff --git a/config/schedule.yml b/config/schedule.yml index b66848fa7..bbddd7346 100644 --- a/config/schedule.yml +++ b/config/schedule.yml @@ -107,3 +107,9 @@ captain_contact_memory_silence_detector_job: cron: '*/10 * * * *' class: 'Captain::ContactMemories::SilenceDetectorJob' queue: scheduled_jobs + +# weekly - Sundays at 03:00 UTC - TTL aging + LRU cap of contact memories +captain_contact_memory_aging_job: + cron: '0 3 * * 0' + class: 'Captain::ContactMemories::AgingJob' + queue: scheduled_jobs diff --git a/enterprise/app/jobs/captain/contact_memories/aging_job.rb b/enterprise/app/jobs/captain/contact_memories/aging_job.rb new file mode 100644 index 000000000..f6cfe4d70 --- /dev/null +++ b/enterprise/app/jobs/captain/contact_memories/aging_job.rb @@ -0,0 +1,47 @@ +class Captain::ContactMemories::AgingJob < ApplicationJob + queue_as :scheduled_jobs + + DELETE_ON_EXPIRE = %w[reclamacao feedback_positivo vinculo_social].freeze + MAX_ACTIVE_PER_CONTACT = 50 + + def perform + soft_delete_expired_deletable + prune_per_contact_lru + end + + private + + def soft_delete_expired_deletable + Captain::ContactMemory + .where(memory_type: DELETE_ON_EXPIRE) + .where('expires_at < ?', Time.current) + .where(deleted_at: nil) + .update_all(deleted_at: Time.current, updated_at: Time.current) # rubocop:disable Rails/SkipsModelValidations + end + + def prune_per_contact_lru + over_limit_contact_ids.each do |contact_id| + excess = Captain::ContactMemory.active.for_contact(contact_id).count - MAX_ACTIVE_PER_CONTACT + next if excess <= 0 + + ids_to_delete = Captain::ContactMemory + .active + .for_contact(contact_id) + .order(last_verified_at: :asc) + .limit(excess) + .pluck(:id) + + Captain::ContactMemory + .where(id: ids_to_delete) + .update_all(deleted_at: Time.current, updated_at: Time.current) # rubocop:disable Rails/SkipsModelValidations + end + end + + def over_limit_contact_ids + Captain::ContactMemory + .active + .group(:contact_id) + .having('COUNT(*) > ?', MAX_ACTIVE_PER_CONTACT) + .pluck(:contact_id) + end +end diff --git a/spec/enterprise/jobs/captain/contact_memories/aging_job_spec.rb b/spec/enterprise/jobs/captain/contact_memories/aging_job_spec.rb new file mode 100644 index 000000000..00e47059d --- /dev/null +++ b/spec/enterprise/jobs/captain/contact_memories/aging_job_spec.rb @@ -0,0 +1,26 @@ +require 'rails_helper' + +RSpec.describe Captain::ContactMemories::AgingJob do + let(:account) { create(:account) } + let(:contact) { create(:contact, account: account) } + + it 'soft-deletes expired reclamacao (delete on expire type)' do + mem = create(:captain_contact_memory, account: account, contact: contact, memory_type: 'reclamacao', expires_at: 1.day.ago) + described_class.perform_now + expect(mem.reload.deleted_at).not_to be_nil + end + + it 'does NOT soft-delete expired preferencia (reduce-weight type)' do + mem = create(:captain_contact_memory, account: account, contact: contact, memory_type: 'preferencia', expires_at: 1.day.ago) + described_class.perform_now + expect(mem.reload.deleted_at).to be_nil + end + + it 'applies LRU soft-delete when contact exceeds 50 active facts' do + 55.times do |i| + create(:captain_contact_memory, account: account, contact: contact, last_verified_at: i.days.ago) + end + described_class.perform_now + expect(Captain::ContactMemory.where(contact: contact).active.count).to eq(50) + end +end