# frozen_string_literal: true class MiniSchedulerLongRunningJobLogger DEFAULT_POLL_INTERVAL_SECONDS = 6 attr_reader :thread def initialize(poll_interval_seconds: nil) @mutex = Mutex.new @stop_requested = false @poll_interval_seconds = if poll_interval_seconds begin Integer(poll_interval_seconds) rescue ArgumentError DEFAULT_POLL_INTERVAL_SECONDS end else DEFAULT_POLL_INTERVAL_SECONDS end end def start @thread ||= Thread.new do hostname = Discourse.os_hostname loop do break if self.stop_requested? current_long_running_jobs = Set.new begin MiniScheduler::Manager.discover_running_scheduled_jobs.each do |job| job_class = job[:class] job_started_at = job[:started_at] mini_scheduler_worker_thread_id = job[:thread_id] job_frequency_minutes = if job_class.daily 1.day.in_minutes.minutes else job_class.every.in_minutes.minutes end warning_duration = begin if job_frequency_minutes < 30.minutes 30.minutes elsif job_frequency_minutes < 2.hours job_frequency_minutes else 2.hours end end next if job_started_at >= (Time.zone.now - warning_duration) running_thread = Thread.list.find do |thread| thread[:mini_scheduler_worker_thread_id] == mini_scheduler_worker_thread_id end next if running_thread.nil? current_long_running_jobs << job_class next if @seen_long_running_jobs&.include?(job_class) Rails.logger.warn(<<~MSG) Sidekiq scheduled job `#{job_class}` has been running for more than #{warning_duration.in_minutes.to_i} minutes #{running_thread.backtrace.join("\n")} MSG end @seen_long_running_jobs = current_long_running_jobs yield if block_given? rescue => error Discourse.warn_exception( error, message: "Unexpected error in MiniSchedulerLongRunningJobLogger thread", ) end sleep @poll_interval_seconds end end end # Used for testing to stop the thread. In production, the thread is expected to live for the lifetime of the process. def stop @mutex.synchronize { @stop_requested = true } if @thread @thread.wakeup @thread.join @thread = nil end end private def stop_requested? @mutex.synchronize { @stop_requested } end end