job.rb 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. class Job < ApplicationModel
  3. include ChecksClientNotification
  4. include ChecksConditionValidation
  5. include ChecksHtmlSanitized
  6. include HasTimeplan
  7. include HasSearchIndexBackend
  8. include CanSelector
  9. include CanSearch
  10. include Job::Assets
  11. include Job::SearchIndex
  12. OBJECTS_BATCH_SIZE = 2_000
  13. store :condition
  14. store :perform
  15. validates :name, presence: true, uniqueness: { case_sensitive: false }
  16. validates :object, presence: true, inclusion: { in: %w[Ticket User Organization] }
  17. validates :perform, 'validations/verify_perform_rules': true
  18. before_save :updated_matching, :update_next_run_at
  19. validates :note, length: { maximum: 250 }
  20. sanitized_html :note
  21. =begin
  22. verify each job if needed to run (e. g. if true and times are matching) and execute it
  23. Job.run
  24. =end
  25. def self.run
  26. start_at = Time.zone.now
  27. Job.where(active: true).each do |job|
  28. next if !job.executable?
  29. job.run(false, start_at)
  30. end
  31. true
  32. end
  33. =begin
  34. execute a single job if needed (e. g. if true and times are matching)
  35. job = Job.find(123)
  36. job.run
  37. force to run job (ignore times are matching)
  38. job.run(true)
  39. =end
  40. def run(force = false, start_at = Time.zone.now)
  41. logger.debug { "Execute job #{inspect}" }
  42. object_ids = start_job(start_at, force)
  43. return if object_ids.nil?
  44. object_ids.each_slice(10) do |slice|
  45. run_slice(slice)
  46. end
  47. finish_job
  48. end
  49. def executable?(start_at = Time.zone.now)
  50. return false if !active
  51. # only execute jobs older than 1 min to give admin time to make last-minute changes
  52. return false if updated_at > 1.minute.ago
  53. # check if job got stuck
  54. return false if running == true && last_run_at && 1.day.ago < last_run_at
  55. # check if jobs need to be executed
  56. # ignore if job was running within last 10 min.
  57. return false if last_run_at && last_run_at > start_at - 10.minutes
  58. true
  59. end
  60. def matching_count
  61. object_count, _objects = object.constantize.selectors(condition, limit: 1, execution_time: true)
  62. object_count || 0
  63. end
  64. private
  65. def next_run_at_calculate(time = Time.zone.now)
  66. return nil if !active
  67. if last_run_at && (time - last_run_at).positive?
  68. time += 10.minutes
  69. end
  70. timeplan_calculation.next_at(time)
  71. end
  72. def updated_matching
  73. self.matching = matching_count
  74. end
  75. def update_next_run_at
  76. self.next_run_at = next_run_at_calculate
  77. end
  78. def finish_job
  79. Transaction.execute(reset_user_id: true) do
  80. mark_as_finished
  81. end
  82. end
  83. def mark_as_finished
  84. self.running = false
  85. self.last_run_at = Time.zone.now
  86. save!
  87. end
  88. def start_job(start_at, force)
  89. Transaction.execute(reset_user_id: true) do
  90. next if !start_job_executable?(start_at, force)
  91. next if !start_job_in_timeplan?(start_at, force)
  92. object_count, objects = object.constantize.selectors(condition, limit: OBJECTS_BATCH_SIZE, execution_time: true)
  93. logger.debug { "Job #{name} with #{object_count} object(s)" }
  94. mark_as_started(objects&.count || 0)
  95. objects&.pluck(:id) || []
  96. end
  97. end
  98. def start_job_executable?(start_at, force)
  99. return true if executable?(start_at) || force
  100. if next_run_at && next_run_at <= Time.zone.now
  101. save!
  102. end
  103. false
  104. end
  105. def start_job_in_timeplan?(start_at, force)
  106. return true if in_timeplan?(start_at) || force
  107. save! # trigger updating matching tickets count and next_run_at time even if not in timeplan
  108. false
  109. end
  110. def mark_as_started(batch_count)
  111. self.processed = batch_count
  112. self.running = true
  113. self.last_run_at = Time.zone.now
  114. save!
  115. end
  116. def run_slice(slice)
  117. Transaction.execute(disable_notification: disable_notification, reset_user_id: true) do
  118. _, objects = object.constantize.selectors(condition, limit: OBJECTS_BATCH_SIZE, execution_time: true)
  119. return if objects.nil?
  120. objects
  121. .where(id: slice)
  122. .each do |object|
  123. object.perform_changes(self, 'job')
  124. end
  125. end
  126. end
  127. end