From dd637c2370f23147c426fd56abc77e761e2bc8e3 Mon Sep 17 00:00:00 2001 From: akumari Date: Thu, 27 Nov 2025 03:51:13 +0530 Subject: [PATCH] Fixes #38956 - Add cron rake tasks for recurring jobs Introduce `Foreman::Cron` with a hard-coded mapping from hourly/daily/weekly/monthly to existing rake tasks, and add `cron:hourly`, `cron:daily`, `cron:weekly` and `cron:monthly` rake tasks. These serve as generic entrypoints for scheduling (e.g. systemd timers in foremanctl) instead of wiring individual tasks. Allow plugins to register extra tasks in Foreman::Cron Fail cron:* rake tasks when a job fails Refine Foreman::Cron task Add unit tests for Foreman::Cron unified registry --- lib/foreman/cron.rb | 74 ++++++++++++++++++++++++++++++++++ lib/tasks/cron.rake | 41 +++++++++++++++++++ test/unit/foreman/cron_test.rb | 72 +++++++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 lib/foreman/cron.rb create mode 100644 lib/tasks/cron.rake create mode 100644 test/unit/foreman/cron_test.rb diff --git a/lib/foreman/cron.rb b/lib/foreman/cron.rb new file mode 100644 index 00000000000..8af78a648e8 --- /dev/null +++ b/lib/foreman/cron.rb @@ -0,0 +1,74 @@ +require 'rake' + +module Foreman + module Cron + SUPPORTED_CADENCES = [:hourly, :daily, :weekly, :monthly].freeze + + class << self + def register(cadence, task_name) + cadence = cadence.to_sym + + unless SUPPORTED_CADENCES.include?(cadence) + logger.warn( + "Foreman::Cron[#{cadence}]: unknown cadence when registering #{task_name}, ignoring" + ) + return + end + + cadence_tasks = tasks_for(cadence) + cadence_tasks << task_name unless cadence_tasks.include?(task_name) + end + + def run(cadence) + cadence = cadence.to_sym + + cadence_tasks = tasks_for(cadence) + if cadence_tasks.empty? + logger.debug("Foreman::Cron[#{cadence}]: no tasks configured for this cadence") + return false + end + + logger.info("Foreman::Cron[#{cadence}]: running #{cadence_tasks.size} task(s)") + + failed = false + + cadence_tasks.each do |task_name| + failed ||= !run_task(cadence, task_name) + end + + failed + end + + private + + def tasks + @tasks ||= Hash.new { |h, k| h[k] = [] } + end + + def tasks_for(cadence) + tasks[cadence.to_sym] + end + + def run_task(cadence, task_name) + logger.info("Foreman::Cron[#{cadence}]: starting #{task_name}") + + task = Rake::Task[task_name] + task.reenable + task.invoke + + logger.info("Foreman::Cron[#{cadence}]: finished #{task_name}") + true + rescue StandardError => e + logger.error( + "Foreman::Cron[#{cadence}]: #{task_name} failed: #{e.class}: #{e.message}" + ) + logger.debug(e.backtrace.join("\n")) if e.backtrace + false + end + + def logger + @logger ||= Rails.logger + end + end + end +end diff --git a/lib/tasks/cron.rake b/lib/tasks/cron.rake new file mode 100644 index 00000000000..40b49b086d5 --- /dev/null +++ b/lib/tasks/cron.rake @@ -0,0 +1,41 @@ +require 'foreman/cron' + +# Register built-in recurring tasks for each cadence. +# Plugins can also call Foreman::Cron.register(:daily, 'my_plugin:task'). +Foreman::Cron.register(:hourly, 'ldap:refresh_usergroups') + +Foreman::Cron.register(:daily, 'reports:daily') +Foreman::Cron.register(:daily, 'db:sessions:clear') +Foreman::Cron.register(:daily, 'reports:expire') +Foreman::Cron.register(:daily, 'audits:expire') + +Foreman::Cron.register(:weekly, 'reports:weekly') +Foreman::Cron.register(:weekly, 'notifications:clean') + +Foreman::Cron.register(:monthly, 'reports:monthly') + +namespace :cron do + desc 'Run hourly Foreman cron jobs' + task hourly: :environment do + failed = Foreman::Cron.run(:hourly) + raise "One or more hourly cron tasks failed" if failed + end + + desc 'Run daily Foreman cron jobs' + task daily: :environment do + failed = Foreman::Cron.run(:daily) + raise "One or more daily cron tasks failed" if failed + end + + desc 'Run weekly Foreman cron jobs' + task weekly: :environment do + failed = Foreman::Cron.run(:weekly) + raise "One or more weekly cron tasks failed" if failed + end + + desc 'Run monthly Foreman cron jobs' + task monthly: :environment do + failed = Foreman::Cron.run(:monthly) + raise "One or more monthly cron tasks failed" if failed + end +end diff --git a/test/unit/foreman/cron_test.rb b/test/unit/foreman/cron_test.rb new file mode 100644 index 00000000000..0ec64de4b47 --- /dev/null +++ b/test/unit/foreman/cron_test.rb @@ -0,0 +1,72 @@ +require 'test_helper' +require 'foreman/cron' +require 'logger' + +class Foreman::CronTest < ActiveSupport::TestCase + setup do + Foreman::Cron.instance_variable_set(:@tasks, nil) + Foreman::Cron.instance_variable_set(:@logger, Logger.new(nil)) + end + + test 'register adds task for valid cadence' do + Foreman::Cron.register(:daily, 'test:task') + + tasks = Foreman::Cron.send(:tasks_for, :daily) + assert_includes tasks, 'test:task' + end + + test 'register does not add duplicate tasks' do + Foreman::Cron.register(:daily, 'test:task') + Foreman::Cron.register(:daily, 'test:task') + + tasks = Foreman::Cron.send(:tasks_for, :daily) + assert_equal 1, tasks.count('test:task') + end + + test 'register ignores invalid cadence' do + Foreman::Cron.register(:invalid, 'test:task') + + tasks = Foreman::Cron.send(:tasks_for, :invalid) + assert_empty(tasks) + end + + test 'run returns false when all tasks succeed' do + Foreman::Cron.instance_variable_set(:@tasks, { daily: ['test:task'] }) + + task = mock('rake_task') + task.expects(:reenable).once + task.expects(:invoke).once + Rake::Task.stubs(:[]).with('test:task').returns(task) + + result = Foreman::Cron.run(:daily) + + assert_equal false, result + end + + test 'run returns true when a task fails but continues executing others' do + Foreman::Cron.instance_variable_set(:@tasks, { daily: %w[first second] }) + + failing = mock('failing_task') + failing.expects(:reenable).once + failing.expects(:invoke).raises(StandardError.new('boom')) + + succeeding = mock('succeeding_task') + succeeding.expects(:reenable).once + succeeding.expects(:invoke).once + + Rake::Task.stubs(:[]).with('first').returns(failing) + Rake::Task.stubs(:[]).with('second').returns(succeeding) + + result = Foreman::Cron.run(:daily) + + assert_equal true, result + end + + test 'run returns false when no tasks are configured' do + Foreman::Cron.instance_variable_set(:@tasks, { hourly: [] }) + + result = Foreman::Cron.run(:hourly) + + assert_equal false, result + end +end