scheduler_spec.rb 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. # Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. require 'models/concerns/has_xss_sanitized_note_examples'
  4. require 'models/concerns/has_timeplan_examples'
  5. RSpec.describe Scheduler do
  6. let(:test_backend_class) do
  7. Class.new do
  8. def self.start
  9. # noop
  10. end
  11. # rubocop:disable Style/TrivialAccessors
  12. def self.reschedule=(reschedule)
  13. @reschedule = reschedule
  14. end
  15. # rubocop:enable Style/TrivialAccessors
  16. def self.reschedule?(_delayed_job)
  17. @reschedule || false
  18. end
  19. end
  20. end
  21. let(:test_backend_name) { 'SpecSpace::DelayedJobBackend' }
  22. before do
  23. stub_const test_backend_name, test_backend_class
  24. end
  25. it_behaves_like 'HasXssSanitizedNote', model_factory: :scheduler
  26. it_behaves_like 'HasTimeplan'
  27. describe '.failed_jobs' do
  28. it 'does list failed jobs' do
  29. job = create(:scheduler, status: 'error', active: false)
  30. failed_list = described_class.failed_jobs
  31. expect(failed_list).to be_present
  32. expect(failed_list).to include(job)
  33. end
  34. end
  35. describe '.restart_failed_jobs' do
  36. it 'does restart failed jobs' do
  37. job = create(:scheduler, status: 'error', active: false)
  38. described_class.restart_failed_jobs
  39. job.reload
  40. expect(job.active).to be true
  41. end
  42. end
  43. describe '._start_job' do
  44. it 'sets error status/message for failed jobs' do
  45. job = create(:scheduler)
  46. described_class._start_job(job)
  47. expect(job.status).to eq 'error'
  48. expect(job.active).to be false
  49. expect(job.error_message).to be_present
  50. end
  51. it 'executes job that is expected to succeed' do
  52. expect(Setting).to receive(:reload)
  53. job = create(:scheduler, method: 'Setting.reload')
  54. described_class._start_job(job)
  55. expect(job.status).to eq 'ok'
  56. end
  57. end
  58. describe '.cleanup' do
  59. it 'gets called by .threads' do
  60. allow(described_class).to receive(:cleanup).and_throw(:called)
  61. expect do
  62. described_class.threads
  63. end.to throw_symbol(:called)
  64. end
  65. context 'not called from .threads method' do
  66. it 'throws an exception' do
  67. expect do
  68. described_class.cleanup
  69. end.to raise_error(RuntimeError)
  70. end
  71. it 'throws no exception with force parameter' do
  72. expect do
  73. described_class.cleanup(force: true)
  74. end.not_to raise_error
  75. end
  76. end
  77. # helpers to avoid the throwing behaviour "describe"d above
  78. def simulate_threads_call
  79. threads
  80. end
  81. def threads
  82. described_class.cleanup
  83. end
  84. context 'Delayed::Job' do
  85. it 'keeps unlocked' do
  86. # meta :)
  87. described_class.delay.cleanup
  88. expect do
  89. simulate_threads_call
  90. end.not_to change {
  91. Delayed::Job.count
  92. }
  93. end
  94. context 'locked' do
  95. it 'gets destroyed' do
  96. # meta :)
  97. described_class.delay.cleanup
  98. # lock job (simluates interrupted scheduler task)
  99. locked_job = Delayed::Job.last
  100. locked_job.update!(locked_at: Time.zone.now)
  101. expect do
  102. simulate_threads_call
  103. end.to change {
  104. Delayed::Job.count
  105. }.by(-1)
  106. end
  107. context 'respond to reschedule?' do
  108. it 'gets rescheduled for positive responses' do
  109. SpecSpace::DelayedJobBackend.reschedule = true
  110. SpecSpace::DelayedJobBackend.delay.start
  111. # lock job (simluates interrupted scheduler task)
  112. locked_job = Delayed::Job.last
  113. locked_job.update!(locked_at: Time.zone.now)
  114. expect do
  115. simulate_threads_call
  116. end.to not_change {
  117. Delayed::Job.count
  118. }.and change {
  119. Delayed::Job.last.locked_at
  120. }
  121. end
  122. it 'gets destroyed for negative responses' do
  123. SpecSpace::DelayedJobBackend.reschedule = false
  124. SpecSpace::DelayedJobBackend.delay.start
  125. # lock job (simluates interrupted scheduler task)
  126. locked_job = Delayed::Job.last
  127. locked_job.update!(locked_at: Time.zone.now)
  128. expect do
  129. simulate_threads_call
  130. end.to change {
  131. Delayed::Job.count
  132. }.by(-1)
  133. end
  134. end
  135. end
  136. end
  137. context 'ImportJob' do
  138. context 'affected job' do
  139. let(:job) { create(:import_job, started_at: 5.minutes.ago) }
  140. it 'finishes stuck jobs' do
  141. expect do
  142. simulate_threads_call
  143. end.to change {
  144. job.reload.finished_at
  145. }
  146. end
  147. it 'adds an error message to the result' do
  148. expect do
  149. simulate_threads_call
  150. end.to change {
  151. job.reload.result[:error]
  152. }
  153. end
  154. end
  155. it "doesn't change jobs added after stop" do
  156. job = create(:import_job)
  157. expect do
  158. simulate_threads_call
  159. end.not_to change {
  160. job.reload
  161. }
  162. end
  163. end
  164. end
  165. describe '#timeplan_match?' do
  166. let(:job) do
  167. create(:scheduler,
  168. method: 'Ticket.first.touch',
  169. period: 10.minutes,
  170. prio: 2,
  171. active: true,
  172. last_run: nil,
  173. timeplan: {
  174. 'days' => {
  175. 'Mon' => true,
  176. 'Tue' => true,
  177. 'Wed' => true,
  178. 'Thu' => true,
  179. 'Fri' => true,
  180. 'Sat' => true,
  181. 'Sun' => true
  182. },
  183. 'hours' => {
  184. '0' => true,
  185. '1' => true,
  186. '2' => true,
  187. '3' => false,
  188. '4' => false,
  189. '5' => false,
  190. '6' => false,
  191. '7' => false,
  192. '8' => false,
  193. '9' => false,
  194. '10' => false,
  195. '11' => false,
  196. '12' => false,
  197. '13' => false,
  198. '14' => false,
  199. '15' => false,
  200. '16' => false,
  201. '17' => false,
  202. '18' => false,
  203. '19' => false,
  204. '20' => false,
  205. '21' => false,
  206. '22' => false,
  207. '23' => false
  208. },
  209. 'minutes' => {
  210. '0' => true,
  211. '10' => false,
  212. '20' => false,
  213. '30' => false,
  214. '40' => false,
  215. '50' => false
  216. }
  217. })
  218. end
  219. def run_job
  220. travel 1.minute
  221. described_class._try_job(job)
  222. end
  223. before do
  224. allow(described_class).to receive(:start_job)
  225. end
  226. context 'when it is mid-day' do
  227. before do
  228. travel_to Time.current.change(hour: 12)
  229. end
  230. context 'when the job has no last_run' do
  231. it 'handles the job based on the timeplan' do
  232. run_job
  233. expect(described_class).not_to have_received(:start_job)
  234. end
  235. end
  236. context 'when the job has outdated last_run' do
  237. before do
  238. job.last_run = 2.days.ago
  239. end
  240. it 'handles the job based on the timeplan' do
  241. run_job
  242. expect(described_class).not_to have_received(:start_job)
  243. end
  244. end
  245. context 'when the job has last_run' do
  246. before do
  247. job.last_run = Time.zone.now
  248. end
  249. it 'handles the job based on the timeplan' do
  250. run_job
  251. expect(described_class).not_to have_received(:start_job)
  252. end
  253. end
  254. end
  255. context 'when it is night' do
  256. before do
  257. travel_to Time.current.change(hour: 0)
  258. end
  259. context 'when the job has no last_run' do
  260. it 'handles the job based on the timeplan' do
  261. run_job
  262. expect(described_class).to have_received(:start_job)
  263. end
  264. end
  265. context 'when the job has outdated last_run' do
  266. before do
  267. job.last_run = 2.days.ago
  268. end
  269. it 'handles the job based on the timeplan' do
  270. run_job
  271. expect(described_class).to have_received(:start_job)
  272. end
  273. end
  274. context 'when the job has last_run' do
  275. before do
  276. job.last_run = Time.zone.now
  277. end
  278. it 'handles the job based on the timeplan' do
  279. run_job
  280. expect(described_class).not_to have_received(:start_job)
  281. end
  282. end
  283. end
  284. context 'Clean up cache job' do
  285. let(:job) { described_class.find_by method: 'CacheClearJob.perform_now' }
  286. it 'runs at 23-ish' do
  287. travel_to Time.current.change(hour: 23, minute: 5)
  288. run_job
  289. expect(described_class).to have_received(:start_job)
  290. end
  291. it 'does not run at 11-ish' do
  292. travel_to Time.current.change(hour: 11, minute: 5)
  293. run_job
  294. expect(described_class).not_to have_received(:start_job)
  295. end
  296. end
  297. end
  298. end