job_executor.rb 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. class BackgroundServices::Service::ProcessScheduledJobs::JobExecutor
  3. include BackgroundServices::Concerns::HasInterruptibleSleep
  4. TRY_COUNT_MAX = 10
  5. attr_reader :job, :try_count, :try_run_time, :started_at
  6. def self.run(job)
  7. return if BackgroundServices.shutdown_requested
  8. klass = job.runs_as_persistent_loop? ? Continuous : OneTime
  9. klass.new(job).run
  10. end
  11. protected
  12. def initialize(job)
  13. @job = job
  14. @try_count = 0
  15. @try_run_time = Time.current
  16. end
  17. def execute
  18. mark_as_started
  19. eval_job_method
  20. rescue => e
  21. log_execution_error(e)
  22. # reconnect in case db connection is lost
  23. begin
  24. ActiveRecord::Base.connection.reconnect!
  25. rescue => e
  26. Rails.logger.error "Can't reconnect to database #{e.inspect}"
  27. end
  28. retry_execution
  29. # rescue any other Exceptions that are not StandardError or childs of it
  30. # https://stackoverflow.com/questions/10048173/why-is-it-bad-style-to-rescue-exception-e-in-ruby
  31. # http://rubylearning.com/satishtalim/ruby_exceptions.html
  32. rescue Exception => e # rubocop:disable Lint/RescueException
  33. log_execution_error(e)
  34. raise
  35. ensure
  36. ActiveSupport::CurrentAttributes.clear_all
  37. end
  38. def eval_job_method
  39. Rails.logger.info "execute #{job.method} (try_count #{try_count})..."
  40. eval job.method # rubocop:disable Security/Eval
  41. Rails.logger.info "ended #{job.method} took: #{since_started} seconds."
  42. end
  43. def log_execution_error(e)
  44. error_description = e.is_a?(StandardError) ? 'error' : 'a non standard error'
  45. Rails.logger.error "execute #{job.method} (try_count #{try_count}) exited with #{error_description} #{e.inspect} in: #{since_started} seconds."
  46. end
  47. def mark_as_started
  48. @started_at = Time.current
  49. job.update!(
  50. last_run: started_at,
  51. pid: Thread.current.object_id,
  52. status: 'ok',
  53. error_message: '',
  54. )
  55. end
  56. def since_started
  57. Time.current - started_at
  58. end
  59. def retry_execution
  60. @try_count += 1
  61. # reset error counter if to old
  62. if try_run_time < 5.minutes.ago
  63. @try_count = 0
  64. end
  65. if @try_count > TRY_COUNT_MAX
  66. retry_limit_reached
  67. return
  68. end
  69. interruptible_sleep(try_count) if Rails.env.production?
  70. return if BackgroundServices.shutdown_requested
  71. execute
  72. end
  73. def retry_limit_reached
  74. error = "Failed to run #{job.method} after #{try_count} tries."
  75. Rails.logger.error error
  76. job.update!(
  77. error_message: error,
  78. status: 'error',
  79. active: false,
  80. )
  81. raise BackgroundServices::Service::ProcessScheduledJobs::RetryLimitReachedError.new(job), "Scheduler #{job.name} reached retry limit while being executed"
  82. end
  83. end