job.rb 3.9 KB

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