job.rb 6.6 KB

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