Skip to content

Commit

Permalink
Add built-in systemd notify support (#3011)
Browse files Browse the repository at this point in the history
  • Loading branch information
joaomarcos96 committed Jan 3, 2023
1 parent ed396c1 commit 6d8b728
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 12 deletions.
1 change: 0 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ gem "minitest", "~> 5.11"
gem "minitest-retry"
gem "minitest-proveit"
gem "minitest-stub-const"
gem "sd_notify"

gem "rack", (ENV['PUMA_CI_RACK_2'] ? "~> 2.2" : ">= 2.2")

Expand Down
3 changes: 1 addition & 2 deletions docs/systemd.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ After=network.target

[Service]
# Puma supports systemd's `Type=notify` and watchdog service
# monitoring, if the [sd_notify](https://github.com/agis/ruby-sdnotify) gem is installed,
# as of Puma 5.1 or later.
# monitoring, as of Puma 5.1 or later.
# On earlier versions of Puma or JRuby, change this to `Type=simple` and remove
# the `WatchdogSec` line.
Type=notify
Expand Down
7 changes: 1 addition & 6 deletions lib/puma/launcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -318,12 +318,7 @@ def reload_worker_directory
def integrate_with_systemd
return unless ENV["NOTIFY_SOCKET"]

begin
require_relative 'systemd'
rescue LoadError
log "Systemd integration failed. It looks like you're trying to use systemd notify but don't have sd_notify gem installed"
return
end
require_relative 'systemd'

log "* Enabling systemd notification integration"

Expand Down
149 changes: 149 additions & 0 deletions lib/puma/sd_notify.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# frozen_string_literal: true

require "socket"

module Puma
# The MIT License
#
# Copyright (c) 2017-2022 Agis Anastasopoulos
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# This is a copy of https://github.com/agis/ruby-sdnotify as of commit cca575c
# The only changes made was "rehoming" it within the Puma module to avoid
# namespace collisions and applying standard's code formatting style.
#
# SdNotify is a pure-Ruby implementation of sd_notify(3). It can be used to
# notify systemd about state changes. Methods of this package are no-op on
# non-systemd systems (eg. Darwin).
#
# The API maps closely to the original implementation of sd_notify(3),
# therefore be sure to check the official man pages prior to using SdNotify.
#
# @see https://www.freedesktop.org/software/systemd/man/sd_notify.html
module SdNotify
# Exception raised when there's an error writing to the notification socket
class NotifyError < RuntimeError; end

READY = "READY=1"
RELOADING = "RELOADING=1"
STOPPING = "STOPPING=1"
STATUS = "STATUS="
ERRNO = "ERRNO="
MAINPID = "MAINPID="
WATCHDOG = "WATCHDOG=1"
FDSTORE = "FDSTORE=1"

def self.ready(unset_env=false)
notify(READY, unset_env)
end

def self.reloading(unset_env=false)
notify(RELOADING, unset_env)
end

def self.stopping(unset_env=false)
notify(STOPPING, unset_env)
end

# @param status [String] a custom status string that describes the current
# state of the service
def self.status(status, unset_env=false)
notify("#{STATUS}#{status}", unset_env)
end

# @param errno [Integer]
def self.errno(errno, unset_env=false)
notify("#{ERRNO}#{errno}", unset_env)
end

# @param pid [Integer]
def self.mainpid(pid, unset_env=false)
notify("#{MAINPID}#{pid}", unset_env)
end

def self.watchdog(unset_env=false)
notify(WATCHDOG, unset_env)
end

def self.fdstore(unset_env=false)
notify(FDSTORE, unset_env)
end

# @param [Boolean] true if the service manager expects watchdog keep-alive
# notification messages to be sent from this process.
#
# If the $WATCHDOG_USEC environment variable is set,
# and the $WATCHDOG_PID variable is unset or set to the PID of the current
# process
#
# @note Unlike sd_watchdog_enabled(3), this method does not mutate the
# environment.
def self.watchdog?
wd_usec = ENV["WATCHDOG_USEC"]
wd_pid = ENV["WATCHDOG_PID"]

return false if !wd_usec

begin
wd_usec = Integer(wd_usec)
rescue
return false
end

return false if wd_usec <= 0
return true if !wd_pid || wd_pid == $$.to_s

false
end

# Notify systemd with the provided state, via the notification socket, if
# any.
#
# Generally this method will be used indirectly through the other methods
# of the library.
#
# @param state [String]
# @param unset_env [Boolean]
#
# @return [Fixnum, nil] the number of bytes written to the notification
# socket or nil if there was no socket to report to (eg. the program wasn't
# started by systemd)
#
# @raise [NotifyError] if there was an error communicating with the systemd
# socket
#
# @see https://www.freedesktop.org/software/systemd/man/sd_notify.html
def self.notify(state, unset_env=false)
sock = ENV["NOTIFY_SOCKET"]

return nil if !sock

ENV.delete("NOTIFY_SOCKET") if unset_env

begin
Addrinfo.unix(sock, :DGRAM).connect do |s|
s.close_on_exec = true
s.write(state)
end
rescue StandardError => e
raise NotifyError, "#{e.class}: #{e.message}", e.backtrace
end
end
end
end
2 changes: 1 addition & 1 deletion lib/puma/systemd.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

require 'sd_notify'
require_relative 'sd_notify'

module Puma
class Systemd
Expand Down
2 changes: 0 additions & 2 deletions test/test_integration_systemd.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
require_relative "helper"
require_relative "helpers/integration"

require 'sd_notify'

class TestIntegrationSystemd < TestIntegration
def setup
skip "Skipped because Systemd support is linux-only" if windows? || osx?
Expand Down

0 comments on commit 6d8b728

Please sign in to comment.