123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- class BackgroundServices
- def self.available_services
- BackgroundServices::Service.descendants
- end
- # Waiting time before processes get killed.
- SHUTDOWN_GRACE_PERIOD = 30.seconds
- class_attribute :shutdown_requested
- attr_accessor :threads, :child_pids
- attr_reader :config
- def initialize(config)
- @config = Array(config)
- @child_pids = []
- @threads = []
- install_signal_trap
- end
- def run
- Rails.logger.info 'Starting BackgroundServices...'
- # Fork before starting the threads in the main process to ensure a consistent state
- # and minimal memory overhead (see also #5420).
- config
- .in_order_of(:start_as, %i[fork thread])
- .each do |service_config|
- run_service service_config
- end
- child_pids.each { |pid| Process.waitpid(pid) }
- threads.each(&:join)
- ensure
- Rails.logger.info('Stopping BackgroundServices.')
- end
- private
- def install_signal_trap
- Signal.trap('TERM') { handle_signal('TERM') }
- Signal.trap('INT') { handle_signal('INT') }
- end
- def handle_signal(signal)
- # Write operations cannot be handled in a signal handler, use a thread for that.
- # This thread is not waited for via `join`, so that the main process should end
- # somewhere during the sleep statement if it is able to shut down cleanly.
- # If it doesn't, it will send KILL signals to all child processes and the main process
- # to enforce the termination.
- Thread.new do
- Rails.logger.info { "BackgroundServices shutdown requested via #{signal} signal for process #{Process.pid}" }
- sleep SHUTDOWN_GRACE_PERIOD
- Rails.logger.error { "BackgroundServices did not shutdown cleanly after #{SHUTDOWN_GRACE_PERIOD}s, forcing termination" }
- child_pids.each { |pid| Process.kill('KILL', pid) }
- Process.kill('KILL', Process.pid)
- end
- self.class.shutdown_requested = true
- child_pids.each do |pid|
- Process.kill(signal, pid)
- rescue Errno::ESRCH, RangeError
- # Don't fail if processes terminated already.
- end
- end
- def run_service(service_config)
- if !service_config.enabled?
- Rails.logger.info { "Skipping disabled service #{service_config.service.service_name}." }
- return
- end
- if service_config.service.skip?(manager: self)
- Rails.logger.info { "Skipping service #{service_config.service.service_name}." }
- return
- end
- service_config.service.pre_run
- case service_config.start_as
- when :fork
- child_pids.push(*start_as_forks(service_config.service, service_config.workers))
- when :thread
- threads.push start_as_thread(service_config.service)
- end
- end
- def start_as_forks(service, forks)
- (1..forks).map do |i|
- Process.fork do
- Process.setproctitle("#{$PROGRAM_NAME} #{service.service_name}##{i}")
- install_signal_trap
- Rails.logger.info { "Starting process ##{Process.pid} for service #{service.service_name}." }
- service.new(manager: self, fork_id: i).run
- rescue Interrupt
- nil
- end
- end
- end
- def start_as_thread(service)
- Thread.new do
- Thread.current.abort_on_exception = true
- Rails.logger.info { "Starting thread for service #{service.service_name} in the main process." }
- service.new(manager: self).run
- # BackgroundServices rspec test is using Timeout.timeout to stop background services.
- # It was fine for a long time, but started throwing following error in Rails 7.2.
- # This seems to affect that test case only.
- # Unfortunately, since it's running on a separate thread, that error has to be rescued here.
- # That said, this should be handled by improving services loops to support graceful exiting.
- rescue ActiveRecord::ActiveRecordError => e
- raise e if Rails.env.test? && e.message != 'Cannot expire connection, it is not currently leased.' # rubocop:disable Zammad/DetectTranslatableString
- end
- end
- end
|