fix: captain auto sync scheduler config (#14336)

This commit is contained in:
Aakash Bakhle
2026-05-04 13:37:25 +05:30
committed by GitHub
parent a01adf860a
commit d00867d636
10 changed files with 100 additions and 50 deletions
@@ -0,0 +1,7 @@
class Internal::TriggerHourlyScheduledItemsJob < ApplicationJob
queue_as :scheduled_jobs
def perform; end
end
Internal::TriggerHourlyScheduledItemsJob.prepend_mod_with('Internal::TriggerHourlyScheduledItemsJob')
-5
View File
@@ -40,11 +40,6 @@ Rails.application.reloader.to_prepare do
if File.exist?(schedule_file) && Sidekiq.server?
schedule = YAML.load_file(schedule_file)
# Merge enterprise-only cron entries when running an enterprise build.
# Mirrors the conditional-load pattern already used for enterprise initializers.
enterprise_schedule_file = Rails.root.join('enterprise/config/schedule.yml')
schedule.merge!(YAML.load_file(enterprise_schedule_file)) if ChatwootApp.enterprise? && enterprise_schedule_file.exist?
# Cron entries removed from schedule.yml but possibly still in Redis
# with source:'dynamic' (predating the source tag). load_from_hash!
# only cleans up source:'schedule' entries, so these need explicit removal.
+6
View File
@@ -209,6 +209,12 @@
description: 'The limits for the Captain AI service for different plans'
value:
type: code
- name: CAPTAIN_DOCUMENT_AUTO_SYNC_INTERVALS
display_title: 'Captain Document Auto Sync Intervals'
description: 'JSON map of plan-wise Captain document auto-sync intervals in hours. Use null to disable auto-sync for a plan.'
value:
locked: false
type: code
# End of Captain Config
# ------- Context.dev Config ------- #
+6
View File
@@ -14,6 +14,12 @@ trigger_scheduled_items_job:
class: 'TriggerScheduledItemsJob'
queue: scheduled_jobs
# executed hourly for scheduled jobs that do not need minute-level cadence
trigger_hourly_scheduled_items_job:
cron: '0 * * * *'
class: 'Internal::TriggerHourlyScheduledItemsJob'
queue: scheduled_jobs
# executed At every minute..
trigger_imap_email_inboxes_job:
cron: '*/1 * * * *'
@@ -7,6 +7,7 @@ class Captain::Documents::ScheduleSyncsJob < ApplicationJob
def perform
@remaining_global_capacity = GLOBAL_HOURLY_CAP
sync_intervals = Enterprise::Account.captain_document_sync_intervals
stats = { accounts_scanned: 0, accounts_enabled: 0, accounts_scheduled: 0, documents_enqueued: 0 }
Account.joins(:captain_documents).distinct.find_each(batch_size: 100) do |account|
@@ -16,7 +17,7 @@ class Captain::Documents::ScheduleSyncsJob < ApplicationJob
next unless account.feature_enabled?('captain_document_auto_sync')
stats[:accounts_enabled] += 1
interval = account.captain_document_sync_interval
interval = account.captain_document_sync_interval(sync_intervals)
next unless interval
stats[:accounts_scheduled] += 1
@@ -0,0 +1,7 @@
module Enterprise::Internal::TriggerHourlyScheduledItemsJob
def perform
super
Captain::Documents::ScheduleSyncsJob.perform_later
end
end
+23 -8
View File
@@ -1,10 +1,22 @@
module Enterprise::Account
CAPTAIN_SYNC_INTERVALS = {
'hacker' => nil,
'startups' => 7.days,
'business' => 1.day,
'enterprise' => 6.hours
}.freeze
class << self
def captain_document_sync_intervals
parse_captain_document_sync_intervals(InstallationConfig.find_by(name: 'CAPTAIN_DOCUMENT_AUTO_SYNC_INTERVALS')&.value)
end
private
def parse_captain_document_sync_intervals(configured_intervals)
return {} if configured_intervals.blank?
parsed_intervals = configured_intervals.is_a?(String) ? JSON.parse(configured_intervals) : configured_intervals
return {} unless parsed_intervals.is_a?(Hash)
parsed_intervals.transform_keys { |plan| plan.to_s.downcase }
rescue JSON::ParserError
{}
end
end
# TODO: Remove this when we upgrade administrate gem to the latest version
# this is a temporary method since current administrate doesn't support virtual attributes
@@ -41,12 +53,15 @@ module Enterprise::Account
custom_attributes.delete('marked_for_deletion_at') && custom_attributes.delete('marked_for_deletion_reason') && save
end
def captain_document_sync_interval
def captain_document_sync_interval(sync_intervals = Enterprise::Account.captain_document_sync_intervals)
plan = custom_attributes['plan_name']
plan = 'enterprise' if plan.blank? && ChatwootApp.self_hosted_enterprise?
return nil if plan.blank?
CAPTAIN_SYNC_INTERVALS[plan.downcase]
interval_hours = sync_intervals[plan.downcase]
return nil unless interval_hours.is_a?(Integer) && interval_hours.positive?
interval_hours.hours
end
def saml_enabled?
-10
View File
@@ -1,10 +0,0 @@
# Enterprise-only Sidekiq cron schedule.
# Loaded by config/initializers/sidekiq.rb only when ChatwootApp.enterprise? is true.
# Add cron entries here when the referenced job class lives under enterprise/.
# Captain document auto-sync scheduler
# Runs hourly, finds due documents based on plan sync intervals
captain_documents_schedule_syncs_job:
cron: '0 * * * *'
class: 'Captain::Documents::ScheduleSyncsJob'
queue: scheduled_jobs
@@ -5,6 +5,7 @@ RSpec.describe Captain::Documents::ScheduleSyncsJob, type: :job do
let(:assistant) { create(:captain_assistant, account: account) }
before do
create(:installation_config, name: 'CAPTAIN_DOCUMENT_AUTO_SYNC_INTERVALS', value: { business: 24, hacker: nil }.to_json)
account.enable_features!('captain_document_auto_sync')
clear_enqueued_jobs
end
+48 -26
View File
@@ -225,46 +225,68 @@ RSpec.describe Account, type: :model do
describe 'captain document sync cadence' do
let(:account) { create(:account) }
it 'has no cadence on the hacker plan' do
account.update!(custom_attributes: { plan_name: 'hacker' })
expect(account.captain_document_sync_interval).to be_nil
end
it 'syncs weekly on the startups plan' do
account.update!(custom_attributes: { plan_name: 'startups' })
expect(account.captain_document_sync_interval).to eq(7.days)
end
it 'syncs daily on the business plan' do
it 'has no cadence when installation config is missing' do
account.update!(custom_attributes: { plan_name: 'business' })
expect(account.captain_document_sync_interval).to eq(1.day)
end
it 'syncs every six hours on the enterprise plan' do
account.update!(custom_attributes: { plan_name: 'enterprise' })
expect(account.captain_document_sync_interval).to eq(6.hours)
end
it 'has no cadence when plan is missing' do
account.update!(custom_attributes: {})
expect(account.captain_document_sync_interval).to be_nil
end
it 'has no cadence for unknown plans' do
account.update!(custom_attributes: { plan_name: 'mystery' })
expect(account.captain_document_sync_interval).to be_nil
it 'uses configured plan intervals from installation config' do
intervals = {
business: 48,
enterprise: 24
}
create(:installation_config, name: 'CAPTAIN_DOCUMENT_AUTO_SYNC_INTERVALS', value: intervals.to_json)
account.update!(custom_attributes: { plan_name: 'business' })
expect(account.captain_document_sync_interval).to eq(2.days)
end
it 'normalizes plan name casing' do
it 'normalizes configured plan name casing' do
create(:installation_config, name: 'CAPTAIN_DOCUMENT_AUTO_SYNC_INTERVALS', value: { business: 24 }.to_json)
account.update!(custom_attributes: { plan_name: 'Business' })
expect(account.captain_document_sync_interval).to eq(1.day)
end
it 'syncs every six hours on self-hosted enterprise installs without a plan_name' do
it 'uses the enterprise cadence for self-hosted enterprise installs without a plan_name' do
allow(ChatwootApp).to receive(:self_hosted_enterprise?).and_return(true)
create(:installation_config, name: 'CAPTAIN_DOCUMENT_AUTO_SYNC_INTERVALS', value: { enterprise: 6 }.to_json)
account.update!(custom_attributes: {})
expect(account.captain_document_sync_interval).to eq(6.hours)
end
it 'allows installation config to disable a plan cadence' do
create(:installation_config, name: 'CAPTAIN_DOCUMENT_AUTO_SYNC_INTERVALS', value: { business: nil }.to_json)
account.update!(custom_attributes: { plan_name: 'business' })
expect(account.captain_document_sync_interval).to be_nil
end
it 'has no cadence when installation config is invalid' do
create(:installation_config, name: 'CAPTAIN_DOCUMENT_AUTO_SYNC_INTERVALS', value: 'invalid-json')
account.update!(custom_attributes: { plan_name: 'business' })
expect(account.captain_document_sync_interval).to be_nil
end
it 'treats invalid plan interval values as disabled' do
intervals = {
business: false,
enterprise: { hours: 6 },
startups: '168'
}
create(:installation_config, name: 'CAPTAIN_DOCUMENT_AUTO_SYNC_INTERVALS', value: intervals.to_json)
account.update!(custom_attributes: { plan_name: 'business' })
expect(account.captain_document_sync_interval).to be_nil
account.update!(custom_attributes: { plan_name: 'enterprise' })
expect(account.captain_document_sync_interval).to be_nil
account.update!(custom_attributes: { plan_name: 'startups' })
expect(account.captain_document_sync_interval).to be_nil
end
end
describe 'account deletion' do