job_executor.rb 2.6 KB

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