background_services.rb 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. class BackgroundServices
  3. def self.available_services
  4. BackgroundServices::Service.descendants
  5. end
  6. # Waiting time before processes get killed.
  7. SHUTDOWN_GRACE_PERIOD = 30.seconds
  8. class_attribute :shutdown_requested
  9. attr_accessor :threads, :child_pids
  10. attr_reader :config
  11. def initialize(config)
  12. @config = Array(config)
  13. @child_pids = []
  14. @threads = []
  15. install_signal_trap
  16. AppVersion.start_maintenance_thread(process_name: 'background-worker')
  17. end
  18. def run
  19. Rails.logger.info 'Starting BackgroundServices...'
  20. # Fork before starting the threads in the main process to ensure a consistent state
  21. # and minimal memory overhead (see also #5420).
  22. config
  23. .in_order_of(:start_as, %i[fork thread])
  24. .each do |service_config|
  25. run_service service_config
  26. end
  27. child_pids.each { |pid| Process.waitpid(pid) }
  28. threads.each(&:join)
  29. ensure
  30. Rails.logger.info('Stopping BackgroundServices.')
  31. end
  32. private
  33. def install_signal_trap
  34. Signal.trap('TERM') { handle_signal('TERM') }
  35. Signal.trap('INT') { handle_signal('INT') }
  36. end
  37. def handle_signal(signal)
  38. # Write operations cannot be handled in a signal handler, use a thread for that.
  39. # This thread is not waited for via `join`, so that the main process should end
  40. # somewhere during the sleep statement if it is able to shut down cleanly.
  41. # If it doesn't, it will send KILL signals to all child processes and the main process
  42. # to enforce the termination.
  43. Thread.new do
  44. Rails.logger.info { "BackgroundServices shutdown requested via #{signal} signal for process #{Process.pid}" }
  45. sleep SHUTDOWN_GRACE_PERIOD
  46. Rails.logger.error { "BackgroundServices did not shutdown cleanly after #{SHUTDOWN_GRACE_PERIOD}s, forcing termination" }
  47. child_pids.each { |pid| Process.kill('KILL', pid) }
  48. Process.kill('KILL', Process.pid)
  49. end
  50. self.class.shutdown_requested = true
  51. child_pids.each do |pid|
  52. Process.kill(signal, pid)
  53. rescue Errno::ESRCH, RangeError
  54. # Don't fail if processes terminated already.
  55. end
  56. end
  57. def run_service(service_config)
  58. if !service_config.enabled?
  59. Rails.logger.info { "Skipping disabled service #{service_config.service.service_name}." }
  60. return
  61. end
  62. if service_config.service.skip?(manager: self)
  63. Rails.logger.info { "Skipping service #{service_config.service.service_name}." }
  64. return
  65. end
  66. service_config.service.pre_run
  67. case service_config.start_as
  68. when :fork
  69. child_pids.push(*start_as_forks(service_config.service, service_config.workers))
  70. when :thread
  71. threads.push start_as_thread(service_config.service)
  72. end
  73. end
  74. def start_as_forks(service, forks)
  75. (1..forks).map do |i|
  76. Process.fork do
  77. Process.setproctitle("#{$PROGRAM_NAME} #{service.service_name}##{i}")
  78. install_signal_trap
  79. Rails.logger.info { "Starting process ##{Process.pid} for service #{service.service_name}." }
  80. service.new(manager: self, fork_id: i).run
  81. rescue Interrupt
  82. nil
  83. end
  84. end
  85. end
  86. def start_as_thread(service)
  87. Thread.new do
  88. Thread.current.abort_on_exception = true
  89. Rails.logger.info { "Starting thread for service #{service.service_name} in the main process." }
  90. service.new(manager: self).run
  91. # BackgroundServices rspec test is using Timeout.timeout to stop background services.
  92. # It was fine for a long time, but started throwing following error in Rails 7.2.
  93. # This seems to affect that test case only.
  94. # Unfortunately, since it's running on a separate thread, that error has to be rescued here.
  95. # That said, this should be handled by improving services loops to support graceful exiting.
  96. rescue ActiveRecord::ActiveRecordError => e
  97. raise e if Rails.env.test? && e.message != 'Cannot expire connection, it is not currently leased.' # rubocop:disable Zammad/DetectTranslatableString
  98. end
  99. end
  100. end