lib/puma/plugin/systemd.rb



# frozen_string_literal: true

require_relative '../plugin'

# Puma's systemd integration allows Puma to inform systemd:
#  1. when it has successfully started
#  2. when it is starting shutdown
#  3. periodically for a liveness check with a watchdog thread
#  4. periodically set the status
Puma::Plugin.create do
  def start(launcher)
    require_relative '../sd_notify'

    launcher.log_writer.log "* Enabling systemd notification integration"

    # hook_events
    launcher.events.on_booted { Puma::SdNotify.ready }
    launcher.events.on_stopped { Puma::SdNotify.stopping }
    launcher.events.on_restart { Puma::SdNotify.reloading }

    # start watchdog
    if Puma::SdNotify.watchdog?
      ping_f = watchdog_sleep_time

      in_background do
        launcher.log_writer.log "Pinging systemd watchdog every #{ping_f.round(1)} sec"
        loop do
          sleep ping_f
          Puma::SdNotify.watchdog
        end
      end
    end

    # start status loop
    instance = self
    sleep_time = 1.0
    in_background do
      launcher.log_writer.log "Sending status to systemd every #{sleep_time.round(1)} sec"

      loop do
        sleep sleep_time
        # TODO: error handling?
        Puma::SdNotify.status(instance.status)
      end
    end
  end

  def status
    if clustered?
      messages = stats[:worker_status].map do |worker|
        common_message(worker[:last_status])
      end.join(',')

      "Puma #{Puma::Const::VERSION}: cluster: #{booted_workers}/#{workers}, worker_status: [#{messages}]"
    else
      "Puma #{Puma::Const::VERSION}: worker: #{common_message(stats)}"
    end
  end

  private

  def watchdog_sleep_time
    usec = Integer(ENV["WATCHDOG_USEC"])

    sec_f = usec / 1_000_000.0
    # "It is recommended that a daemon sends a keep-alive notification message
    # to the service manager every half of the time returned here."
    sec_f / 2
  end

  def stats
    Puma.stats_hash
  end

  def clustered?
    stats.has_key?(:workers)
  end

  def workers
    stats.fetch(:workers, 1)
  end

  def booted_workers
    stats.fetch(:booted_workers, 1)
  end

  def common_message(stats)
    "{ #{stats[:running]}/#{stats[:max_threads]} threads, #{stats[:pool_capacity]} available, #{stats[:backlog]} backlog }"
  end
end