job.rb 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. # Copyright (C) 2012-2021 Zammad Foundation, http://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_create :updated_matching, :update_next_run_at
  13. before_update :updated_matching, :update_next_run_at
  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. tickets = nil
  38. Transaction.execute(reset_user_id: true) do
  39. if !executable?(start_at) && force == false
  40. if next_run_at && next_run_at <= Time.zone.now
  41. save!
  42. end
  43. return
  44. end
  45. # find tickets to change
  46. matching = matching_count
  47. if self.matching != matching
  48. self.matching = matching
  49. save!
  50. end
  51. if !in_timeplan?(start_at) && force == false
  52. if next_run_at && next_run_at <= Time.zone.now
  53. save!
  54. end
  55. return
  56. end
  57. ticket_count, tickets = Ticket.selectors(condition, limit: 2_000, execution_time: true)
  58. logger.debug { "Job #{name} with #{ticket_count} tickets" }
  59. self.processed = ticket_count || 0
  60. self.running = true
  61. self.last_run_at = Time.zone.now
  62. save!
  63. end
  64. tickets&.each do |ticket|
  65. Transaction.execute(disable_notification: disable_notification, reset_user_id: true) do
  66. ticket.perform_changes(self, 'job')
  67. end
  68. end
  69. Transaction.execute(reset_user_id: true) do
  70. self.running = false
  71. self.last_run_at = Time.zone.now
  72. save!
  73. end
  74. end
  75. def executable?(start_at = Time.zone.now)
  76. return false if !active
  77. # only execute jobs older than 1 min to give admin time to make last-minute changes
  78. return false if updated_at > Time.zone.now - 1.minute
  79. # check if job got stuck
  80. return false if running == true && last_run_at && Time.zone.now - 1.day < last_run_at
  81. # check if jobs need to be executed
  82. # ignore if job was running within last 10 min.
  83. return false if last_run_at && last_run_at > start_at - 10.minutes
  84. true
  85. end
  86. def in_timeplan?(time = Time.zone.now)
  87. day_map = {
  88. 0 => 'Sun',
  89. 1 => 'Mon',
  90. 2 => 'Tue',
  91. 3 => 'Wed',
  92. 4 => 'Thu',
  93. 5 => 'Fri',
  94. 6 => 'Sat',
  95. }
  96. # check day
  97. return false if !timeplan['days']
  98. return false if !timeplan['days'][day_map[time.wday]]
  99. # check hour
  100. return false if !timeplan['hours']
  101. return false if !timeplan['hours'][time.hour.to_s] && !timeplan['hours'][time.hour]
  102. # check min
  103. return false if !timeplan['minutes']
  104. return false if !timeplan['minutes'][match_minutes(time.min).to_s] && !timeplan['minutes'][match_minutes(time.min)]
  105. true
  106. end
  107. def matching_count
  108. ticket_count, _tickets = Ticket.selectors(condition, limit: 1, execution_time: true)
  109. ticket_count || 0
  110. end
  111. def next_run_at_calculate(time = Time.zone.now)
  112. if last_run_at
  113. diff = time - last_run_at
  114. if diff.positive?
  115. time += 10.minutes
  116. end
  117. end
  118. day_map = {
  119. 0 => 'Sun',
  120. 1 => 'Mon',
  121. 2 => 'Tue',
  122. 3 => 'Wed',
  123. 4 => 'Thu',
  124. 5 => 'Fri',
  125. 6 => 'Sat',
  126. }
  127. return nil if !active
  128. return nil if !timeplan['days']
  129. return nil if !timeplan['hours']
  130. return nil if !timeplan['minutes']
  131. # loop week days
  132. (0..7).each do |day_counter|
  133. time_to_check = nil
  134. day_to_check = if day_counter.zero?
  135. time
  136. else
  137. time + 1.day
  138. end
  139. if !timeplan['days'][day_map[day_to_check.wday]]
  140. # start on next day at 00:00:00
  141. time = day_to_check - day_to_check.sec.seconds
  142. time -= day_to_check.min.minutes
  143. time -= day_to_check.hour.hours
  144. next
  145. end
  146. min = day_to_check.min
  147. min = if min < 10
  148. 0
  149. elsif min < 20
  150. 10
  151. elsif min < 30
  152. 20
  153. elsif min < 40
  154. 30
  155. elsif min < 50
  156. 40
  157. else
  158. 50
  159. end
  160. # move to [0-5]0:00 time stamps
  161. day_to_check = day_to_check - day_to_check.min.minutes + min.minutes
  162. day_to_check -= day_to_check.sec.seconds
  163. # loop minutes till next full hour
  164. if day_to_check.min.nonzero?
  165. (0..5).each do |minute_counter|
  166. if minute_counter.nonzero?
  167. break if day_to_check.min.zero?
  168. day_to_check += 10.minutes
  169. end
  170. next if !timeplan['hours'][day_to_check.hour] && !timeplan['hours'][day_to_check.hour.to_s]
  171. next if !timeplan['minutes'][match_minutes(day_to_check.min)] && !timeplan['minutes'][match_minutes(day_to_check.min).to_s]
  172. return day_to_check
  173. end
  174. end
  175. # loop hours
  176. hour_to_check = nil
  177. (0..23).each do |hour_counter|
  178. hour_to_check = day_to_check + hour_counter.hours
  179. # start on next day
  180. if hour_to_check.day != day_to_check.day
  181. time = day_to_check - day_to_check.hour.hours
  182. break
  183. end
  184. # ignore not configured hours
  185. next if !timeplan['hours'][hour_to_check.hour] && !timeplan['hours'][hour_to_check.hour.to_s]
  186. return nil if !hour_to_check
  187. # loop minutes
  188. minute_to_check = nil
  189. (0..5).each do |minute_counter|
  190. minute_to_check = hour_to_check + minute_counter.minutes * 10
  191. next if !timeplan['minutes'][match_minutes(minute_to_check.min)] && !timeplan['minutes'][match_minutes(minute_to_check.min).to_s]
  192. time_to_check = minute_to_check
  193. break
  194. end
  195. next if !minute_to_check
  196. return time_to_check
  197. end
  198. end
  199. nil
  200. end
  201. private
  202. def updated_matching
  203. self.matching = matching_count
  204. true
  205. end
  206. def update_next_run_at
  207. self.next_run_at = next_run_at_calculate
  208. true
  209. end
  210. def match_minutes(minutes)
  211. return 0 if minutes < 10
  212. "#{minutes.to_s.gsub(%r{(\d)\d}, '\\1')}0".to_i
  213. end
  214. end