zammad/zammad

View on GitHub
app/models/job.rb

Summary

Maintainability
A
1 hr
Test Coverage
# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/

class Job < ApplicationModel
  include ChecksClientNotification
  include ChecksConditionValidation
  include ChecksHtmlSanitized
  include HasTimeplan

  include Job::Assets

  OBJECTS_BATCH_SIZE = 2_000

  store     :condition
  store     :perform
  validates :name,    presence: true, uniqueness: { case_sensitive: false }
  validates :object,  presence: true, inclusion: { in: %w[Ticket User Organization] }
  validates :perform, 'validations/verify_perform_rules': true

  before_save :updated_matching, :update_next_run_at

  validates :note, length: { maximum: 250 }
  sanitized_html :note

=begin

verify each job if needed to run (e. g. if true and times are matching) and execute it

Job.run

=end

  def self.run
    start_at = Time.zone.now

    Job.where(active: true).each do |job|
      next if !job.executable?

      job.run(false, start_at)
    end

    true
  end

=begin

execute a single job if needed (e. g. if true and times are matching)

job = Job.find(123)

job.run

force to run job (ignore times are matching)

job.run(true)

=end

  def run(force = false, start_at = Time.zone.now)
    logger.debug { "Execute job #{inspect}" }

    object_ids = start_job(start_at, force)

    return if object_ids.nil?

    object_ids.each_slice(10) do |slice|
      run_slice(slice)
    end

    finish_job
  end

  def executable?(start_at = Time.zone.now)
    return false if !active

    # only execute jobs older than 1 min to give admin time to make last-minute changes
    return false if updated_at > 1.minute.ago

    # check if job got stuck
    return false if running == true && last_run_at && 1.day.ago < last_run_at

    # check if jobs need to be executed
    # ignore if job was running within last 10 min.
    return false if last_run_at && last_run_at > start_at - 10.minutes

    true
  end

  def matching_count
    object_count, _objects = object.constantize.selectors(condition, limit: 1, execution_time: true)
    object_count || 0
  end

  private

  def next_run_at_calculate(time = Time.zone.now)
    return nil if !active

    if last_run_at && (time - last_run_at).positive?
      time += 10.minutes
    end

    timeplan_calculation.next_at(time)
  end

  def updated_matching
    self.matching = matching_count
  end

  def update_next_run_at
    self.next_run_at = next_run_at_calculate
  end

  def finish_job
    Transaction.execute(reset_user_id: true) do
      mark_as_finished
    end
  end

  def mark_as_finished
    self.running = false
    self.last_run_at = Time.zone.now
    save!
  end

  def start_job(start_at, force)
    Transaction.execute(reset_user_id: true) do
      next if !start_job_executable?(start_at, force)
      next if !start_job_in_timeplan?(start_at, force)

      object_count, objects = object.constantize.selectors(condition, limit: OBJECTS_BATCH_SIZE, execution_time: true)

      logger.debug { "Job #{name} with #{object_count} object(s)" }

      mark_as_started(objects&.count || 0)

      objects&.pluck(:id) || []
    end
  end

  def start_job_executable?(start_at, force)
    return true if executable?(start_at) || force

    if next_run_at && next_run_at <= Time.zone.now
      save!
    end

    false
  end

  def start_job_in_timeplan?(start_at, force)
    return true if in_timeplan?(start_at) || force

    save! # trigger updating matching tickets count and next_run_at time even if not in timeplan

    false
  end

  def mark_as_started(batch_count)
    self.processed = batch_count
    self.running = true
    self.last_run_at = Time.zone.now
    save!
  end

  def run_slice(slice)
    Transaction.execute(disable_notification: disable_notification, reset_user_id: true) do
      _, objects = object.constantize.selectors(condition, limit: OBJECTS_BATCH_SIZE, execution_time: true)

      return if objects.nil?

      objects
        .where(id: slice)
        .each do |object|
          object.perform_changes(self, 'job')
        end
    end
  end
end