Sovite.Systemd (sovite v0.6.0)

Copy Markdown View Source

The systemd service manager protocol, without libsystemd: readiness and status notifications (sd_notify(3)), the watchdog (sd_watchdog_enabled(3), see Sovite.Systemd.Watchdog), and socket activation (sd_listen_fds(3)).

:ok = Sovite.Systemd.ready(status: "Accepting mail")

Everything is read from the environment systemd sets for the service. Outside systemd these variables are missing, and every function is a no-op: notifications return {:error, :no_socket}, listen_fds/1 returns [], and watchdog_interval/1 returns :disabled. Callers can ignore the results.

Notifications are only accepted from the service's main process unless the unit sets NotifyAccess=all, so the BEAM must be the process systemd started (a release's start script execs it, which is enough).

Options

Every function takes:

  • :env - a map of environment variables to read instead of the OS environment, for tests. Defaults to reading System.get_env/1.

Summary

Types

Why a notification was not sent: :no_socket outside systemd, :invalid_socket for a NOTIFY_SOCKET that is not an absolute path or an abstract @name, :unsupported for a VSOCK address, or a :socket error.

A notification: a string such as "READY=1", or a list of {key, value} pairs, sent as KEY=value lines. Atom keys are upcased, so [ready: 1, status: "Started"] sends "READY=1\nSTATUS=Started\n". Newlines in values are replaced by spaces.

Functions

Sends EXTEND_TIMEOUT_USEC=usec: the start, reload, or stop in progress needs usec more microseconds.

Returns the sockets systemd passed for socket activation, as {name, fd} pairs in order, or [] when there are none or they are meant for another process (LISTEN_PID).

Opens a :gen_tcp listen socket on fd, an inherited listening socket (from listen_fds/1). opts are :gen_tcp.listen/2 options, such as :binary and active: false. The socket is already bound, so leave out :ip and :port.

Returns the current time of CLOCK_MONOTONIC in microseconds, the clock of MONOTONIC_USEC, or :error when the runtime does not read that clock.

Sends a notification to the service manager, through the unix datagram socket in NOTIFY_SOCKET. A socket name starting with @ is in the abstract namespace.

Returns whether NOTIFY_SOCKET is set, so notifications can be sent.

Sends READY=1: the service has started. Takes the :status option, a text sent with it (see status/2).

Sends RELOADING=1: the service is reloading its configuration. Send ready/1 when done. Takes the :status option, as ready/1.

The address an inherited socket is bound to, as {ip, port} for a TCP or UDP socket, or {:local, path}. The socket is not changed: this looks at a duplicate of fd. Returns :error for a closed fd or another kind of socket.

Sends STATUS=text, shown by systemctl status. Newlines in text are replaced by spaces.

Sends STOPPING=1: the service is shutting down. Takes the :status option, as ready/1.

Sends WATCHDOG=1, the watchdog keep-alive ping. See Sovite.Systemd.Watchdog to send it on a schedule.

Returns the watchdog interval in milliseconds, from WATCHDOG_USEC, or :disabled. The watchdog is disabled when WATCHDOG_USEC is not a positive integer, or when WATCHDOG_PID is set to another process.

Types

notify_error()

@type notify_error() :: :no_socket | :invalid_socket | :unsupported | term()

Why a notification was not sent: :no_socket outside systemd, :invalid_socket for a NOTIFY_SOCKET that is not an absolute path or an abstract @name, :unsupported for a VSOCK address, or a :socket error.

state()

@type state() :: String.t() | [{atom() | String.t(), String.Chars.t()}]

A notification: a string such as "READY=1", or a list of {key, value} pairs, sent as KEY=value lines. Atom keys are upcased, so [ready: 1, status: "Started"] sends "READY=1\nSTATUS=Started\n". Newlines in values are replaced by spaces.

Functions

extend_timeout(usec, opts \\ [])

@spec extend_timeout(non_neg_integer(), keyword()) :: :ok | {:error, notify_error()}

Sends EXTEND_TIMEOUT_USEC=usec: the start, reload, or stop in progress needs usec more microseconds.

listen_fds(opts \\ [])

@spec listen_fds(keyword()) :: [{String.t(), non_neg_integer()}]

Returns the sockets systemd passed for socket activation, as {name, fd} pairs in order, or [] when there are none or they are meant for another process (LISTEN_PID).

Names come from FileDescriptorName= in the socket units (LISTEN_FDNAMES). When they are missing, or their number does not match, every name is "unknown".

Turn a listening TCP socket into a :gen_tcp socket with listen_socket/2.

Options

  • :env - as in the module docs.
  • :unset - remove LISTEN_PID, LISTEN_FDS, and LISTEN_FDNAMES from the OS environment, so processes started later do not see them. Ignored with :env. Defaults to false.

listen_socket(fd, opts \\ [])

@spec listen_socket(non_neg_integer(), [:gen_tcp.listen_option()]) ::
  {:ok, :gen_tcp.socket()} | {:error, term()}

Opens a :gen_tcp listen socket on fd, an inherited listening socket (from listen_fds/1). opts are :gen_tcp.listen/2 options, such as :binary and active: false. The socket is already bound, so leave out :ip and :port.

:gen_tcp must be told the socket's family. Without :inet, :inet6, or :local in opts, each is tried in turn, since opening a socket as the wrong family fails without closing it.

monotonic_usec()

@spec monotonic_usec() :: {:ok, non_neg_integer()} | :error

Returns the current time of CLOCK_MONOTONIC in microseconds, the clock of MONOTONIC_USEC, or :error when the runtime does not read that clock.

Erlang's own monotonic time has an arbitrary offset, and its rate can be corrected, so this reads the runtime's OS monotonic time source instead, when it is clock_gettime(CLOCK_MONOTONIC) (as on Linux).

notify(state, opts \\ [])

@spec notify(state(), keyword()) :: :ok | {:error, notify_error()}

Sends a notification to the service manager, through the unix datagram socket in NOTIFY_SOCKET. A socket name starting with @ is in the abstract namespace.

Returns :ok once the datagram is sent, or {:error, reason} (see notify_error/0). It never raises.

notify_socket?(opts \\ [])

@spec notify_socket?(keyword()) :: boolean()

Returns whether NOTIFY_SOCKET is set, so notifications can be sent.

ready(opts \\ [])

@spec ready(keyword()) :: :ok | {:error, notify_error()}

Sends READY=1: the service has started. Takes the :status option, a text sent with it (see status/2).

reloading(opts \\ [])

@spec reloading(keyword()) :: :ok | {:error, notify_error()}

Sends RELOADING=1: the service is reloading its configuration. Send ready/1 when done. Takes the :status option, as ready/1.

MONOTONIC_USEC is sent with it when monotonic_usec/0 can read the clock, as Type=notify-reload units need. Where it cannot, use Type=notify.

socket_address(fd)

@spec socket_address(non_neg_integer()) ::
  {:ok, {:inet.ip_address(), :inet.port_number()} | {:local, binary()}} | :error

The address an inherited socket is bound to, as {ip, port} for a TCP or UDP socket, or {:local, path}. The socket is not changed: this looks at a duplicate of fd. Returns :error for a closed fd or another kind of socket.

status(text, opts \\ [])

@spec status(String.t(), keyword()) :: :ok | {:error, notify_error()}

Sends STATUS=text, shown by systemctl status. Newlines in text are replaced by spaces.

stopping(opts \\ [])

@spec stopping(keyword()) :: :ok | {:error, notify_error()}

Sends STOPPING=1: the service is shutting down. Takes the :status option, as ready/1.

watchdog(opts \\ [])

@spec watchdog(keyword()) :: :ok | {:error, notify_error()}

Sends WATCHDOG=1, the watchdog keep-alive ping. See Sovite.Systemd.Watchdog to send it on a schedule.

watchdog_interval(opts \\ [])

@spec watchdog_interval(keyword()) :: {:ok, pos_integer()} | :disabled

Returns the watchdog interval in milliseconds, from WATCHDOG_USEC, or :disabled. The watchdog is disabled when WATCHDOG_USEC is not a positive integer, or when WATCHDOG_PID is set to another process.

The service must send watchdog/1 within every interval; sending it every half interval is the usual choice.