job.rb 4.0 KB

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