feat: distributed scheduling for version check job (#13042)
This change spreads Chatwoot Hub version checks across the day by scheduling each installation at a stable minute derived from its installation identifier, instead of having all instances check at the same fixed time. Closes - https://linear.app/chatwoot/issue/CW-6107/handle-the-spike-at-12-utc-on-chatwoot-hub What changed - Added `Internal::TriggerDailyScheduledItemsJob` to act as the daily trigger for deferred internal jobs. - Updated the version check cron entry to run once daily at `00:00 UTC` and enqueue the actual version check for that installation’s assigned minute of the day. - Used a deterministic minute-of-day derived from `ChatwootHub.installation_identifier` so the check time stays stable across deploys and restarts. - Kept the existing cron schedule key while switching it to the new orchestrator job. How to test - Run `bundle exec rspec spec/jobs/internal/check_new_versions_job_spec.rb spec/jobs/internal/trigger_daily_scheduled_items_job_spec.rb spec/configs/schedule_spec.rb` - In a Rails console, run `Internal::TriggerDailyScheduledItemsJob.perform_now` and verify `Internal::CheckNewVersionsJob` is enqueued with a `wait_until` later the same UTC day. - In Super Admin settings, use Refresh and verify the version check still runs immediately. --------- Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
25
app/jobs/internal/trigger_daily_scheduled_items_job.rb
Normal file
25
app/jobs/internal/trigger_daily_scheduled_items_job.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
class Internal::TriggerDailyScheduledItemsJob < ApplicationJob
|
||||
queue_as :scheduled_jobs
|
||||
|
||||
def perform
|
||||
# Schedule daily deferred jobs here so each installation can spread load
|
||||
# across the day without changing its slot on deploys or restarts.
|
||||
schedule_version_check
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def schedule_version_check
|
||||
return unless Rails.env.production?
|
||||
|
||||
Internal::CheckNewVersionsJob.set(wait_until: version_check_run_at).perform_later
|
||||
end
|
||||
|
||||
def version_check_run_at
|
||||
Time.current.utc.beginning_of_day + designated_minute.minutes
|
||||
end
|
||||
|
||||
def designated_minute
|
||||
@designated_minute ||= Digest::MD5.hexdigest(ChatwootHub.installation_identifier).hex % 1440
|
||||
end
|
||||
end
|
||||
@@ -34,5 +34,7 @@ end
|
||||
|
||||
# https://github.com/ondrejbartas/sidekiq-cron
|
||||
Rails.application.reloader.to_prepare do
|
||||
# TODO: Switch to `load_from_hash!(..., source: 'schedule')` once we have a
|
||||
# safe cleanup path for YAML-backed cron jobs already persisted in Redis.
|
||||
Sidekiq::Cron::Job.load_from_hash YAML.load_file(schedule_file) if File.exist?(schedule_file) && Sidekiq.server?
|
||||
end
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
# use https://crontab.guru/ to validate
|
||||
# validations for this file exist in /spec/configs/schedule_spec.rb
|
||||
|
||||
# executed At 12:00 on every day-of-month.
|
||||
# executed daily at 0000 UTC
|
||||
# schedules daily deferred jobs at stable times for each installation
|
||||
# keep the existing schedule key while the cron loader still uses load_from_hash
|
||||
internal_check_new_versions_job:
|
||||
cron: '0 12 */1 * *'
|
||||
class: 'Internal::CheckNewVersionsJob'
|
||||
cron: '0 0 * * *'
|
||||
class: 'Internal::TriggerDailyScheduledItemsJob'
|
||||
queue: scheduled_jobs
|
||||
# # executed At every 5th minute..
|
||||
trigger_scheduled_items_job:
|
||||
|
||||
39
spec/jobs/internal/trigger_daily_scheduled_items_job_spec.rb
Normal file
39
spec/jobs/internal/trigger_daily_scheduled_items_job_spec.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Internal::TriggerDailyScheduledItemsJob do
|
||||
subject(:perform_job) { described_class.perform_now }
|
||||
|
||||
let(:installation_id) { 'test-installation-id' }
|
||||
let(:designated_minute) { Digest::MD5.hexdigest(installation_id).hex % 1440 }
|
||||
let(:scheduled_time) { Time.current.utc.beginning_of_day + designated_minute.minutes }
|
||||
let(:configured_job) { instance_double(ActiveJob::ConfiguredJob, perform_later: true) }
|
||||
|
||||
before do
|
||||
allow(ChatwootHub).to receive(:installation_identifier).and_return(installation_id)
|
||||
allow(Internal::CheckNewVersionsJob).to receive(:set).and_return(configured_job)
|
||||
end
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { described_class.perform_later }.to have_enqueued_job(described_class)
|
||||
.on_queue('scheduled_jobs')
|
||||
end
|
||||
|
||||
it 'schedules the version check at a stable minute in production' do
|
||||
allow(Rails.env).to receive(:production?).and_return(true)
|
||||
|
||||
travel_to Time.zone.parse('2026-03-17 08:00:00 UTC') do
|
||||
perform_job
|
||||
|
||||
expect(Internal::CheckNewVersionsJob).to have_received(:set).with(wait_until: scheduled_time)
|
||||
expect(configured_job).to have_received(:perform_later)
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not schedule the version check outside production' do
|
||||
allow(Rails.env).to receive(:production?).and_return(false)
|
||||
|
||||
perform_job
|
||||
|
||||
expect(Internal::CheckNewVersionsJob).not_to have_received(:set)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user