lib/copy_tuner_client/process_guard.rb



module CopyTunerClient
  # Starts the poller from a worker process, or register hooks for a spawner
  # process (such as in Unicorn or Passenger). Also registers hooks for exiting
  # processes and completing background jobs. Applications using the client
  # will not need to interact with this class directly.
  class ProcessGuard
    # @param options [Hash]
    # @option options [Logger] :logger where errors should be logged
    def initialize(cache, poller, options)
      @cache  = cache
      @poller = poller
      @logger = options[:logger]
    end

    # Starts the poller or registers hooks
    def start
      if spawner?
        register_spawn_hooks
      else
        register_exit_hooks
        start_polling
      end
    end

    private

    def start_polling
      @poller.start
    end

    def spawner?
      passenger_spawner? || unicorn_spawner? || delayed_job_spawner? || puma_spawner? || good_job_spawner?
    end

    def passenger_spawner?
      defined?(PhusionPassenger) && ($0.include?("Passenger AppPreloader") || $0.include?("ApplicationSpawner") || $0.include?("rack-preloader"))
    end

    def unicorn_spawner?
      defined?(Unicorn::HttpServer) && ($0.include?("unicorn") && !caller.any? { |line| line.include?("worker_loop") })
    end

    def puma_spawner?
      defined?(Puma::Runner) && $0.include?('puma')
    end

    def delayed_job_spawner?
      # delayed_job は二種類の起動の仕方がある。
      # - bin/delayed_job start
      # - bin/rake jobs:work
      # 前者の呼び出しでのみジョブ処理用の子プロセスが作られるため、 poller を作るフックを仕込む必要がある。
      defined?(Delayed::Worker) && $0.include?('delayed_job')
    end

    def good_job_spawner?
      $0.include?('good_job') && defined?(GoodJob) && ARGV.include?('--daemonize')
    end

    def register_spawn_hooks
      if passenger_spawner?
        register_passenger_hook
      elsif unicorn_spawner?
        register_unicorn_hook
      elsif puma_spawner?
        register_puma_hook
      elsif delayed_job_spawner?
        register_delayed_hook
      elsif good_job_spawner?
        register_good_job_hook
      end
    end

    def register_passenger_hook
      @logger.info("Registered Phusion Passenger fork hook")
      PhusionPassenger.on_event(:starting_worker_process) do |forked|
        start_polling
      end
    end

    def register_unicorn_hook
      @logger.info("Registered Unicorn fork hook")
      poller = @poller
      Unicorn::HttpServer.class_eval do
        alias_method :worker_loop_without_copy_tuner, :worker_loop
        define_method :worker_loop do |worker|
          poller.start
          worker_loop_without_copy_tuner(worker)
        end
      end
    end

    def register_delayed_hook
      @logger.info("Registered Delayed::Job start hook")
      poller = @poller
      Delayed::Worker.class_eval do
        alias_method :start_without_copy_tuner, :start
        define_method :start do
          poller.start
          start_without_copy_tuner
        end
      end
    end

    def register_good_job_hook
      @logger.info("Registered good_job start hook")
      poller = @poller
      hook_module = Module.new do
        define_method :daemon do
          super() # NOTE: define_method 内で super を呼ぶ場合は引数を明示的に指定する必要があるので注意
          poller.start
        end
      end
      ::Process.singleton_class.prepend hook_module
    end

    def register_puma_hook
      # If Puma is clustered mode without preload_app, this method is called on worker process.
      # Just start poller and return.
      if $0.include?('cluster worker')
        @logger.info('Puma would be clustered mode without preload_app')
        @poller.start
        return
      end

      @logger.info('Register Puma fork hook')
      # If Puma is clustered mode with preload_app, this method is called before fork.
      # Delay poller start until Puma::Runner#start_server which is called on worker process.
      poller = @poller
      hook_module = Module.new do
        define_method :start_server do
          poller.start
          super() # NOTE: define_method 内で super を呼ぶ場合は引数を明示的に指定する必要があるので注意
        end
      end
      Puma::Runner.prepend hook_module
    end

    def register_exit_hooks
      at_exit do
        @cache.flush
      end
    end
  end
end