Packages
postgrex
0.17.4
1.0.0-rc.1
retired
1.0.0-rc.0
retired
0.22.3
0.22.2
0.22.1
0.22.0
0.21.1
0.21.0
0.20.0
0.19.3
0.19.2
0.19.1
0.19.0
0.18.0
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.13
0.15.12
0.15.11
0.15.10
0.15.9
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.5
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.13.0-rc.0
0.12.2
0.12.1
0.12.0
0.11.2
0.11.1
0.11.0
0.10.0
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.0
0.6.0
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.2
PostgreSQL driver for Elixir
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/postgrex/simple_connection.ex
defmodule Postgrex.SimpleConnection do
@moduledoc ~S"""
A generic connection suitable for simple queries and pubsub functionality.
On its own, a SimpleConnection server only maintains a connection. To execute
queries, process results, or relay notices you must implement a callback module
with the SimpleConnection behaviour.
## Example
The SimpleConnection behaviour abstracts common client/server interactions,
along with optional mechanisms for running queries or relaying notifications.
Let's start with a minimal callback module that executes a query and relays
the result back to the caller.
defmodule MyConnection do
@behaviour Postgrex.SimpleConnection
@impl true
def init(_args) do
{:ok, %{from: nil}}
end
@impl true
def handle_call({:query, query}, from, state) do
{:query, query, %{state | from: from}}
end
@impl true
def handle_result(results, state) when is_list(results) do
SimpleConnection.reply(state.from, results)
{:noreply, state}
end
@impl true
def handle_result(%Postgrex.Error{} = error, state) do
SimpleConnection.reply(state.from, error)
{:noreply, state}
end
end
# Start the connection
{:ok, pid} = SimpleConnection.start_link(MyConnection, [], database: "demo")
# Execute a literal query
SimpleConnection.call(pid, {:query, "SELECT 1"})
# => %Postgrex.Result{rows: [["1"]]}
We start a connection by passing the callback module, callback options, and
server options to `SimpleConnection.start_link/3`. The `init/1` function
receives any callback options and returns the callback state.
Queries are sent through `SimpleConnection.call/2`, executed on the server,
and the result is handed off to `handle_result/2`. At that point the callback
can process the result before replying back to the caller with
`SimpleConnection.reply/2`.
## Building a PubSub Connection
With the `notify/3` callback you can also build a pubsub server on top of
`LISTEN/NOTIFY`. Here's a naive pubsub implementation:
defmodule MyPubSub do
@behaviour Postgrex.SimpleConnection
defstruct [:from, listeners: %{}]
@impl true
def init(args) do
{:ok, struct!(__MODULE__, args)}
end
@impl true
def notify(channel, payload, state) do
for pid <- state.listeners[channel] do
send(pid, {:notice, channel, payload})
end
end
@impl true
def handle_call({:listen, channel}, {pid, _} = from, state) do
listeners = Map.update(state.listeners, channel, [pid], &[pid | &1])
{:query, ~s(LISTEN "#{channel}"), %{state | from: from, listeners: listeners}}
end
def handle_call({:query, query}, from, state) do
{:query, query, %{state | from: from}}
end
@impl true
def handle_result(_results, state) do
SimpleConnection.reply(state.from, :ok)
{:noreply, %{state | from: nil}}
end
end
# Start the connection
{:ok, pid} = SimpleConnection.start_link(MyPubSub, [], database: "demo")
# Start listening to the "demo" channel
SimpleConnection.call(pid, {:listen, "demo"})
# => %Postgrex.Result{command: :listen}
# Notify all listeners
SimpleConnection.call(pid, {:query, ~s(NOTIFY "demo", 'hello')})
# => %Postgrex.Result{command: :notify}
# Check the inbox to see the notice message
flush()
# => {:notice, "demo", "hello"}
See `Postgrex.Notifications` for a more complex implementation that can
unlisten, handle process exits, and persist across reconnection.
## Name registration
A `Postgrex.SimpleConnection` is bound to the same name registration rules as a
`GenServer`. Read more about them in the `GenServer` docs.
"""
@behaviour :gen_statem
require Logger
alias Postgrex.Protocol
@doc false
defstruct idle_interval: 5000,
protocol: nil,
auto_reconnect: false,
reconnect_backoff: 500,
state: nil
## PUBLIC API ##
@type query :: iodata
@type state :: term
@typedoc since: "0.17.0"
@type from :: {pid, term}
@doc """
Callback for process initialization.
This is called once and before the Postgrex connection is established.
"""
@callback init(term) :: {:ok, state}
@doc """
Callback for processing or relaying pubsub notifications.
"""
@callback notify(binary, binary, state) :: :ok
@doc """
Invoked after connecting or reconnecting.
This may be called multiple times if `:auto_reconnect` is true.
"""
@callback handle_connect(state) :: {:noreply, state} | {:query, query, state}
@doc """
Invoked after disconnection.
This is invoked regardless of the `:auto_reconnect` option.
"""
@callback handle_disconnect(state) :: {:noreply, state}
@doc """
Callback for `SimpleConnection.call/3`.
Replies must be sent with `SimpleConnection.reply/2`.
"""
@callback handle_call(term, from, state) ::
{:noreply, state} | {:query, query, state}
@doc """
Callback for `Kernel.send/2`.
"""
@callback handle_info(term, state) :: {:noreply, state} | {:query, query, state}
@doc """
Callback for processing or relaying queries executed via `{:query, query, state}`.
Either a list of successful query results or an error will be passed to this callback.
A list is passed because the simple query protocol allows multiple commands to be
issued in a single query.
"""
@callback handle_result([Postgrex.Result.t()] | Postgrex.Error.t(), state) ::
{:noreply, state}
@optional_callbacks handle_call: 3,
handle_connect: 1,
handle_disconnect: 1,
handle_info: 2,
handle_result: 2
@doc """
Replies to the given client.
Wrapper for `:gen_statem.reply/2`.
"""
def reply({caller_pid, from} = _from, reply) when is_pid(caller_pid) do
:gen_statem.reply(from, reply)
end
@doc """
Calls the given server.
Wrapper for `:gen_statem.call/3`.
"""
def call(server, message, timeout \\ 5000) do
with {__MODULE__, reason} <- :gen_statem.call(server, {message, self()}, timeout) do
exit({reason, {__MODULE__, :call, [server, message, timeout]}})
end
end
@doc false
def child_spec(opts) do
%{id: __MODULE__, start: {__MODULE__, :start_link, opts}}
end
@doc """
Start the connection process and connect to Postgres.
The options that this function accepts are the same as those accepted by
`Postgrex.start_link/1`, as well as the extra options `:sync_connect`,
`:auto_reconnect`, `:reconnect_backoff`, and `:configure`.
## Options
* `:auto_reconnect` - automatically attempt to reconnect to the database
in event of a disconnection. See the
[note about async connect and auto-reconnects](#module-async-connect-and-auto-reconnects)
above. Defaults to `false`, which means the process terminates.
* `:configure` - A function to run before every connect attempt to dynamically
configure the options as a `{module, function, args}`, where the current
options will prepended to `args`. Defaults to `nil`.
* `:idle_interval` - while also accepted on `Postgrex.start_link/1`, it has
a default of `5000ms` in `Postgrex.SimpleConnection` (instead of 1000ms).
* `:reconnect_backoff` - time (in ms) between reconnection attempts when
`auto_reconnect` is enabled. Defaults to `500`.
* `:sync_connect` - controls if the connection should be established on boot
or asynchronously right after boot. Defaults to `true`.
"""
@spec start_link(module, term, Keyword.t()) :: {:ok, pid} | {:error, Postgrex.Error.t() | term}
def start_link(module, args, opts) do
{name, opts} = Keyword.pop(opts, :name)
opts = Keyword.put_new(opts, :sync_connect, true)
connection_opts = Postgrex.Utils.default_opts(opts)
start_args = {module, args, connection_opts}
case name do
nil ->
:gen_statem.start_link(__MODULE__, start_args, [])
atom when is_atom(atom) ->
:gen_statem.start_link({:local, atom}, __MODULE__, start_args, [])
{:global, _term} = tuple ->
:gen_statem.start_link(tuple, __MODULE__, start_args, [])
{:via, via_module, _term} = tuple when is_atom(via_module) ->
:gen_statem.start_link(tuple, __MODULE__, start_args, [])
other ->
raise ArgumentError, """
expected :name option to be one of the following:
* nil
* atom
* {:global, term}
* {:via, module, term}
Got: #{inspect(other)}
"""
end
end
## CALLBACKS ##
@state :no_state
@doc false
@impl :gen_statem
def callback_mode, do: :handle_event_function
@doc false
@impl :gen_statem
def init({mod, args, opts}) do
case mod.init(args) do
{:ok, mod_state} ->
idle_timeout = opts[:idle_timeout]
if idle_timeout do
Logger.warning(
":idle_timeout in Postgrex.SimpleConnection is deprecated, " <>
"please use :idle_interval instead"
)
end
{idle_interval, opts} = Keyword.pop(opts, :idle_interval, idle_timeout || 5000)
{auto_reconnect, opts} = Keyword.pop(opts, :auto_reconnect, false)
{reconnect_backoff, opts} = Keyword.pop(opts, :reconnect_backoff, 500)
state = %__MODULE__{
idle_interval: idle_interval,
auto_reconnect: auto_reconnect,
reconnect_backoff: reconnect_backoff,
state: {mod, mod_state}
}
put_opts(mod, opts)
if opts[:sync_connect] do
case handle_event(:internal, {:connect, :init}, @state, state) do
{:keep_state, state} -> {:ok, @state, state}
{:keep_state, state, actions} -> {:ok, @state, state, actions}
{:stop, _reason, _state} = stop -> stop
end
else
{:ok, @state, state, {:next_event, :internal, {:connect, :init}}}
end
end
end
@doc false
@impl :gen_statem
def handle_event(type, content, statem_state, state)
def handle_event(:internal, {:connect, _}, @state, %{state: {mod, mod_state}} = state) do
opts =
case Keyword.get(opts(mod), :configure) do
{module, fun, args} -> apply(module, fun, [opts(mod) | args])
fun when is_function(fun, 1) -> fun.(opts(mod))
nil -> opts(mod)
end
case Protocol.connect(opts) do
{:ok, protocol} ->
state = %{state | protocol: protocol}
with {:keep_state, state, _} <- maybe_handle(mod, :handle_connect, [mod_state], state) do
{:keep_state, state}
end
{:error, reason} ->
if state.auto_reconnect do
{:keep_state, state, {{:timeout, :backoff}, state.reconnect_backoff, nil}}
else
{:stop, reason, state}
end
end
end
def handle_event({:timeout, :backoff}, nil, @state, state) do
{:keep_state, state, {:next_event, :internal, {:connect, :reconnect}}}
end
def handle_event({:call, from}, {msg, caller_pid}, @state, %{state: {mod, mod_state}} = state) do
# We have to do a hack here to carry the actual caller PID over to the handle_call/3
# callback, because gen_statem uses a proxy process to do calls with timeout != :infinity.
# This results in the caller PID not being the same as the PID in the "from" tuple,
# so things like Postgrex.Notifications cannot use that "from"'s PID to register
# notification handlers. This approach is paired with reconstructing the proper
# "from" tuple in the reply/2 function in this module.
callback_from = {caller_pid, from}
handle(mod, :handle_call, [msg, callback_from, mod_state], from, state)
end
def handle_event(:timeout, nil, @state, %{protocol: protocol} = state) do
case Protocol.ping(protocol) do
{:ok, protocol} ->
{:keep_state, %{state | protocol: protocol}, {:timeout, state.idle_interval, nil}}
{error, reason, protocol} ->
reconnect_or_stop(error, reason, protocol, state)
end
end
def handle_event(:info, msg, @state, %{protocol: protocol, state: {mod, mod_state}} = state) do
opts = [notify: &mod.notify(&1, &2, mod_state)]
case Protocol.handle_info(msg, opts, protocol) do
{:ok, protocol} ->
{:keep_state, %{state | protocol: protocol}, {:timeout, state.idle_interval, nil}}
{:unknown, protocol} ->
maybe_handle(mod, :handle_info, [msg, mod_state], %{state | protocol: protocol})
{error, reason, protocol} ->
reconnect_or_stop(error, reason, protocol, state)
end
end
def handle_event(:info, msg, @state, %{state: {mod, mod_state}} = state) do
maybe_handle(mod, :handle_info, [msg, mod_state], state)
end
@doc false
@impl :gen_statem
def format_status(_opts, [_pdict, @state, state]) do
state
end
## Helpers
defp maybe_handle(mod, fun, args, state) do
if function_exported?(mod, fun, length(args)) do
handle(mod, fun, args, nil, state)
else
{:keep_state, state, {:timeout, state.idle_interval, nil}}
end
end
defp handle(mod, fun, args, from, state) do
case apply(mod, fun, args) do
{:noreply, mod_state} ->
{:keep_state, %{state | state: {mod, mod_state}}, {:timeout, state.idle_interval, nil}}
{:query, query, mod_state} ->
opts = [notify: &mod.notify(&1, &2, mod_state)]
state = %{state | state: {mod, mod_state}}
with {:ok, results, protocol} <- Protocol.handle_simple(query, opts, state.protocol),
{:ok, protocol} <- Protocol.checkin(protocol) do
state = %{state | protocol: protocol}
handle(mod, :handle_result, [results, mod_state], from, state)
else
{:error, %Postgrex.Error{} = error, protocol} ->
handle(mod, :handle_result, [error, mod_state], from, %{state | protocol: protocol})
{:disconnect, reason, protocol} ->
reconnect_or_stop(:disconnect, reason, protocol, state)
end
end
end
defp reconnect_or_stop(error, reason, protocol, %{state: {mod, mod_state}} = state)
when error in [:error, :disconnect] do
{:keep_state, state, _actions} = maybe_handle(mod, :handle_disconnect, [mod_state], state)
if state.auto_reconnect do
{:keep_state, state, {:next_event, :internal, {:connect, :reconnect}}}
else
{:stop, reason, %{state | protocol: protocol}}
end
end
defp opts(mod), do: Process.get(mod)
defp put_opts(mod, opts), do: Process.put(mod, opts)
end