update
This commit is contained in:
1991
phoenix/deps/db_connection/lib/db_connection.ex
Normal file
1991
phoenix/deps/db_connection/lib/db_connection.ex
Normal file
File diff suppressed because it is too large
Load Diff
23
phoenix/deps/db_connection/lib/db_connection/app.ex
Normal file
23
phoenix/deps/db_connection/lib/db_connection/app.ex
Normal file
@@ -0,0 +1,23 @@
|
||||
defmodule DBConnection.App do
|
||||
@moduledoc false
|
||||
use Application
|
||||
|
||||
@impl true
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
{Task.Supervisor, name: DBConnection.Task},
|
||||
dynamic_supervisor(DBConnection.Ownership.Supervisor),
|
||||
dynamic_supervisor(DBConnection.ConnectionPool.Supervisor),
|
||||
DBConnection.Watcher
|
||||
]
|
||||
|
||||
Supervisor.start_link(children, strategy: :one_for_all, name: __MODULE__)
|
||||
end
|
||||
|
||||
defp dynamic_supervisor(name) do
|
||||
Supervisor.child_spec(
|
||||
{DynamicSupervisor, name: name, strategy: :one_for_one},
|
||||
id: name
|
||||
)
|
||||
end
|
||||
end
|
||||
111
phoenix/deps/db_connection/lib/db_connection/backoff.ex
Normal file
111
phoenix/deps/db_connection/lib/db_connection/backoff.ex
Normal file
@@ -0,0 +1,111 @@
|
||||
defmodule DBConnection.Backoff do
|
||||
# This module provides a functional abstraction over backoffs with different types. It exposes
|
||||
# a struct and a couple of functions to work with it.
|
||||
@moduledoc false
|
||||
@compile :nowarn_deprecated_function
|
||||
|
||||
alias DBConnection.Backoff
|
||||
|
||||
@default_type :rand_exp
|
||||
@min 1_000
|
||||
@max 30_000
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
type: :stop | :rand | :exp | :rand_exp,
|
||||
min: non_neg_integer(),
|
||||
max: non_neg_integer(),
|
||||
state: term()
|
||||
}
|
||||
|
||||
defstruct [:type, :min, :max, :state]
|
||||
|
||||
@spec new(keyword) :: t | nil
|
||||
def new(opts) when is_list(opts) do
|
||||
case Keyword.get(opts, :backoff_type, @default_type) do
|
||||
:stop ->
|
||||
nil
|
||||
|
||||
type ->
|
||||
{min, max} = min_max(opts)
|
||||
new(type, min, max)
|
||||
end
|
||||
end
|
||||
|
||||
@spec backoff(t) :: {non_neg_integer, t}
|
||||
def backoff(backoff)
|
||||
|
||||
def backoff(%Backoff{type: :rand, min: min, max: max} = s) do
|
||||
{rand(min, max), s}
|
||||
end
|
||||
|
||||
def backoff(%Backoff{type: :exp, min: min, state: nil} = s) do
|
||||
{min, %{s | state: min}}
|
||||
end
|
||||
|
||||
def backoff(%Backoff{type: :exp, max: max, state: prev} = s) do
|
||||
next = min(Bitwise.<<<(prev, 1), max)
|
||||
{next, %{s | state: next}}
|
||||
end
|
||||
|
||||
def backoff(%Backoff{type: :rand_exp, max: max, state: state} = s) do
|
||||
{prev, lower} = state
|
||||
next_min = min(prev, lower)
|
||||
next_max = min(prev * 3, max)
|
||||
next = rand(next_min, next_max)
|
||||
{next, %{s | state: {next, lower}}}
|
||||
end
|
||||
|
||||
@spec reset(t) :: t
|
||||
def reset(backoff)
|
||||
|
||||
def reset(%Backoff{type: :rand} = s), do: s
|
||||
def reset(%Backoff{type: :exp} = s), do: %{s | state: nil}
|
||||
|
||||
def reset(%Backoff{type: :rand_exp, min: min, state: {_, lower}} = s) do
|
||||
%{s | state: {min, lower}}
|
||||
end
|
||||
|
||||
## Internal
|
||||
|
||||
defp min_max(opts) do
|
||||
case {opts[:backoff_min], opts[:backoff_max]} do
|
||||
{nil, nil} -> {@min, @max}
|
||||
{nil, max} -> {min(@min, max), max}
|
||||
{min, nil} -> {min, max(min, @max)}
|
||||
{min, max} -> {min, max}
|
||||
end
|
||||
end
|
||||
|
||||
defp new(_, min, _) when not (is_integer(min) and min >= 0) do
|
||||
raise ArgumentError, "minimum #{inspect(min)} not 0 or a positive integer"
|
||||
end
|
||||
|
||||
defp new(_, _, max) when not (is_integer(max) and max >= 0) do
|
||||
raise ArgumentError, "maximum #{inspect(max)} not 0 or a positive integer"
|
||||
end
|
||||
|
||||
defp new(_, min, max) when min > max do
|
||||
raise ArgumentError, "minimum #{min} is greater than maximum #{max}"
|
||||
end
|
||||
|
||||
defp new(:rand, min, max) do
|
||||
%Backoff{type: :rand, min: min, max: max, state: nil}
|
||||
end
|
||||
|
||||
defp new(:exp, min, max) do
|
||||
%Backoff{type: :exp, min: min, max: max, state: nil}
|
||||
end
|
||||
|
||||
defp new(:rand_exp, min, max) do
|
||||
lower = max(min, div(max, 3))
|
||||
%Backoff{type: :rand_exp, min: min, max: max, state: {min, lower}}
|
||||
end
|
||||
|
||||
defp new(type, _, _) do
|
||||
raise ArgumentError, "unknown type #{inspect(type)}"
|
||||
end
|
||||
|
||||
defp rand(min, max) do
|
||||
:rand.uniform(max - min + 1) + min - 1
|
||||
end
|
||||
end
|
||||
536
phoenix/deps/db_connection/lib/db_connection/connection.ex
Normal file
536
phoenix/deps/db_connection/lib/db_connection/connection.ex
Normal file
@@ -0,0 +1,536 @@
|
||||
defmodule DBConnection.Connection do
|
||||
@moduledoc false
|
||||
|
||||
@behaviour :gen_statem
|
||||
|
||||
require Logger
|
||||
alias DBConnection.Backoff
|
||||
alias DBConnection.Holder
|
||||
alias DBConnection.Util
|
||||
|
||||
@timeout 15_000
|
||||
@sensitive_opts [:parameters, :hostname, :port, :username, :password, :database]
|
||||
|
||||
@doc false
|
||||
def start_link(mod, opts, pool, tag) do
|
||||
start_opts = Keyword.take(opts, [:debug, :spawn_opt])
|
||||
:gen_statem.start_link(__MODULE__, {mod, opts, pool, tag}, start_opts)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def child_spec(mod, opts, pool, tag, child_opts) do
|
||||
Supervisor.child_spec(
|
||||
%{id: __MODULE__, start: {__MODULE__, :start_link, [mod, opts, pool, tag]}},
|
||||
child_opts
|
||||
)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def disconnect({pid, ref}, err, state) do
|
||||
:gen_statem.cast(pid, {:disconnect, ref, err, state})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def stop({pid, ref}, err, state) do
|
||||
:gen_statem.cast(pid, {:stop, ref, err, state})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def ping({pid, ref}, state) do
|
||||
:gen_statem.cast(pid, {:ping, ref, state})
|
||||
end
|
||||
|
||||
## gen_statem API
|
||||
|
||||
@doc false
|
||||
@impl :gen_statem
|
||||
def callback_mode, do: :handle_event_function
|
||||
|
||||
@doc false
|
||||
@impl :gen_statem
|
||||
def init({mod, opts, pool, tag}) do
|
||||
pool_index = Keyword.get(opts, :pool_index)
|
||||
label = if pool_index, do: "db_conn_#{pool_index}", else: "db_conn"
|
||||
Util.set_label(label)
|
||||
|
||||
s = %{
|
||||
mod: mod,
|
||||
opts: opts,
|
||||
state: nil,
|
||||
client: :closed,
|
||||
pool: pool,
|
||||
tag: tag,
|
||||
timer: nil,
|
||||
backoff: Backoff.new(opts),
|
||||
connection_listeners: Keyword.get(opts, :connection_listeners, []),
|
||||
after_connect: Keyword.get(opts, :after_connect),
|
||||
after_connect_timeout: Keyword.get(opts, :after_connect_timeout, @timeout)
|
||||
}
|
||||
|
||||
{:ok, :no_state, s, {:next_event, :internal, {:connect, :init}}}
|
||||
end
|
||||
|
||||
@impl :gen_statem
|
||||
def handle_event(type, info, state, s)
|
||||
|
||||
def handle_event(:internal, {:connect, _info}, :no_state, s) do
|
||||
%{mod: mod, opts: opts, backoff: backoff, after_connect: after_connect} = s
|
||||
|
||||
try do
|
||||
apply(mod, :connect, [connect_opts(opts)])
|
||||
rescue
|
||||
e ->
|
||||
{e, stack} = maybe_sanitize_exception(e, __STACKTRACE__, opts)
|
||||
reraise e, stack
|
||||
else
|
||||
{:ok, state} when after_connect != nil ->
|
||||
ref = make_ref()
|
||||
:gen_statem.cast(self(), {:after_connect, ref})
|
||||
{:keep_state, %{s | state: state, client: {ref, :connect}}}
|
||||
|
||||
{:ok, state} ->
|
||||
backoff = backoff && Backoff.reset(backoff)
|
||||
ref = make_ref()
|
||||
:gen_statem.cast(self(), {:connected, ref})
|
||||
{:keep_state, %{s | state: state, client: {ref, :connect}, backoff: backoff}}
|
||||
|
||||
{:error, err} when is_nil(backoff) ->
|
||||
Logger.error(
|
||||
fn ->
|
||||
[
|
||||
inspect(mod),
|
||||
" (",
|
||||
Util.inspect_pid(self()),
|
||||
") failed to connect: " | Exception.format_banner(:error, err, [])
|
||||
]
|
||||
end,
|
||||
crash_reason: {err, []}
|
||||
)
|
||||
|
||||
raise err
|
||||
|
||||
{:error, err} ->
|
||||
Logger.error(
|
||||
fn ->
|
||||
[
|
||||
inspect(mod),
|
||||
?\s,
|
||||
?(,
|
||||
Util.inspect_pid(self()),
|
||||
") failed to connect: "
|
||||
| Exception.format_banner(:error, err, [])
|
||||
]
|
||||
end,
|
||||
crash_reason: {err, []}
|
||||
)
|
||||
|
||||
{timeout, backoff} = Backoff.backoff(backoff)
|
||||
{:keep_state, %{s | backoff: backoff}, {{:timeout, :backoff}, timeout, nil}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event(:internal, {:disconnect, {log, err}}, :no_state, %{mod: mod} = s) do
|
||||
if log == :log do
|
||||
severity =
|
||||
case err do
|
||||
%DBConnection.ConnectionError{severity: severity} -> severity
|
||||
_ -> :error
|
||||
end
|
||||
|
||||
Logger.log(severity, fn ->
|
||||
[
|
||||
inspect(mod),
|
||||
?\s,
|
||||
?(,
|
||||
Util.inspect_pid(self()),
|
||||
") disconnected: " | Exception.format_banner(:error, err, [])
|
||||
]
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
%{state: state, client: client, timer: timer, backoff: backoff} = s
|
||||
demonitor(client)
|
||||
cancel_timer(timer)
|
||||
:ok = apply(mod, :disconnect, [err, state])
|
||||
s = %{s | state: nil, client: :closed, timer: nil}
|
||||
|
||||
notify_connection_listeners(:disconnected, s)
|
||||
|
||||
case client do
|
||||
_ when backoff == nil ->
|
||||
{:stop, {:shutdown, err}, s}
|
||||
|
||||
{_, :after_connect} ->
|
||||
{timeout, backoff} = Backoff.backoff(backoff)
|
||||
{:keep_state, %{s | backoff: backoff}, {{:timeout, :backoff}, timeout, nil}}
|
||||
|
||||
_ ->
|
||||
{:keep_state, s, {:next_event, :internal, {:connect, :disconnect}}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event({:timeout, :backoff}, _content, :no_state, s) do
|
||||
{:keep_state, s, {:next_event, :internal, {:connect, :backoff}}}
|
||||
end
|
||||
|
||||
def handle_event(:cast, {:ping, ref, state}, :no_state, %{client: {ref, :pool}, mod: mod} = s) do
|
||||
case apply(mod, :ping, [state]) do
|
||||
{:ok, state} ->
|
||||
pool_update(state, s)
|
||||
|
||||
{:disconnect, err, state} ->
|
||||
{:keep_state, %{s | state: state}, {:next_event, :internal, {:disconnect, {:log, err}}}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event(:cast, {:disconnect, ref, err, state}, :no_state, %{client: {ref, _}} = s) do
|
||||
{:keep_state, %{s | state: state}, {:next_event, :internal, {:disconnect, {:log, err}}}}
|
||||
end
|
||||
|
||||
def handle_event(:cast, {:stop, ref, err, state}, :no_state, %{client: {ref, _}} = s) do
|
||||
{_, stack} = :erlang.process_info(self(), :current_stacktrace)
|
||||
|
||||
case err do
|
||||
ok when ok in [:normal, :shutdown] ->
|
||||
:ok
|
||||
|
||||
{:shutdown, _term} ->
|
||||
:ok
|
||||
|
||||
_ ->
|
||||
reason =
|
||||
case err do
|
||||
%{__exception__: true} -> Exception.format_banner(:error, err, stack)
|
||||
_other -> "** #{inspect(err)}"
|
||||
end
|
||||
|
||||
format =
|
||||
~c"** State machine ~p terminating~n" ++
|
||||
~c"** Reason for termination ==~n" ++
|
||||
~c"~s~n"
|
||||
|
||||
:error_logger.format(format, [self(), reason])
|
||||
end
|
||||
|
||||
{:stop, {err, stack}, %{s | state: state}}
|
||||
end
|
||||
|
||||
def handle_event(:cast, {tag, _, _, _}, :no_state, s) when tag in [:disconnect, :stop] do
|
||||
handle_timeout(s)
|
||||
end
|
||||
|
||||
def handle_event(:cast, {:after_connect, ref}, :no_state, %{client: {ref, :connect}} = s) do
|
||||
%{
|
||||
mod: mod,
|
||||
state: state,
|
||||
after_connect: after_connect,
|
||||
after_connect_timeout: timeout,
|
||||
opts: opts
|
||||
} = s
|
||||
|
||||
notify_connection_listeners(:connected, s)
|
||||
|
||||
case apply(mod, :checkout, [state]) do
|
||||
{:ok, state} ->
|
||||
opts = [timeout: timeout] ++ opts
|
||||
opts = Keyword.drop(opts, @sensitive_opts)
|
||||
{pid, ref} = DBConnection.Task.run_child(mod, state, after_connect, opts)
|
||||
timer = start_timer(pid, timeout)
|
||||
s = %{s | client: {ref, :after_connect}, timer: timer, state: state}
|
||||
{:keep_state, s}
|
||||
|
||||
{:disconnect, err, state} ->
|
||||
{:keep_state, %{s | state: state}, {:next_event, :internal, {:disconnect, {:log, err}}}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event(:cast, {:after_connect, _}, :no_state, _s) do
|
||||
:keep_state_and_data
|
||||
end
|
||||
|
||||
def handle_event(:cast, {:connected, ref}, :no_state, %{client: {ref, :connect}} = s) do
|
||||
%{mod: mod, state: state} = s
|
||||
|
||||
notify_connection_listeners(:connected, s)
|
||||
|
||||
case apply(mod, :checkout, [state]) do
|
||||
{:ok, state} ->
|
||||
pool_update(state, s)
|
||||
|
||||
{:disconnect, err, state} ->
|
||||
{:keep_state, %{s | state: state}, {:next_event, :internal, {:disconnect, {:log, err}}}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event(:cast, {:connected, _}, :no_state, _s) do
|
||||
:keep_state_and_data
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
:info,
|
||||
{:DOWN, ref, _, pid, reason},
|
||||
:no_state,
|
||||
%{client: {ref, :after_connect}} = s
|
||||
) do
|
||||
message =
|
||||
"client #{Util.inspect_pid(pid)} exited: " <> Exception.format_exit(reason)
|
||||
|
||||
err = DBConnection.ConnectionError.exception(message)
|
||||
|
||||
{:keep_state, %{s | client: {nil, :after_connect}},
|
||||
{:next_event, :internal, {:disconnect, {down_log(reason), err}}}}
|
||||
end
|
||||
|
||||
def handle_event(:info, {:DOWN, mon, _, pid, reason}, :no_state, %{client: {ref, mon}} = s) do
|
||||
message =
|
||||
"client #{Util.inspect_pid(pid)} exited: " <> Exception.format_exit(reason)
|
||||
|
||||
err = DBConnection.ConnectionError.exception(message)
|
||||
|
||||
{:keep_state, %{s | client: {ref, nil}},
|
||||
{:next_event, :internal, {:disconnect, {down_log(reason), err}}}}
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
:info,
|
||||
{:timeout, timer, {__MODULE__, pid, timeout}},
|
||||
:no_state,
|
||||
%{timer: timer} = s
|
||||
)
|
||||
when is_reference(timer) do
|
||||
message =
|
||||
"client #{Util.inspect_pid(pid)} timed out because it checked out " <>
|
||||
"the connection for longer than #{timeout}ms"
|
||||
|
||||
exc =
|
||||
case Process.info(pid, :current_stacktrace) do
|
||||
{:current_stacktrace, stacktrace} ->
|
||||
message <>
|
||||
"\n\n#{Util.inspect_pid(pid)} was at location:\n\n" <>
|
||||
Exception.format_stacktrace(stacktrace)
|
||||
|
||||
_ ->
|
||||
message
|
||||
end
|
||||
|> DBConnection.ConnectionError.exception()
|
||||
|
||||
{:keep_state, %{s | timer: nil}, {:next_event, :internal, {:disconnect, {:log, exc}}}}
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
:info,
|
||||
{:"ETS-TRANSFER", holder, _pid, {msg, ref, extra}},
|
||||
:no_state,
|
||||
%{client: {ref, :after_connect}, timer: timer} = s
|
||||
) do
|
||||
{_, state} = Holder.delete(holder)
|
||||
cancel_timer(timer)
|
||||
s = %{s | timer: nil}
|
||||
|
||||
case msg do
|
||||
:checkin -> handle_checkin(state, s)
|
||||
:disconnect -> handle_event(:cast, {:disconnect, ref, extra, state}, :no_state, s)
|
||||
:stop -> handle_event(:cast, {:stop, ref, extra, state}, :no_state, s)
|
||||
end
|
||||
end
|
||||
|
||||
# We discard EXIT messages which may arrive if the process is trapping exits
|
||||
def handle_event(:info, {:EXIT, _, _}, :no_state, s) do
|
||||
handle_timeout(s)
|
||||
end
|
||||
|
||||
def handle_event(:info, msg, :no_state, %{mod: mod} = s) do
|
||||
Logger.info(fn ->
|
||||
[inspect(mod), ?\s, ?(, Util.inspect_pid(self()), ") missed message: " | inspect(msg)]
|
||||
end)
|
||||
|
||||
handle_timeout(s)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@impl :gen_statem
|
||||
# If client is :closed then the connection was previously disconnected
|
||||
# and cleanup is not required.
|
||||
def terminate(_, _, %{client: :closed}), do: :ok
|
||||
|
||||
def terminate(reason, _, s) do
|
||||
%{mod: mod, state: state} = s
|
||||
msg = "connection exited: " <> Exception.format_exit(reason)
|
||||
|
||||
msg
|
||||
|> DBConnection.ConnectionError.exception()
|
||||
|> mod.disconnect(state)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@impl :gen_statem
|
||||
def format_status(info, [_, :no_state, %{client: :closed, mod: mod}]) do
|
||||
case info do
|
||||
:normal -> [{:data, [{~c"Module", mod}]}]
|
||||
:terminate -> mod
|
||||
end
|
||||
end
|
||||
|
||||
def format_status(info, [pdict, :no_state, %{mod: mod, state: state}]) do
|
||||
case function_exported?(mod, :format_status, 2) do
|
||||
true when info == :normal ->
|
||||
normal_status(mod, pdict, state)
|
||||
|
||||
false when info == :normal ->
|
||||
normal_status_default(mod, state)
|
||||
|
||||
true when info == :terminate ->
|
||||
{mod, terminate_status(mod, pdict, state)}
|
||||
|
||||
false when info == :terminate ->
|
||||
{mod, state}
|
||||
end
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp maybe_sanitize_exception(e, stack, opts) do
|
||||
if Keyword.get(opts, :show_sensitive_data_on_connection_error, false) do
|
||||
{e, stack}
|
||||
else
|
||||
message =
|
||||
"connect raised #{inspect(e.__struct__)} exception#{sanitized_message(e)}. " <>
|
||||
"The exception details are hidden, as they may contain sensitive data such as " <>
|
||||
"database credentials. You may set :show_sensitive_data_on_connection_error " <>
|
||||
"to true when starting your connection if you wish to see all of the details"
|
||||
|
||||
{RuntimeError.exception(message), cleanup_stacktrace(stack)}
|
||||
end
|
||||
end
|
||||
|
||||
defp sanitized_message(%KeyError{} = e), do: ": #{Exception.message(%{e | term: nil})}"
|
||||
defp sanitized_message(_), do: ""
|
||||
|
||||
defp connect_opts(opts) do
|
||||
case Keyword.get(opts, :configure) do
|
||||
{mod, fun, args} ->
|
||||
apply(mod, fun, [opts | args])
|
||||
|
||||
fun when is_function(fun, 1) ->
|
||||
fun.(opts)
|
||||
|
||||
nil ->
|
||||
opts
|
||||
end
|
||||
end
|
||||
|
||||
defp down_log(:normal), do: :nolog
|
||||
defp down_log(:shutdown), do: :nolog
|
||||
defp down_log({:shutdown, _}), do: :nolog
|
||||
defp down_log(_), do: :log
|
||||
|
||||
defp handle_timeout(s), do: {:keep_state, s}
|
||||
|
||||
defp demonitor({_, mon}) when is_reference(mon) do
|
||||
Process.demonitor(mon, [:flush])
|
||||
end
|
||||
|
||||
defp demonitor({mon, :after_connect}) when is_reference(mon) do
|
||||
Process.demonitor(mon, [:flush])
|
||||
end
|
||||
|
||||
defp demonitor({_, _}), do: true
|
||||
defp demonitor(nil), do: true
|
||||
|
||||
defp start_timer(_, :infinity), do: nil
|
||||
|
||||
defp start_timer(pid, timeout) do
|
||||
:erlang.start_timer(timeout, self(), {__MODULE__, pid, timeout})
|
||||
end
|
||||
|
||||
defp cancel_timer(nil), do: :ok
|
||||
|
||||
defp cancel_timer(timer) do
|
||||
case :erlang.cancel_timer(timer) do
|
||||
false -> flush_timer(timer)
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp flush_timer(timer) do
|
||||
receive do
|
||||
{:timeout, ^timer, {__MODULE__, _, _}} ->
|
||||
:ok
|
||||
after
|
||||
0 ->
|
||||
raise ArgumentError, "timer #{inspect(timer)} does not exist"
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_checkin(state, s) do
|
||||
%{backoff: backoff, client: client} = s
|
||||
backoff = backoff && Backoff.reset(backoff)
|
||||
demonitor(client)
|
||||
pool_update(state, %{s | client: nil, backoff: backoff})
|
||||
end
|
||||
|
||||
defp pool_update(state, %{pool: pool, tag: tag, mod: mod} = s) do
|
||||
case Holder.update(pool, tag, mod, state) do
|
||||
{:ok, ref} ->
|
||||
{:keep_state, %{s | client: {ref, :pool}, state: state}, :hibernate}
|
||||
|
||||
:error ->
|
||||
{:stop, {:shutdown, :no_more_pool}, s}
|
||||
end
|
||||
end
|
||||
|
||||
defp normal_status(mod, pdict, state) do
|
||||
try do
|
||||
mod.format_status(:normal, [pdict, state])
|
||||
catch
|
||||
_, _ ->
|
||||
normal_status_default(mod, state)
|
||||
else
|
||||
status ->
|
||||
status
|
||||
end
|
||||
end
|
||||
|
||||
defp normal_status_default(mod, state) do
|
||||
[{:data, [{~c"Module", mod}, {~c"State", state}]}]
|
||||
end
|
||||
|
||||
defp terminate_status(mod, pdict, state) do
|
||||
try do
|
||||
mod.format_status(:terminate, [pdict, state])
|
||||
catch
|
||||
_, _ ->
|
||||
state
|
||||
else
|
||||
status ->
|
||||
status
|
||||
end
|
||||
end
|
||||
|
||||
defp cleanup_stacktrace(stack) do
|
||||
case stack do
|
||||
[{_, _, arity, _} | _rest] = stacktrace when is_integer(arity) ->
|
||||
stacktrace
|
||||
|
||||
[{mod, fun, args, info} | rest] when is_list(args) ->
|
||||
[{mod, fun, length(args), info} | rest]
|
||||
end
|
||||
end
|
||||
|
||||
defp notify_connection_listeners(action, %{} = state) do
|
||||
%{connection_listeners: connection_listeners} = state
|
||||
|
||||
{listeners, message} =
|
||||
case connection_listeners do
|
||||
listeners when is_list(listeners) ->
|
||||
{listeners, {action, self()}}
|
||||
|
||||
{listeners, tag} when is_list(listeners) ->
|
||||
{listeners, {action, self(), tag}}
|
||||
end
|
||||
|
||||
Enum.each(listeners, &send(&1, message))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
defmodule DBConnection.ConnectionError do
|
||||
@moduledoc """
|
||||
A generic connection error exception.
|
||||
|
||||
The raised exception might include the reason which would be useful
|
||||
to programmatically determine what was causing the error.
|
||||
"""
|
||||
|
||||
@typedoc since: "2.7.0"
|
||||
@type t() :: %__MODULE__{
|
||||
message: String.t(),
|
||||
reason: :error | :queue_timeout,
|
||||
severity: Logger.level()
|
||||
}
|
||||
|
||||
defexception [:message, severity: :error, reason: :error]
|
||||
|
||||
@doc false
|
||||
def exception(message, reason) when is_binary(message) and reason in [:error, :queue_timeout] do
|
||||
message
|
||||
|> exception()
|
||||
|> Map.replace!(:reason, reason)
|
||||
end
|
||||
end
|
||||
391
phoenix/deps/db_connection/lib/db_connection/connection_pool.ex
Normal file
391
phoenix/deps/db_connection/lib/db_connection/connection_pool.ex
Normal file
@@ -0,0 +1,391 @@
|
||||
defmodule DBConnection.ConnectionPool do
|
||||
@moduledoc """
|
||||
The default connection pool.
|
||||
|
||||
The queueing algorithm is based on [CoDel](https://queue.acm.org/appendices/codel.html).
|
||||
|
||||
You're not supposed to call any functions on this pool directly, but only pass this
|
||||
as the value of the `:pool` option in functions such as `DBConnection.start_link/2`.
|
||||
|
||||
`disconnect_all/3`, which by default will result in connections being
|
||||
reestablished, can be called periodically to recycle checked-in connections
|
||||
after a maximum lifetime is reached. `Ecto SQL` users may find it at
|
||||
https://hexdocs.pm/ecto_sql/Ecto.Adapters.SQL.html#disconnect_all/3
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
alias DBConnection.Holder
|
||||
alias DBConnection.Util
|
||||
|
||||
@behaviour DBConnection.Pool
|
||||
|
||||
@queue_target 50
|
||||
@queue_interval 2000
|
||||
@idle_interval 1000
|
||||
@time_unit 1000
|
||||
|
||||
@doc false
|
||||
def start_link({mod, opts}) do
|
||||
GenServer.start_link(__MODULE__, {mod, opts}, start_opts(opts))
|
||||
end
|
||||
|
||||
@doc false
|
||||
@impl DBConnection.Pool
|
||||
def checkout(pool, callers, opts) do
|
||||
Holder.checkout(pool, callers, opts)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@impl DBConnection.Pool
|
||||
def disconnect_all(pool, interval, _opts) do
|
||||
GenServer.call(pool, {:disconnect_all, interval}, :infinity)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@impl DBConnection.Pool
|
||||
def get_connection_metrics(pool) do
|
||||
GenServer.call(pool, :get_connection_metrics, :infinity)
|
||||
end
|
||||
|
||||
## GenServer api
|
||||
|
||||
@impl GenServer
|
||||
def init({mod, opts}) do
|
||||
DBConnection.register_as_pool(mod)
|
||||
|
||||
queue = :ets.new(__MODULE__.Queue, [:protected, :ordered_set, decentralized_counters: true])
|
||||
ts = {System.monotonic_time(), 0}
|
||||
{:ok, _} = DBConnection.ConnectionPool.Pool.start_supervised(queue, mod, opts)
|
||||
target = Keyword.get(opts, :queue_target, @queue_target)
|
||||
interval = Keyword.get(opts, :queue_interval, @queue_interval)
|
||||
idle_interval = Keyword.get(opts, :idle_interval, @idle_interval)
|
||||
idle_limit = Keyword.get_lazy(opts, :idle_limit, fn -> Keyword.get(opts, :pool_size, 1) end)
|
||||
now_in_native = System.monotonic_time()
|
||||
now_in_ms = System.convert_time_unit(now_in_native, :native, @time_unit)
|
||||
|
||||
codel = %{
|
||||
target: target,
|
||||
interval: interval,
|
||||
delay: 0,
|
||||
slow: false,
|
||||
next: now_in_ms,
|
||||
poll: nil,
|
||||
idle_interval: idle_interval,
|
||||
idle_limit: idle_limit,
|
||||
idle: nil
|
||||
}
|
||||
|
||||
codel = start_idle(now_in_native, start_poll(now_in_ms, now_in_ms, codel))
|
||||
{:ok, {:busy, queue, codel, ts}}
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def handle_call(:get_connection_metrics, _from, {status, queue, _, _} = state) do
|
||||
{ready_conn_count, checkout_queue_length} =
|
||||
case status do
|
||||
:busy ->
|
||||
{0, :ets.select_count(queue, [{{{:_, :_, :_}}, [], [true]}])}
|
||||
|
||||
:ready ->
|
||||
{:ets.select_count(queue, [{{{:_, :_}}, [], [true]}]), 0}
|
||||
end
|
||||
|
||||
metrics = %{
|
||||
source: {:pool, self()},
|
||||
ready_conn_count: ready_conn_count,
|
||||
checkout_queue_length: checkout_queue_length
|
||||
}
|
||||
|
||||
{:reply, [metrics], state}
|
||||
end
|
||||
|
||||
def handle_call({:disconnect_all, interval}, _from, {type, queue, codel, _ts}) do
|
||||
ts = {System.monotonic_time(), interval}
|
||||
{:reply, :ok, {type, queue, codel, ts}}
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def handle_info(
|
||||
{:db_connection, from, {:checkout, _caller, now, queue?}},
|
||||
{:busy, queue, _, _} = busy
|
||||
) do
|
||||
case queue? do
|
||||
true ->
|
||||
:ets.insert(queue, {{now, System.unique_integer(), from}})
|
||||
{:noreply, busy}
|
||||
|
||||
false ->
|
||||
message = "connection not available and queuing is disabled"
|
||||
err = DBConnection.ConnectionError.exception(message)
|
||||
Holder.reply_error(from, err)
|
||||
{:noreply, busy}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
{:db_connection, from, {:checkout, _caller, _now, _queue?}} = checkout,
|
||||
{:ready, queue, _codel, _ts} = ready
|
||||
) do
|
||||
case :ets.first(queue) do
|
||||
{queued_in_native, holder} = key ->
|
||||
Holder.handle_checkout(holder, from, queue, queued_in_native) and :ets.delete(queue, key)
|
||||
{:noreply, ready}
|
||||
|
||||
:"$end_of_table" ->
|
||||
handle_info(checkout, put_elem(ready, 0, :busy))
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info({:"ETS-TRANSFER", holder, pid, queue}, {_, queue, _, _} = data) do
|
||||
message = "client #{Util.inspect_pid(pid)} exited"
|
||||
err = DBConnection.ConnectionError.exception(message: message, severity: :info)
|
||||
Holder.handle_disconnect(holder, err)
|
||||
{:noreply, data}
|
||||
end
|
||||
|
||||
def handle_info({:"ETS-TRANSFER", holder, _, {msg, queue, extra}}, {_, queue, _, ts} = data) do
|
||||
case msg do
|
||||
:checkin ->
|
||||
owner = self()
|
||||
|
||||
case :ets.info(holder, :owner) do
|
||||
^owner ->
|
||||
{time, interval} = ts
|
||||
|
||||
if Holder.maybe_disconnect(holder, time, interval) do
|
||||
{:noreply, data}
|
||||
else
|
||||
handle_checkin(holder, extra, data)
|
||||
end
|
||||
|
||||
:undefined ->
|
||||
{:noreply, data}
|
||||
end
|
||||
|
||||
:disconnect ->
|
||||
Holder.handle_disconnect(holder, extra)
|
||||
{:noreply, data}
|
||||
|
||||
:stop ->
|
||||
Holder.handle_stop(holder, extra)
|
||||
{:noreply, data}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info({:timeout, deadline, {queue, holder, pid, len}}, {_, queue, _, _} = data) do
|
||||
# Check that timeout refers to current holder (and not previous)
|
||||
if Holder.handle_deadline(holder, deadline) do
|
||||
message =
|
||||
"client #{Util.inspect_pid(pid)} timed out because " <>
|
||||
"it queued and checked out the connection for longer than #{len}ms"
|
||||
|
||||
exc =
|
||||
case Process.info(pid, :current_stacktrace) do
|
||||
{:current_stacktrace, stacktrace} ->
|
||||
message <>
|
||||
"\n\n#{Util.inspect_pid(pid)} was at location:\n\n" <>
|
||||
Exception.format_stacktrace(stacktrace)
|
||||
|
||||
_ ->
|
||||
message
|
||||
end
|
||||
|> DBConnection.ConnectionError.exception()
|
||||
|
||||
Holder.handle_disconnect(holder, exc)
|
||||
end
|
||||
|
||||
{:noreply, data}
|
||||
end
|
||||
|
||||
def handle_info({:timeout, poll, {time, last_sent}}, {_, _, %{poll: poll}, _} = data) do
|
||||
{status, queue, codel, ts} = data
|
||||
|
||||
# If no queue progress since last poll check queue
|
||||
case :ets.first(queue) do
|
||||
{sent, _, _} when sent <= last_sent and status == :busy ->
|
||||
delay = time - sent
|
||||
timeout(delay, time, queue, start_poll(time, sent, codel), ts)
|
||||
|
||||
{sent, _, _} ->
|
||||
{:noreply, {status, queue, start_poll(time, sent, codel), ts}}
|
||||
|
||||
_ ->
|
||||
{:noreply, {status, queue, start_poll(time, time, codel), ts}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info({:timeout, idle, past_in_native}, {_, _, %{idle: idle}, _} = data) do
|
||||
{status, queue, %{idle_limit: limit} = codel, ts} = data
|
||||
drop_idle(past_in_native, limit, status, queue, codel, ts)
|
||||
end
|
||||
|
||||
defp drop_idle(past_in_native, limit, status, queue, codel, ts) do
|
||||
with true <- status == :ready and limit > 0,
|
||||
{queued_in_native, holder} = key when queued_in_native <= past_in_native <-
|
||||
:ets.first(queue) do
|
||||
:ets.delete(queue, key)
|
||||
Holder.maybe_disconnect(holder, elem(ts, 0), 0) or Holder.handle_ping(holder)
|
||||
drop_idle(past_in_native, limit - 1, status, queue, codel, ts)
|
||||
else
|
||||
_ ->
|
||||
{:noreply, {status, queue, start_idle(System.monotonic_time(), codel), ts}}
|
||||
end
|
||||
end
|
||||
|
||||
defp timeout(delay, time, queue, codel, ts) do
|
||||
case codel do
|
||||
%{delay: min_delay, next: next, target: target, interval: interval}
|
||||
when time >= next and min_delay > target ->
|
||||
codel = %{codel | slow: true, delay: delay, next: time + interval}
|
||||
drop_slow(time, target * 2, queue)
|
||||
{:noreply, {:busy, queue, codel, ts}}
|
||||
|
||||
%{next: next, interval: interval} when time >= next ->
|
||||
codel = %{codel | slow: false, delay: delay, next: time + interval}
|
||||
{:noreply, {:busy, queue, codel, ts}}
|
||||
|
||||
_ ->
|
||||
{:noreply, {:busy, queue, codel, ts}}
|
||||
end
|
||||
end
|
||||
|
||||
defp drop_slow(time, timeout, queue) do
|
||||
min_sent = time - timeout
|
||||
match = {{:"$1", :_, :"$2"}}
|
||||
guards = [{:<, :"$1", min_sent}]
|
||||
select_slow = [{match, guards, [{{:"$1", :"$2"}}]}]
|
||||
|
||||
for {sent, from} <- :ets.select(queue, select_slow) do
|
||||
drop(time - sent, from)
|
||||
end
|
||||
|
||||
:ets.select_delete(queue, [{match, guards, [true]}])
|
||||
end
|
||||
|
||||
defp handle_checkin(holder, now_in_native, {:ready, queue, _, _} = data) do
|
||||
:ets.insert(queue, {{now_in_native, holder}})
|
||||
{:noreply, data}
|
||||
end
|
||||
|
||||
defp handle_checkin(holder, now_in_native, {:busy, queue, codel, ts}) do
|
||||
now_in_ms = System.convert_time_unit(now_in_native, :native, @time_unit)
|
||||
|
||||
case dequeue(now_in_ms, holder, queue, codel, ts) do
|
||||
{:busy, _, _, _} = busy ->
|
||||
{:noreply, busy}
|
||||
|
||||
{:ready, _, _, _} = ready ->
|
||||
:ets.insert(queue, {{now_in_native, holder}})
|
||||
{:noreply, ready}
|
||||
end
|
||||
end
|
||||
|
||||
defp dequeue(time, holder, queue, codel, ts) do
|
||||
case codel do
|
||||
%{next: next, delay: delay, target: target} when time >= next ->
|
||||
dequeue_first(time, delay > target, holder, queue, codel, ts)
|
||||
|
||||
%{slow: false} ->
|
||||
dequeue_fast(time, holder, queue, codel, ts)
|
||||
|
||||
%{slow: true, target: target} ->
|
||||
dequeue_slow(time, target * 2, holder, queue, codel, ts)
|
||||
end
|
||||
end
|
||||
|
||||
defp dequeue_first(time, slow?, holder, queue, codel, ts) do
|
||||
%{interval: interval} = codel
|
||||
next = time + interval
|
||||
|
||||
case :ets.first(queue) do
|
||||
{sent, _, from} = key ->
|
||||
:ets.delete(queue, key)
|
||||
delay = time - sent
|
||||
codel = %{codel | next: next, delay: delay, slow: slow?}
|
||||
go(delay, from, time, holder, queue, codel, ts)
|
||||
|
||||
:"$end_of_table" ->
|
||||
codel = %{codel | next: next, delay: 0, slow: slow?}
|
||||
{:ready, queue, codel, ts}
|
||||
end
|
||||
end
|
||||
|
||||
defp dequeue_fast(time, holder, queue, codel, ts) do
|
||||
case :ets.first(queue) do
|
||||
{sent, _, from} = key ->
|
||||
:ets.delete(queue, key)
|
||||
go(time - sent, from, time, holder, queue, codel, ts)
|
||||
|
||||
:"$end_of_table" ->
|
||||
{:ready, queue, %{codel | delay: 0}, ts}
|
||||
end
|
||||
end
|
||||
|
||||
defp dequeue_slow(time, timeout, holder, queue, codel, ts) do
|
||||
case :ets.first(queue) do
|
||||
{sent, _, from} = key when time - sent > timeout ->
|
||||
:ets.delete(queue, key)
|
||||
drop(time - sent, from)
|
||||
dequeue_slow(time, timeout, holder, queue, codel, ts)
|
||||
|
||||
{sent, _, from} = key ->
|
||||
:ets.delete(queue, key)
|
||||
go(time - sent, from, time, holder, queue, codel, ts)
|
||||
|
||||
:"$end_of_table" ->
|
||||
{:ready, queue, %{codel | delay: 0}, ts}
|
||||
end
|
||||
end
|
||||
|
||||
defp go(delay, from, time, holder, queue, %{delay: min} = codel, ts) do
|
||||
case Holder.handle_checkout(holder, from, queue, 0) do
|
||||
true when delay < min ->
|
||||
{:busy, queue, %{codel | delay: delay}, ts}
|
||||
|
||||
true ->
|
||||
{:busy, queue, codel, ts}
|
||||
|
||||
false ->
|
||||
dequeue(time, holder, queue, codel, ts)
|
||||
end
|
||||
end
|
||||
|
||||
defp drop(delay, from) do
|
||||
message = """
|
||||
[#{ancestor()}] connection not available and request was dropped from queue after #{delay}ms. \
|
||||
This means requests are coming in and your connection pool cannot serve them fast enough. \
|
||||
You can address this by:
|
||||
|
||||
1. Ensuring your database is available and that you can connect to it
|
||||
2. Tracking down slow queries and making sure they are running fast enough
|
||||
3. Increasing the pool_size (although this increases resource consumption)
|
||||
4. Allowing requests to wait longer by increasing :queue_target and :queue_interval
|
||||
|
||||
See DBConnection.start_link/2 for more information
|
||||
"""
|
||||
|
||||
err = DBConnection.ConnectionError.exception(message, :queue_timeout)
|
||||
|
||||
Holder.reply_error(from, err)
|
||||
end
|
||||
|
||||
defp ancestor do
|
||||
Process.get(:"$ancestors", []) |> Enum.find(&is_atom/1)
|
||||
end
|
||||
|
||||
defp start_opts(opts) do
|
||||
Keyword.take(opts, [:name, :spawn_opt])
|
||||
end
|
||||
|
||||
defp start_poll(now, last_sent, %{interval: interval} = codel) do
|
||||
timeout = now + interval
|
||||
poll = :erlang.start_timer(timeout, self(), {timeout, last_sent}, abs: true)
|
||||
%{codel | poll: poll}
|
||||
end
|
||||
|
||||
defp start_idle(now_in_native, %{idle_interval: interval} = codel) do
|
||||
timeout = System.convert_time_unit(now_in_native, :native, :millisecond) + interval
|
||||
idle = :erlang.start_timer(timeout, self(), now_in_native, abs: true)
|
||||
%{codel | idle: idle}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
defmodule DBConnection.ConnectionPool.Pool do
|
||||
@moduledoc false
|
||||
use Supervisor, restart: :temporary
|
||||
|
||||
def start_supervised(tag, mod, opts) do
|
||||
DBConnection.Watcher.watch(
|
||||
DBConnection.ConnectionPool.Supervisor,
|
||||
{DBConnection.ConnectionPool.Pool, {self(), tag, mod, opts}}
|
||||
)
|
||||
end
|
||||
|
||||
def start_link(arg) do
|
||||
Supervisor.start_link(__MODULE__, arg)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init({owner, tag, mod, opts}) do
|
||||
size = Keyword.get(opts, :pool_size, 1)
|
||||
|
||||
if size < 1, do: raise(ArgumentError, "pool size must be greater or equal to 1, got #{size}")
|
||||
|
||||
children = for id <- 1..size, do: conn(owner, tag, id, mod, opts)
|
||||
sup_opts = [strategy: :one_for_one] ++ Keyword.take(opts, [:max_restarts, :max_seconds])
|
||||
Supervisor.init(children, sup_opts)
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp conn(owner, tag, id, mod, opts) do
|
||||
child_opts = [id: {mod, owner, id}] ++ Keyword.take(opts, [:shutdown])
|
||||
DBConnection.Connection.child_spec(mod, [pool_index: id] ++ opts, owner, tag, child_opts)
|
||||
end
|
||||
end
|
||||
448
phoenix/deps/db_connection/lib/db_connection/holder.ex
Normal file
448
phoenix/deps/db_connection/lib/db_connection/holder.ex
Normal file
@@ -0,0 +1,448 @@
|
||||
defmodule DBConnection.Holder do
|
||||
@moduledoc false
|
||||
require Record
|
||||
|
||||
alias DBConnection.Util
|
||||
|
||||
@queue true
|
||||
@timeout 15000
|
||||
@time_unit 1000
|
||||
|
||||
Record.defrecord(:conn, [:connection, :module, :state, :lock, :ts, deadline: nil, status: :ok])
|
||||
Record.defrecord(:pool_ref, [:pool, :reference, :deadline, :holder, :lock])
|
||||
|
||||
@type t :: :ets.tid()
|
||||
@type checkin_time :: non_neg_integer() | nil
|
||||
|
||||
## Holder API
|
||||
|
||||
@spec new(pid, reference, module, term) :: t
|
||||
def new(pool, ref, mod, state) do
|
||||
# Insert before setting heir so that pool can't receive empty table
|
||||
holder = :ets.new(__MODULE__, [:public, :ordered_set, decentralized_counters: true])
|
||||
|
||||
conn = conn(connection: self(), module: mod, state: state, ts: System.monotonic_time())
|
||||
true = :ets.insert_new(holder, conn)
|
||||
|
||||
:ets.setopts(holder, {:heir, pool, ref})
|
||||
holder
|
||||
end
|
||||
|
||||
@spec update(pid, reference, module, term) :: {:ok, t} | :error
|
||||
def update(pool, ref, mod, state) do
|
||||
holder = new(pool, ref, mod, state)
|
||||
|
||||
try do
|
||||
:ets.give_away(holder, pool, {:checkin, ref, System.monotonic_time()})
|
||||
{:ok, holder}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@spec delete(t) :: {module, term}
|
||||
def delete(holder) do
|
||||
[conn(module: module, state: state)] = :ets.lookup(holder, :conn)
|
||||
:ets.delete(holder)
|
||||
{module, state}
|
||||
end
|
||||
|
||||
## Pool API (invoked by caller)
|
||||
|
||||
@callback checkout(pool :: GenServer.server(), [pid], opts :: Keyword.t()) ::
|
||||
{:ok, pool_ref :: any, module, checkin_time, state :: any}
|
||||
| {:error, Exception.t()}
|
||||
def checkout(pool, callers, opts) do
|
||||
queue? = Keyword.get(opts, :queue, @queue)
|
||||
now = System.monotonic_time(@time_unit)
|
||||
timeout = abs_timeout(now, opts)
|
||||
|
||||
case checkout(pool, callers, queue?, now, timeout) do
|
||||
{:ok, _, _, _, _} = ok ->
|
||||
ok
|
||||
|
||||
{:error, %DBConnection.ConnectionError{} = connection_error} = error ->
|
||||
:telemetry.execute(
|
||||
[:db_connection, :connection_error],
|
||||
%{count: 1},
|
||||
%{
|
||||
error: connection_error,
|
||||
opts: opts
|
||||
}
|
||||
)
|
||||
|
||||
error
|
||||
|
||||
{:error, _} = error ->
|
||||
error
|
||||
|
||||
{:redirect, caller, proxy} ->
|
||||
case checkout(proxy, [caller], opts) do
|
||||
{:ok, _, _, _, _} = ok ->
|
||||
ok
|
||||
|
||||
{:error, %DBConnection.ConnectionError{message: message} = exception} ->
|
||||
{:error,
|
||||
%{
|
||||
exception
|
||||
| message:
|
||||
"could not checkout the connection owned by #{Util.inspect_pid(caller)}. " <>
|
||||
"When using the sandbox, connections are shared, so this may imply " <>
|
||||
"another process is using a connection. Reason: #{message}"
|
||||
}}
|
||||
|
||||
{:error, _} = error ->
|
||||
error
|
||||
end
|
||||
|
||||
{:exit, reason} ->
|
||||
exit({reason, {__MODULE__, :checkout, [pool, opts]}})
|
||||
end
|
||||
end
|
||||
|
||||
@spec checkin(pool_ref :: any) :: :ok
|
||||
def checkin(pool_ref) do
|
||||
# Note we may call checkin after a disconnect/stop. For this reason, we choose
|
||||
# to not change the status on checkin but strictly speaking nobody can access
|
||||
# the holder after disconnect/stop unless they store a copy of %DBConnection{}.
|
||||
# Note status can't be :aborted as aborted is always reverted at the end of a
|
||||
# transaction.
|
||||
done(pool_ref, [{conn(:lock) + 1, nil}], :checkin, System.monotonic_time())
|
||||
end
|
||||
|
||||
@spec disconnect(pool_ref :: any, err :: Exception.t()) :: :ok
|
||||
def disconnect(pool_ref, err) do
|
||||
done(pool_ref, [{conn(:status) + 1, :error}], :disconnect, err)
|
||||
end
|
||||
|
||||
@spec stop(pool_ref :: any, err :: Exception.t()) :: :ok
|
||||
def stop(pool_ref, err) do
|
||||
done(pool_ref, [{conn(:status) + 1, :error}], :stop, err)
|
||||
end
|
||||
|
||||
@spec handle(pool_ref :: any, fun :: atom, args :: [term], Keyword.t()) :: tuple
|
||||
def handle(pool_ref, fun, args, opts) do
|
||||
handle_or_cleanup(:handle, pool_ref, fun, args, opts)
|
||||
end
|
||||
|
||||
@spec cleanup(pool_ref :: any, fun :: atom, args :: [term], Keyword.t()) :: tuple
|
||||
def cleanup(pool_ref, fun, args, opts) do
|
||||
handle_or_cleanup(:cleanup, pool_ref, fun, args, opts)
|
||||
end
|
||||
|
||||
defp handle_or_cleanup(type, pool_ref, fun, args, opts) do
|
||||
pool_ref(holder: holder, lock: lock) = pool_ref
|
||||
|
||||
try do
|
||||
:ets.lookup(holder, :conn)
|
||||
rescue
|
||||
ArgumentError ->
|
||||
msg = "connection is closed because of an error, disconnect or timeout"
|
||||
{:disconnect, DBConnection.ConnectionError.exception(msg), _state = :unused}
|
||||
else
|
||||
[conn(lock: conn_lock)] when conn_lock != lock ->
|
||||
raise "an outdated connection has been given to DBConnection on #{fun}/#{length(args) + 2}"
|
||||
|
||||
[conn(status: :error)] ->
|
||||
msg = "connection is closed because of an error, disconnect or timeout"
|
||||
{:disconnect, DBConnection.ConnectionError.exception(msg), _state = :unused}
|
||||
|
||||
[conn(status: :aborted)] when type != :cleanup ->
|
||||
msg = "transaction rolling back"
|
||||
{:disconnect, DBConnection.ConnectionError.exception(msg), _state = :unused}
|
||||
|
||||
[conn(module: module, state: state)] ->
|
||||
holder_apply(holder, module, fun, args ++ [opts, state])
|
||||
end
|
||||
end
|
||||
|
||||
## Pool state helpers API (invoked by callers)
|
||||
|
||||
@spec put_state(pool_ref :: any, term) :: :ok
|
||||
def put_state(pool_ref(holder: sink_holder), state) do
|
||||
:ets.update_element(sink_holder, :conn, [{conn(:state) + 1, state}])
|
||||
:ok
|
||||
end
|
||||
|
||||
@spec status?(pool_ref :: any, :ok | :aborted) :: boolean()
|
||||
def status?(pool_ref(holder: holder), status) do
|
||||
try do
|
||||
:ets.lookup_element(holder, :conn, conn(:status) + 1) == status
|
||||
rescue
|
||||
ArgumentError -> false
|
||||
end
|
||||
end
|
||||
|
||||
@spec put_status(pool_ref :: any, :ok | :aborted) :: boolean()
|
||||
def put_status(pool_ref(holder: holder), status) do
|
||||
try do
|
||||
:ets.update_element(holder, :conn, [{conn(:status) + 1, status}])
|
||||
rescue
|
||||
ArgumentError -> false
|
||||
end
|
||||
end
|
||||
|
||||
## Pool callbacks (invoked by pools)
|
||||
|
||||
@spec reply_redirect({pid, reference}, pid | :shared | :auto, GenServer.server()) :: :ok
|
||||
def reply_redirect(from, caller, redirect) do
|
||||
GenServer.reply(from, {:redirect, caller, redirect})
|
||||
:ok
|
||||
end
|
||||
|
||||
@spec reply_error({pid, reference}, Exception.t()) :: :ok
|
||||
def reply_error(from, exception) do
|
||||
GenServer.reply(from, {:error, exception})
|
||||
:ok
|
||||
end
|
||||
|
||||
@spec handle_checkout(t, {pid, reference}, reference, checkin_time) :: boolean
|
||||
def handle_checkout(holder, {pid, mref}, ref, checkin_time) do
|
||||
:ets.give_away(holder, pid, {mref, ref, checkin_time})
|
||||
rescue
|
||||
ArgumentError ->
|
||||
if Process.alive?(pid) or :ets.info(holder, :owner) != self() do
|
||||
raise ArgumentError, no_holder(holder, pid)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
@spec handle_deadline(t, reference) :: boolean
|
||||
def handle_deadline(holder, deadline) do
|
||||
:ets.lookup_element(holder, :conn, conn(:deadline) + 1)
|
||||
rescue
|
||||
ArgumentError -> false
|
||||
else
|
||||
^deadline -> true
|
||||
_ -> false
|
||||
end
|
||||
|
||||
@spec handle_ping(t) :: true
|
||||
def handle_ping(holder) do
|
||||
:ets.lookup(holder, :conn)
|
||||
rescue
|
||||
ArgumentError ->
|
||||
raise ArgumentError, no_holder(holder, nil)
|
||||
else
|
||||
[conn(connection: conn, state: state)] ->
|
||||
DBConnection.Connection.ping({conn, holder}, state)
|
||||
:ets.delete(holder)
|
||||
true
|
||||
end
|
||||
|
||||
@spec handle_disconnect(t, Exception.t()) :: boolean
|
||||
def handle_disconnect(holder, err) do
|
||||
handle_done(holder, &DBConnection.Connection.disconnect/3, err)
|
||||
end
|
||||
|
||||
@spec handle_stop(t, term) :: boolean
|
||||
def handle_stop(holder, err) do
|
||||
handle_done(holder, &DBConnection.Connection.stop/3, err)
|
||||
end
|
||||
|
||||
@spec maybe_disconnect(t, integer, non_neg_integer) :: boolean()
|
||||
def maybe_disconnect(holder, start, interval_ms) do
|
||||
ts = :ets.lookup_element(holder, :conn, conn(:ts) + 1)
|
||||
|
||||
cond do
|
||||
ts >= start ->
|
||||
false
|
||||
|
||||
interval_ms == 0 ->
|
||||
true
|
||||
|
||||
true ->
|
||||
pid = :ets.lookup_element(holder, :conn, conn(:connection) + 1)
|
||||
System.monotonic_time() > hash_pid(pid, interval_ms) + start
|
||||
end
|
||||
rescue
|
||||
_ -> false
|
||||
else
|
||||
true ->
|
||||
opts = [message: "disconnect_all requested", severity: :debug]
|
||||
handle_disconnect(holder, DBConnection.ConnectionError.exception(opts))
|
||||
|
||||
false ->
|
||||
false
|
||||
end
|
||||
|
||||
## Private
|
||||
|
||||
defp checkout(pool, callers, queue?, start, timeout) do
|
||||
case GenServer.whereis(pool) do
|
||||
pid when node(pid) == node() ->
|
||||
checkout_call(pid, callers, queue?, start, timeout)
|
||||
|
||||
pid when node(pid) != node() ->
|
||||
{:exit, {:badnode, node(pid)}}
|
||||
|
||||
{_name, node} ->
|
||||
{:exit, {:badnode, node}}
|
||||
|
||||
nil ->
|
||||
{:exit, :noproc}
|
||||
end
|
||||
end
|
||||
|
||||
defp checkout_call(pid, callers, queue?, start, timeout) do
|
||||
lock = Process.monitor(pid)
|
||||
send(pid, {:db_connection, {self(), lock}, {:checkout, callers, start, queue?}})
|
||||
|
||||
receive do
|
||||
{:"ETS-TRANSFER", holder, pool, {^lock, ref, checkin_time}} ->
|
||||
Process.demonitor(lock, [:flush])
|
||||
{deadline, ops} = start_deadline(timeout, pool, ref, holder, start)
|
||||
:ets.update_element(holder, :conn, [{conn(:lock) + 1, lock} | ops])
|
||||
|
||||
pool_ref =
|
||||
pool_ref(pool: pool, reference: ref, deadline: deadline, holder: holder, lock: lock)
|
||||
|
||||
checkout_result(holder, pool_ref, checkin_time)
|
||||
|
||||
{^lock, reply} ->
|
||||
Process.demonitor(lock, [:flush])
|
||||
reply
|
||||
|
||||
{:DOWN, ^lock, _, _, reason} ->
|
||||
{:exit, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp checkout_result(holder, pool_ref, checkin_time) do
|
||||
try do
|
||||
:ets.lookup(holder, :conn)
|
||||
rescue
|
||||
ArgumentError ->
|
||||
# Deadline could hit and be handled pool before using connection
|
||||
msg = "connection not available because deadline reached while in queue"
|
||||
{:error, DBConnection.ConnectionError.exception(msg)}
|
||||
else
|
||||
[conn(module: mod, state: state)] ->
|
||||
{:ok, pool_ref, mod, checkin_time, state}
|
||||
end
|
||||
end
|
||||
|
||||
defp no_holder(holder, maybe_pid) do
|
||||
reason =
|
||||
case :ets.info(holder, :owner) do
|
||||
:undefined -> "does not exist"
|
||||
^maybe_pid -> "is being given to its current owner"
|
||||
owner when owner != self() -> "does not belong to the giving process"
|
||||
_ -> "could not be given away"
|
||||
end
|
||||
|
||||
call_reason =
|
||||
if maybe_pid do
|
||||
"Error happened when attempting to transfer to #{Util.inspect_pid(maybe_pid)} " <>
|
||||
"(alive: #{Process.alive?(maybe_pid)})"
|
||||
else
|
||||
"Error happened when looking up connection"
|
||||
end
|
||||
|
||||
"""
|
||||
#{inspect(__MODULE__)} #{inspect(holder)} #{reason}, pool inconsistent.
|
||||
#{call_reason}.
|
||||
|
||||
SELF: #{Util.inspect_pid(self())}
|
||||
ETS INFO: #{inspect(:ets.info(holder))}
|
||||
|
||||
Please report at https://github.com/elixir-ecto/db_connection/issues"
|
||||
"""
|
||||
end
|
||||
|
||||
defp holder_apply(holder, module, fun, args) do
|
||||
try do
|
||||
apply(module, fun, args)
|
||||
catch
|
||||
kind, reason ->
|
||||
{:catch, kind, reason, __STACKTRACE__}
|
||||
else
|
||||
result when is_tuple(result) ->
|
||||
state = :erlang.element(:erlang.tuple_size(result), result)
|
||||
|
||||
try do
|
||||
:ets.update_element(holder, :conn, {conn(:state) + 1, state})
|
||||
result
|
||||
rescue
|
||||
ArgumentError ->
|
||||
augment_disconnect(result)
|
||||
end
|
||||
|
||||
# If it is not a tuple, we just return it as is so we raise bad return.
|
||||
result ->
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
defp augment_disconnect({:disconnect, %DBConnection.ConnectionError{} = err, state}) do
|
||||
%{message: message} = err
|
||||
|
||||
message =
|
||||
message <>
|
||||
" (the connection was closed by the pool, " <>
|
||||
"possibly due to a timeout or because the pool has been terminated)"
|
||||
|
||||
{:disconnect, %{err | message: message}, state}
|
||||
end
|
||||
|
||||
defp augment_disconnect(result), do: result
|
||||
|
||||
defp done(pool_ref, ops, tag, info) do
|
||||
pool_ref(pool: pool, reference: ref, deadline: deadline, holder: holder) = pool_ref
|
||||
cancel_deadline(deadline)
|
||||
|
||||
try do
|
||||
:ets.update_element(holder, :conn, [{conn(:deadline) + 1, nil} | ops])
|
||||
:ets.give_away(holder, pool, {tag, ref, info})
|
||||
rescue
|
||||
ArgumentError -> :ok
|
||||
else
|
||||
true -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_done(holder, stop, err) do
|
||||
:ets.lookup(holder, :conn)
|
||||
rescue
|
||||
ArgumentError ->
|
||||
false
|
||||
else
|
||||
[conn(connection: pid, deadline: deadline, state: state)] ->
|
||||
cancel_deadline(deadline)
|
||||
:ets.delete(holder)
|
||||
stop.({pid, holder}, err, state)
|
||||
true
|
||||
end
|
||||
|
||||
defp abs_timeout(now, opts) do
|
||||
case Keyword.get(opts, :timeout, @timeout) do
|
||||
:infinity -> Keyword.get(opts, :deadline)
|
||||
timeout -> min(now + timeout, Keyword.get(opts, :deadline))
|
||||
end
|
||||
end
|
||||
|
||||
defp start_deadline(nil, _, _, _, _) do
|
||||
{nil, []}
|
||||
end
|
||||
|
||||
defp start_deadline(timeout, pid, ref, holder, start) do
|
||||
deadline =
|
||||
:erlang.start_timer(timeout, pid, {ref, holder, self(), timeout - start}, abs: true)
|
||||
|
||||
{deadline, [{conn(:deadline) + 1, deadline}]}
|
||||
end
|
||||
|
||||
defp cancel_deadline(nil) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp cancel_deadline(deadline) do
|
||||
:erlang.cancel_timer(deadline, async: true, info: false)
|
||||
end
|
||||
|
||||
defp hash_pid(pid, interval_ms) do
|
||||
hash = :erlang.phash2(pid, interval_ms)
|
||||
System.convert_time_unit(hash, :millisecond, :native)
|
||||
end
|
||||
end
|
||||
83
phoenix/deps/db_connection/lib/db_connection/log_entry.ex
Normal file
83
phoenix/deps/db_connection/lib/db_connection/log_entry.ex
Normal file
@@ -0,0 +1,83 @@
|
||||
defmodule DBConnection.LogEntry do
|
||||
@moduledoc """
|
||||
Struct containing log entry information.
|
||||
|
||||
See `t:t/0` for information on the fields.
|
||||
"""
|
||||
|
||||
defstruct [
|
||||
:call,
|
||||
:query,
|
||||
:params,
|
||||
:result,
|
||||
:pool_time,
|
||||
:connection_time,
|
||||
:decode_time,
|
||||
:idle_time
|
||||
]
|
||||
|
||||
@typedoc """
|
||||
Log entry information.
|
||||
|
||||
* `:call` - The `DBConnection` function called
|
||||
* `:query` - The query used by the function
|
||||
* `:params` - The params passed to the function (if any)
|
||||
* `:result` - The result of the call
|
||||
* `:pool_time` - The length of time awaiting a connection from the pool (if
|
||||
the connection was not already checked out)
|
||||
* `:connection_time` - The length of time using the connection (if a
|
||||
connection was used)
|
||||
* `:decode_time` - The length of time decoding the result (if decoded the
|
||||
result using `DBConnection.Query.decode/3`)
|
||||
* `:idle_time` - The amount of time the connection was idle before use
|
||||
|
||||
All times are in the native time units of the VM, see
|
||||
`System.monotonic_time/0`.
|
||||
"""
|
||||
@type t :: %__MODULE__{
|
||||
call: atom,
|
||||
query: any,
|
||||
params: any,
|
||||
result: {:ok, any} | {:ok, any, any} | {:error, Exception.t()},
|
||||
pool_time: non_neg_integer | nil,
|
||||
connection_time: non_neg_integer | nil,
|
||||
idle_time: non_neg_integer | nil,
|
||||
decode_time: non_neg_integer | nil
|
||||
}
|
||||
|
||||
@doc false
|
||||
def new(call, query, params, times, result) do
|
||||
entry = %__MODULE__{call: call, query: query, params: params, result: result}
|
||||
parse_times(times, entry)
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp parse_times([], entry), do: entry
|
||||
|
||||
defp parse_times(times, entry) do
|
||||
stop = :erlang.monotonic_time()
|
||||
{_, entry} = Enum.reduce(times, {stop, entry}, &parse_time/2)
|
||||
entry
|
||||
end
|
||||
|
||||
defp parse_time({:decode, start}, {stop, entry}) do
|
||||
{start, %{entry | decode_time: stop - start}}
|
||||
end
|
||||
|
||||
defp parse_time({:checkout, start}, {stop, entry}) do
|
||||
{start, %{entry | pool_time: stop - start}}
|
||||
end
|
||||
|
||||
defp parse_time({:checkin, start}, {stop, entry}) do
|
||||
# The checkin time was most likely before checkout but it is
|
||||
# not guaranteed as they are tracked by different processes.
|
||||
# There should be no further measurements after checkin.
|
||||
{stop, %{entry | idle_time: max(stop - start, 0)}}
|
||||
end
|
||||
|
||||
defp parse_time({_, start}, {stop, entry}) do
|
||||
%{connection_time: connection_time} = entry
|
||||
{start, %{entry | connection_time: (connection_time || 0) + (stop - start)}}
|
||||
end
|
||||
end
|
||||
155
phoenix/deps/db_connection/lib/db_connection/ownership.ex
Normal file
155
phoenix/deps/db_connection/lib/db_connection/ownership.ex
Normal file
@@ -0,0 +1,155 @@
|
||||
defmodule DBConnection.OwnershipError do
|
||||
@moduledoc """
|
||||
An exception for when errors with ownership occur.
|
||||
"""
|
||||
|
||||
defexception [:message]
|
||||
|
||||
def exception(message), do: %DBConnection.OwnershipError{message: message}
|
||||
end
|
||||
|
||||
defmodule DBConnection.Ownership do
|
||||
@moduledoc """
|
||||
A DBConnection pool that requires explicit checkout and checkin
|
||||
as a mechanism to coordinate between processes.
|
||||
|
||||
## Options
|
||||
|
||||
* `:ownership_mode` - When mode is `:manual`, all connections must
|
||||
be explicitly checked out before by using `ownership_checkout/2`.
|
||||
Otherwise, mode is `:auto` and connections are checked out
|
||||
implicitly. `{:shared, owner}` mode is also supported so
|
||||
processes are allowed on demand. On all cases, checkins are
|
||||
explicit via `ownership_checkin/2`. Defaults to `:auto`.
|
||||
* `:ownership_timeout` - The maximum time (in milliseconds) that a process
|
||||
is allowed to own a connection or `:infinity`, default `120_000`.
|
||||
This timeout exists mostly for sanity checking purposes and can be increased
|
||||
at will, since DBConnection automatically checks in connections whenever
|
||||
there is a mode change.
|
||||
* `:ownership_log` - The `Logger.level` to log ownership changes, or `nil`
|
||||
not to log, default `nil`.
|
||||
|
||||
There are also two experimental options, `:post_checkout` and `:pre_checkin`
|
||||
which allows a developer to configure what happens when a connection is
|
||||
checked out and checked in. Those options are meant to be used during tests,
|
||||
and have the following behaviour:
|
||||
|
||||
* `:post_checkout` - it must be an anonymous function that receives the
|
||||
connection module, the connection state and it must return either
|
||||
`{:ok, connection_module, connection_state}` or
|
||||
`{:disconnect, err, connection_module, connection_state}`. This allows
|
||||
the developer to change the connection module on post checkout. However,
|
||||
in case of disconnects, the return `connection_module` must be the same
|
||||
as the `connection_module` given. Defaults to simply returning the given
|
||||
connection module and state.
|
||||
|
||||
* `:pre_checkin` - it must be an anonymous function that receives the
|
||||
checkin reason (`:checkin`, `{:disconnect, err}` or `{:stop, err}`),
|
||||
the connection module and the connection state returned by `post_checkout`.
|
||||
It must return either `{:ok, connection_module, connection_state}` or
|
||||
`{:disconnect, err, connection_module, connection_state}` where the connection
|
||||
module is the module given to `:post_checkout` Defaults to simply returning
|
||||
the given connection module and state.
|
||||
|
||||
## Callers lookup
|
||||
|
||||
When checking out, the ownership pool first looks if there is a connection
|
||||
assigned to the current process and then checks if there is a connection
|
||||
assigned to any of the processes listed under the `$callers` process
|
||||
dictionary entry. The `$callers` entry is set by default for tasks from
|
||||
Elixir v1.8.
|
||||
|
||||
You can also pass the `:caller` option on checkout with a pid and that
|
||||
pid will be looked up first, instead of `self()`, and then we fall back
|
||||
to `$callers`.
|
||||
"""
|
||||
|
||||
alias DBConnection.Ownership.Manager
|
||||
alias DBConnection.Holder
|
||||
|
||||
@behaviour DBConnection.Pool
|
||||
|
||||
@doc false
|
||||
defdelegate child_spec(args), to: Manager
|
||||
|
||||
@doc false
|
||||
@impl DBConnection.Pool
|
||||
defdelegate disconnect_all(pool, interval, opts), to: Manager
|
||||
|
||||
@doc false
|
||||
@impl DBConnection.Pool
|
||||
def checkout(pool, callers, opts) do
|
||||
case Manager.proxy_for(callers, opts) do
|
||||
{caller, pool} -> Holder.checkout(pool, [caller], opts)
|
||||
nil -> Holder.checkout(pool, callers, opts)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@impl DBConnection.Pool
|
||||
defdelegate get_connection_metrics(pool), to: Manager
|
||||
|
||||
@doc """
|
||||
Explicitly checks a connection out from the ownership manager.
|
||||
|
||||
It may return `:ok` if the connection is checked out.
|
||||
`{:already, :owner | :allowed}` if the caller process already
|
||||
has a connection, or raise if there was an error.
|
||||
"""
|
||||
@spec ownership_checkout(GenServer.server(), Keyword.t()) ::
|
||||
:ok | {:already, :owner | :allowed}
|
||||
def ownership_checkout(manager, opts) do
|
||||
with {:ok, pid} <- Manager.checkout(manager, opts) do
|
||||
case Holder.checkout(pid, [self()], opts) do
|
||||
{:ok, pool_ref, _module, _idle_time, _state} ->
|
||||
Holder.checkin(pool_ref)
|
||||
|
||||
{:error, err} ->
|
||||
raise err
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Changes the ownership mode.
|
||||
|
||||
`mode` may be `:auto`, `:manual` or `{:shared, owner}`.
|
||||
|
||||
The operation will always succeed when setting the mode to
|
||||
`:auto` or `:manual`. It may fail with reason `:not_owner`
|
||||
or `:not_found` when setting `{:shared, pid}` and the
|
||||
given pid does not own any connection. May return
|
||||
`:already_shared` if another process set the ownership
|
||||
mode to `{:shared, _}` and is still alive.
|
||||
"""
|
||||
@spec ownership_mode(GenServer.server(), :auto | :manual | {:shared, pid}, Keyword.t()) ::
|
||||
:ok | :already_shared | :not_owner | :not_found
|
||||
defdelegate ownership_mode(manager, mode, opts), to: Manager, as: :mode
|
||||
|
||||
@doc """
|
||||
Checks a connection back in.
|
||||
|
||||
A connection can only be checked back in by its owner.
|
||||
"""
|
||||
@spec ownership_checkin(GenServer.server(), Keyword.t()) ::
|
||||
:ok | :not_owner | :not_found
|
||||
defdelegate ownership_checkin(manager, opts), to: Manager, as: :checkin
|
||||
|
||||
@doc """
|
||||
Allows the process given by `allow` to use the connection checked out
|
||||
by `owner_or_allowed`.
|
||||
|
||||
It may return `:ok` if the connection is checked out.
|
||||
`{:already, :owner | :allowed}` if the `allow` process already
|
||||
has a connection. `owner_or_allowed` may either be the owner or any
|
||||
other allowed process. Returns `:not_found` if the given process
|
||||
does not have any connection checked out.
|
||||
|
||||
Setting the `unallow_existing` option to `true` will remove the process given by `allow` from
|
||||
any existing allowance it may have (this is necessary because a given process can only be
|
||||
allowed on a single connection at a time).
|
||||
"""
|
||||
@spec ownership_allow(GenServer.server(), owner_or_allowed :: pid, allow :: pid, Keyword.t()) ::
|
||||
:ok | {:already, :owner | :allowed} | :not_found
|
||||
defdelegate ownership_allow(manager, owner, allow, opts), to: Manager, as: :allow
|
||||
end
|
||||
@@ -0,0 +1,439 @@
|
||||
defmodule DBConnection.Ownership.Manager do
|
||||
@moduledoc false
|
||||
use GenServer
|
||||
require Logger
|
||||
alias DBConnection.Ownership.Proxy
|
||||
alias DBConnection.Util
|
||||
|
||||
@timeout 5_000
|
||||
|
||||
@callback start_link({module, opts :: Keyword.t()}) ::
|
||||
GenServer.on_start()
|
||||
def start_link({module, opts}) do
|
||||
{owner_opts, pool_opts} = Keyword.split(opts, [:name])
|
||||
GenServer.start_link(__MODULE__, {module, owner_opts, pool_opts}, owner_opts)
|
||||
end
|
||||
|
||||
@callback disconnect_all(GenServer.server(), non_neg_integer, Keyword.t()) :: :ok
|
||||
def disconnect_all(pool, interval, opts) do
|
||||
inner_pool = GenServer.call(pool, :pool, :infinity)
|
||||
DBConnection.ConnectionPool.disconnect_all(inner_pool, interval, opts)
|
||||
end
|
||||
|
||||
@spec proxy_for(callers :: [pid], Keyword.t()) :: {caller :: pid, proxy :: pid} | nil
|
||||
def proxy_for(callers, opts) do
|
||||
case Keyword.fetch(opts, :name) do
|
||||
{:ok, name} ->
|
||||
Enum.find_value(callers, &List.first(:ets.lookup(name, &1)))
|
||||
|
||||
:error ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec checkout(GenServer.server(), Keyword.t()) ::
|
||||
{:ok, pid} | {:already, :owner | :allowed}
|
||||
def checkout(manager, opts) do
|
||||
GenServer.call(manager, {:checkout, opts}, :infinity)
|
||||
end
|
||||
|
||||
@spec checkin(GenServer.server(), Keyword.t()) ::
|
||||
:ok | :not_owner | :not_found
|
||||
def checkin(manager, opts) do
|
||||
timeout = Keyword.get(opts, :timeout, @timeout)
|
||||
GenServer.call(manager, :checkin, timeout)
|
||||
end
|
||||
|
||||
@spec mode(GenServer.server(), :auto | :manual | {:shared, pid}, Keyword.t()) ::
|
||||
:ok | :already_shared | :not_owner | :not_found
|
||||
def mode(manager, mode, opts)
|
||||
when mode in [:auto, :manual]
|
||||
when elem(mode, 0) == :shared and is_pid(elem(mode, 1)) do
|
||||
timeout = Keyword.get(opts, :timeout, @timeout)
|
||||
GenServer.call(manager, {:mode, mode}, timeout)
|
||||
end
|
||||
|
||||
@spec allow(GenServer.server(), parent :: pid, allow :: pid, Keyword.t()) ::
|
||||
:ok | {:already, :owner | :allowed} | :not_found
|
||||
def allow(manager, parent, allow, opts) do
|
||||
timeout = Keyword.get(opts, :timeout, @timeout)
|
||||
passed_opts = Keyword.take(opts, [:unallow_existing])
|
||||
GenServer.call(manager, {:allow, parent, allow, passed_opts}, timeout)
|
||||
end
|
||||
|
||||
@spec get_connection_metrics(GenServer.server()) ::
|
||||
{:ok, [DBConnection.Pool.connection_metrics()]} | :error
|
||||
def get_connection_metrics(manager) do
|
||||
GenServer.call(manager, :get_connection_metrics, :infinity)
|
||||
end
|
||||
|
||||
## Callbacks
|
||||
|
||||
@impl true
|
||||
def init({module, owner_opts, pool_opts}) do
|
||||
DBConnection.register_as_pool(module)
|
||||
|
||||
ets =
|
||||
case Keyword.fetch(owner_opts, :name) do
|
||||
{:ok, name} when is_atom(name) ->
|
||||
:ets.new(name, [
|
||||
:set,
|
||||
:named_table,
|
||||
:protected,
|
||||
read_concurrency: true,
|
||||
decentralized_counters: true
|
||||
])
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
|
||||
# We can only start the connection pool directly because
|
||||
# neither the pool's GenServer nor the manager trap exits.
|
||||
# Otherwise we would need a supervisor plus a watcher process.
|
||||
pool_opts = Keyword.delete(pool_opts, :pool)
|
||||
{:ok, pool} = DBConnection.start_link(module, pool_opts)
|
||||
|
||||
log = Keyword.get(pool_opts, :ownership_log, nil)
|
||||
mode = Keyword.get(pool_opts, :ownership_mode, :auto)
|
||||
checkout_opts = Keyword.take(pool_opts, [:ownership_timeout, :queue_target, :queue_interval])
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
pool: pool,
|
||||
checkouts: %{},
|
||||
owners: %{},
|
||||
checkout_opts: checkout_opts,
|
||||
mode: mode,
|
||||
mode_ref: nil,
|
||||
ets: ets,
|
||||
log: log
|
||||
}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:get_connection_metrics, _from, %{pool: pool, owners: owners, log: log} = state) do
|
||||
pool_metrics = DBConnection.ConnectionPool.get_connection_metrics(pool)
|
||||
|
||||
proxy_metrics =
|
||||
owners
|
||||
|> Enum.map(fn {_, {proxy, _, _}} ->
|
||||
try do
|
||||
GenServer.call(proxy, :get_connection_metrics)
|
||||
catch
|
||||
:exit, reason ->
|
||||
if log do
|
||||
Logger.log(
|
||||
log,
|
||||
"Caught :exit while calling :get_connection_metrics due to #{inspect(reason)}"
|
||||
)
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
{:reply, pool_metrics ++ proxy_metrics, state}
|
||||
end
|
||||
|
||||
def handle_call(:pool, _from, %{pool: pool} = state) do
|
||||
{:reply, pool, state}
|
||||
end
|
||||
|
||||
def handle_call({:mode, {:shared, shared}}, {caller, _}, %{mode: {:shared, current}} = state) do
|
||||
cond do
|
||||
shared == current ->
|
||||
{:reply, :ok, state}
|
||||
|
||||
Process.alive?(current) ->
|
||||
{:reply, :already_shared, state}
|
||||
|
||||
true ->
|
||||
share_and_reply(state, shared, caller)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call({:mode, {:shared, shared}}, {caller, _}, state) do
|
||||
share_and_reply(state, shared, caller)
|
||||
end
|
||||
|
||||
def handle_call({:mode, mode}, _from, %{mode: mode} = state) do
|
||||
{:reply, :ok, state}
|
||||
end
|
||||
|
||||
def handle_call({:mode, mode}, {caller, _}, state) do
|
||||
state = proxy_checkin_all_except(state, [], caller)
|
||||
{:reply, :ok, %{state | mode: mode, mode_ref: nil}}
|
||||
end
|
||||
|
||||
def handle_call(:checkin, {caller, _}, state) do
|
||||
{reply, state} = proxy_checkin(state, caller, caller)
|
||||
{:reply, reply, state}
|
||||
end
|
||||
|
||||
def handle_call({:allow, caller, allow, opts}, _from, %{checkouts: checkouts} = state) do
|
||||
unallow_existing = Keyword.get(opts, :unallow_existing, false)
|
||||
kind = already_checked_out(checkouts, allow)
|
||||
|
||||
if !unallow_existing && kind do
|
||||
{:reply, {:already, kind}, state}
|
||||
else
|
||||
case Map.get(checkouts, caller, :not_found) do
|
||||
{:owner, ref, proxy} ->
|
||||
state =
|
||||
if unallow_existing, do: owner_unallow(state, caller, allow, ref, proxy), else: state
|
||||
|
||||
{:reply, :ok, owner_allow(state, caller, allow, ref, proxy)}
|
||||
|
||||
{:allowed, ref, proxy} ->
|
||||
state =
|
||||
if unallow_existing, do: owner_unallow(state, caller, allow, ref, proxy), else: state
|
||||
|
||||
{:reply, :ok, owner_allow(state, caller, allow, ref, proxy)}
|
||||
|
||||
:not_found ->
|
||||
{:reply, :not_found, state}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call({:checkout, opts}, {caller, _}, %{checkouts: checkouts} = state) do
|
||||
if kind = already_checked_out(checkouts, caller) do
|
||||
{:reply, {:already, kind}, state}
|
||||
else
|
||||
{proxy, state} = proxy_checkout(state, caller, opts)
|
||||
{:reply, {:ok, proxy}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:db_connection, from, {:checkout, callers, _now, queue?}}, state) do
|
||||
%{checkouts: checkouts, mode: mode, checkout_opts: checkout_opts} = state
|
||||
caller = find_caller(callers, checkouts, mode)
|
||||
|
||||
case Map.get(checkouts, caller, :not_found) do
|
||||
{status, _ref, proxy} when status in [:owner, :allowed] ->
|
||||
DBConnection.Holder.reply_redirect(from, caller, proxy)
|
||||
{:noreply, state}
|
||||
|
||||
:not_found when mode == :auto ->
|
||||
{proxy, state} = proxy_checkout(state, caller, [queue: queue?] ++ checkout_opts)
|
||||
DBConnection.Holder.reply_redirect(from, caller, proxy)
|
||||
{:noreply, state}
|
||||
|
||||
:not_found when mode == :manual ->
|
||||
not_found(from, mode)
|
||||
{:noreply, state}
|
||||
|
||||
:not_found ->
|
||||
{:shared, shared} = mode
|
||||
{:owner, _ref, proxy} = Map.fetch!(checkouts, shared)
|
||||
DBConnection.Holder.reply_redirect(from, shared, proxy)
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info({:DOWN, ref, _, _, _}, state) do
|
||||
{:noreply, state |> owner_down(ref) |> unshare(ref)}
|
||||
end
|
||||
|
||||
def handle_info(_msg, state) do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
defp already_checked_out(checkouts, pid) do
|
||||
case Map.get(checkouts, pid, :not_found) do
|
||||
{:owner, _, _} -> :owner
|
||||
{:allowed, _, _} -> :allowed
|
||||
:not_found -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp proxy_checkout(state, caller, opts) do
|
||||
%{pool: pool, checkouts: checkouts, owners: owners, ets: ets, log: log, mode: mode} = state
|
||||
|
||||
{:ok, proxy} =
|
||||
DynamicSupervisor.start_child(
|
||||
DBConnection.Ownership.Supervisor,
|
||||
{DBConnection.Ownership.Proxy, {caller, pool, opts}}
|
||||
)
|
||||
|
||||
if log do
|
||||
Logger.log(log, fn ->
|
||||
[
|
||||
Util.inspect_pid(caller),
|
||||
" checked out connection in ",
|
||||
inspect(mode),
|
||||
" mode using proxy ",
|
||||
Util.inspect_pid(proxy)
|
||||
]
|
||||
end)
|
||||
end
|
||||
|
||||
ref = Process.monitor(proxy)
|
||||
checkouts = Map.put(checkouts, caller, {:owner, ref, proxy})
|
||||
owners = Map.put(owners, ref, {proxy, caller, []})
|
||||
ets && :ets.insert(ets, {caller, proxy})
|
||||
{proxy, %{state | checkouts: checkouts, owners: owners}}
|
||||
end
|
||||
|
||||
defp proxy_checkin(state, maybe_owner, caller) do
|
||||
case get_and_update_in(state.checkouts, &Map.pop(&1, maybe_owner, :not_found)) do
|
||||
{{:owner, ref, proxy}, state} ->
|
||||
Proxy.stop(proxy, caller)
|
||||
{:ok, state |> owner_down(ref) |> unshare(ref)}
|
||||
|
||||
{{:allowed, _, _}, _} ->
|
||||
{:not_owner, state}
|
||||
|
||||
{:not_found, _} ->
|
||||
{:not_found, state}
|
||||
end
|
||||
end
|
||||
|
||||
defp proxy_checkin_all_except(state, except, caller) do
|
||||
Enum.reduce(state.checkouts, state, fn {pid, _}, state ->
|
||||
if pid in except do
|
||||
state
|
||||
else
|
||||
{_, state} = proxy_checkin(state, pid, caller)
|
||||
state
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp owner_allow(%{ets: ets, log: log} = state, caller, allow, ref, proxy) do
|
||||
if log do
|
||||
Logger.log(log, fn ->
|
||||
[
|
||||
Util.inspect_pid(allow),
|
||||
" was allowed by ",
|
||||
Util.inspect_pid(caller),
|
||||
" on proxy ",
|
||||
Util.inspect_pid(proxy)
|
||||
]
|
||||
end)
|
||||
end
|
||||
|
||||
state = put_in(state.checkouts[allow], {:allowed, ref, proxy})
|
||||
|
||||
state =
|
||||
update_in(state.owners[ref], fn {proxy, caller, allowed} ->
|
||||
{proxy, caller, [allow | List.delete(allowed, allow)]}
|
||||
end)
|
||||
|
||||
ets && :ets.insert(ets, {allow, proxy})
|
||||
state
|
||||
end
|
||||
|
||||
defp owner_unallow(%{ets: ets, log: log} = state, caller, unallow, ref, proxy) do
|
||||
if log do
|
||||
Logger.log(log, fn ->
|
||||
[
|
||||
Util.inspect_pid(unallow),
|
||||
" was unallowed by ",
|
||||
Util.inspect_pid(caller),
|
||||
" on proxy ",
|
||||
Util.inspect_pid(proxy)
|
||||
]
|
||||
end)
|
||||
end
|
||||
|
||||
state = update_in(state.checkouts, &Map.delete(&1, unallow))
|
||||
|
||||
state =
|
||||
update_in(state.owners[ref], fn {proxy, caller, allowed} ->
|
||||
{proxy, caller, List.delete(allowed, unallow)}
|
||||
end)
|
||||
|
||||
ets && :ets.delete(ets, {unallow, proxy})
|
||||
state
|
||||
end
|
||||
|
||||
defp owner_down(%{ets: ets, log: log} = state, ref) do
|
||||
case get_and_update_in(state.owners, &Map.pop(&1, ref)) do
|
||||
{{proxy, caller, allowed}, state} ->
|
||||
Process.demonitor(ref, [:flush])
|
||||
entries = [caller | allowed]
|
||||
|
||||
if log do
|
||||
Logger.log(log, fn ->
|
||||
[
|
||||
Enum.map_join(entries, ", ", &Util.inspect_pid/1),
|
||||
" lost connection from proxy ",
|
||||
Util.inspect_pid(proxy)
|
||||
]
|
||||
end)
|
||||
end
|
||||
|
||||
ets && Enum.each(entries, &:ets.delete(ets, &1))
|
||||
update_in(state.checkouts, &Map.drop(&1, entries))
|
||||
|
||||
{nil, state} ->
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
defp share_and_reply(%{checkouts: checkouts} = state, shared, caller) do
|
||||
case Map.get(checkouts, shared, :not_found) do
|
||||
{:owner, ref, _} ->
|
||||
state = proxy_checkin_all_except(state, [shared], caller)
|
||||
{:reply, :ok, %{state | mode: {:shared, shared}, mode_ref: ref}}
|
||||
|
||||
{:allowed, _, _} ->
|
||||
{:reply, :not_owner, state}
|
||||
|
||||
:not_found ->
|
||||
{:reply, :not_found, state}
|
||||
end
|
||||
end
|
||||
|
||||
defp unshare(%{mode_ref: ref} = state, ref) do
|
||||
%{state | mode: :manual, mode_ref: nil}
|
||||
end
|
||||
|
||||
defp unshare(state, _ref) do
|
||||
state
|
||||
end
|
||||
|
||||
defp find_caller(callers, checkouts, :manual) do
|
||||
Enum.find(callers, &Map.has_key?(checkouts, &1)) || hd(callers)
|
||||
end
|
||||
|
||||
defp find_caller([caller | _], _checkouts, _mode) do
|
||||
caller
|
||||
end
|
||||
|
||||
defp not_found({pid, _} = from, mode) do
|
||||
msg = """
|
||||
cannot find ownership process for #{Util.inspect_pid(pid)}
|
||||
using mode #{inspect(mode)}.
|
||||
|
||||
When using ownership, you must manage connections in one
|
||||
of the four ways:
|
||||
|
||||
* By explicitly checking out a connection
|
||||
* By explicitly allowing a spawned process
|
||||
* By running the pool in shared mode
|
||||
* By using :caller option with allowed process
|
||||
|
||||
The first two options require every new process to explicitly
|
||||
check a connection out or be allowed by calling checkout or
|
||||
allow respectively.
|
||||
|
||||
The third option requires a {:shared, pid} mode to be set.
|
||||
If using shared mode in tests, make sure your tests are not
|
||||
async.
|
||||
|
||||
The fourth option requires [caller: pid] to be used when
|
||||
checking out a connection from the pool. The caller process
|
||||
should already be allowed on a connection.
|
||||
|
||||
If you are reading this error, it means you have not done one
|
||||
of the steps above or that the owner process has crashed.
|
||||
"""
|
||||
|
||||
DBConnection.Holder.reply_error(from, DBConnection.OwnershipError.exception(msg))
|
||||
end
|
||||
end
|
||||
334
phoenix/deps/db_connection/lib/db_connection/ownership/proxy.ex
Normal file
334
phoenix/deps/db_connection/lib/db_connection/ownership/proxy.ex
Normal file
@@ -0,0 +1,334 @@
|
||||
defmodule DBConnection.Ownership.Proxy do
|
||||
@moduledoc false
|
||||
|
||||
alias DBConnection.Holder
|
||||
alias DBConnection.Util
|
||||
use GenServer, restart: :temporary
|
||||
|
||||
@time_unit 1000
|
||||
@ownership_timeout 120_000
|
||||
@queue_target 50
|
||||
@queue_interval 1000
|
||||
|
||||
def start_link({caller, pool, pool_opts}) do
|
||||
GenServer.start_link(__MODULE__, {caller, pool, pool_opts}, [])
|
||||
end
|
||||
|
||||
def stop(proxy, caller) do
|
||||
GenServer.cast(proxy, {:stop, caller})
|
||||
end
|
||||
|
||||
# Callbacks
|
||||
|
||||
@impl true
|
||||
def init({caller, pool, pool_opts}) do
|
||||
pool_opts =
|
||||
pool_opts
|
||||
|> Keyword.put(:timeout, :infinity)
|
||||
|> Keyword.delete(:deadline)
|
||||
|
||||
owner_ref = Process.monitor(caller)
|
||||
ownership_timeout = Keyword.get(pool_opts, :ownership_timeout, @ownership_timeout)
|
||||
timeout = Keyword.get(pool_opts, :queue_target, @queue_target) * 2
|
||||
interval = Keyword.get(pool_opts, :queue_interval, @queue_interval)
|
||||
|
||||
pre_checkin = Keyword.get(pool_opts, :pre_checkin, fn _, mod, state -> {:ok, mod, state} end)
|
||||
post_checkout = Keyword.get(pool_opts, :post_checkout, &{:ok, &1, &2})
|
||||
|
||||
state = %{
|
||||
client: nil,
|
||||
timer: nil,
|
||||
holder: nil,
|
||||
timeout: timeout,
|
||||
interval: interval,
|
||||
poll: nil,
|
||||
owner: {caller, owner_ref},
|
||||
pool: pool,
|
||||
pool_ref: nil,
|
||||
pool_opts: pool_opts,
|
||||
queue: :queue.new(),
|
||||
mod: nil,
|
||||
pre_checkin: pre_checkin,
|
||||
post_checkout: post_checkout,
|
||||
ownership_timer: start_timer(caller, ownership_timeout)
|
||||
}
|
||||
|
||||
now = System.monotonic_time(@time_unit)
|
||||
{:ok, start_poll(now, state)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:DOWN, ref, _, pid, _reason}, %{owner: {_, ref}} = state) do
|
||||
shutdown("owner #{Util.inspect_pid(pid)} exited", state)
|
||||
end
|
||||
|
||||
def handle_info({:timeout, deadline, {_ref, holder, pid, len}}, %{holder: holder} = state) do
|
||||
if Holder.handle_deadline(holder, deadline) do
|
||||
message =
|
||||
"client #{Util.inspect_pid(pid)} timed out because " <>
|
||||
"it queued and checked out the connection for longer than #{len}ms"
|
||||
|
||||
shutdown(message, state)
|
||||
else
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
{:timeout, timer, {__MODULE__, pid, timeout}},
|
||||
%{ownership_timer: timer} = state
|
||||
) do
|
||||
message =
|
||||
"owner #{Util.inspect_pid(pid)} timed out because " <>
|
||||
"it owned the connection for longer than #{timeout}ms (set via the :ownership_timeout option)"
|
||||
|
||||
# We don't invoke shutdown because this is always a disconnect, even if there is no client.
|
||||
# On the other hand, those timeouts are unlikely to trigger, as it defaults to 2 mins.
|
||||
pool_disconnect(DBConnection.ConnectionError.exception(message), false, state)
|
||||
end
|
||||
|
||||
def handle_info({:timeout, poll, time}, %{poll: poll} = state) do
|
||||
state = timeout(time, state)
|
||||
{:noreply, start_poll(time, state)}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
{:db_connection, from, {:checkout, _caller, _now, _queue?}},
|
||||
%{holder: nil} = state
|
||||
) do
|
||||
%{pool: pool, pool_opts: pool_opts, owner: {_, owner_ref}, post_checkout: post_checkout} =
|
||||
state
|
||||
|
||||
case Holder.checkout(pool, [self()], pool_opts) do
|
||||
{:ok, pool_ref, original_mod, _idle_time, conn_state} ->
|
||||
case post_checkout.(original_mod, conn_state) do
|
||||
{:ok, conn_mod, conn_state} ->
|
||||
holder = Holder.new(self(), owner_ref, conn_mod, conn_state)
|
||||
state = %{state | pool_ref: pool_ref, holder: holder, mod: original_mod}
|
||||
checkout(from, state)
|
||||
|
||||
{:disconnect, err, ^original_mod, _conn_state} ->
|
||||
Holder.disconnect(pool_ref, err)
|
||||
Holder.reply_error(from, err)
|
||||
{:stop, {:shutdown, err}, state}
|
||||
end
|
||||
|
||||
{:error, err} ->
|
||||
Holder.reply_error(from, err)
|
||||
{:stop, {:shutdown, err}, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
{:db_connection, from, {:checkout, _caller, _now, _queue?}},
|
||||
%{client: nil} = state
|
||||
) do
|
||||
checkout(from, state)
|
||||
end
|
||||
|
||||
def handle_info({:db_connection, from, {:checkout, _caller, now, queue?}}, state) do
|
||||
if queue? do
|
||||
%{queue: queue} = state
|
||||
queue = :queue.in({now, from}, queue)
|
||||
{:noreply, %{state | queue: queue}}
|
||||
else
|
||||
message = "connection not available and queuing is disabled"
|
||||
err = DBConnection.ConnectionError.exception(message)
|
||||
Holder.reply_error(from, err)
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
{:"ETS-TRANSFER", holder, _, {msg, ref, extra}},
|
||||
%{holder: holder, client: {_, ref, _}} = state
|
||||
) do
|
||||
case msg do
|
||||
:checkin -> checkin(state)
|
||||
:disconnect -> pool_disconnect(extra, true, state)
|
||||
:stop -> pool_stop(extra, state)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info({:"ETS-TRANSFER", holder, pid, ref}, %{holder: holder, owner: {_, ref}} = state) do
|
||||
shutdown("client #{Util.inspect_pid(pid)} exited", state)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:stop, caller}, %{owner: {owner, _}} = state) do
|
||||
message =
|
||||
"#{Util.inspect_pid(caller)} checked in the connection owned by #{Util.inspect_pid(owner)}"
|
||||
|
||||
message =
|
||||
case pruned_stacktrace(caller) do
|
||||
[] ->
|
||||
message
|
||||
|
||||
current_stack ->
|
||||
message <>
|
||||
"\n\n#{Util.inspect_pid(caller)} triggered the checkin at location:\n\n" <>
|
||||
Exception.format_stacktrace(current_stack)
|
||||
end
|
||||
|
||||
shutdown(message, state)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(
|
||||
:get_connection_metrics,
|
||||
_,
|
||||
%{queue: queue, holder: holder, client: client} = state
|
||||
) do
|
||||
connection_metrics = %{
|
||||
source: {:proxy, self()},
|
||||
ready_conn_count:
|
||||
if is_nil(holder) or not is_nil(client) do
|
||||
0
|
||||
else
|
||||
1
|
||||
end,
|
||||
checkout_queue_length: :queue.len(queue)
|
||||
}
|
||||
|
||||
{:reply, connection_metrics, state}
|
||||
end
|
||||
|
||||
defp checkout({pid, ref} = from, %{holder: holder} = state) do
|
||||
if Holder.handle_checkout(holder, from, ref, nil) do
|
||||
{:noreply, %{state | client: {pid, ref, pruned_stacktrace(pid)}}}
|
||||
else
|
||||
next(state)
|
||||
end
|
||||
end
|
||||
|
||||
defp checkin(state) do
|
||||
next(%{state | client: nil})
|
||||
end
|
||||
|
||||
defp next(%{queue: queue} = state) do
|
||||
case :queue.out(queue) do
|
||||
{{:value, {_, from}}, queue} ->
|
||||
checkout(from, %{state | queue: queue})
|
||||
|
||||
{:empty, queue} ->
|
||||
{:noreply, %{state | queue: queue}}
|
||||
end
|
||||
end
|
||||
|
||||
defp start_timer(_, :infinity), do: nil
|
||||
|
||||
defp start_timer(pid, timeout) do
|
||||
:erlang.start_timer(timeout, self(), {__MODULE__, pid, timeout})
|
||||
end
|
||||
|
||||
# If shutting down but it has no client, checkin
|
||||
defp shutdown(reason, %{client: nil} = state) do
|
||||
pool_checkin(reason, state)
|
||||
end
|
||||
|
||||
# If shutting down but it has a client, disconnect
|
||||
defp shutdown(reason, %{client: {client, _, checkout_stack}} = state) do
|
||||
reason =
|
||||
case pruned_stacktrace(client) do
|
||||
[] ->
|
||||
reason
|
||||
|
||||
current_stack ->
|
||||
reason <>
|
||||
"""
|
||||
\n\nClient #{Util.inspect_pid(client)} is still using a connection from owner at location:
|
||||
|
||||
#{Exception.format_stacktrace(current_stack)}
|
||||
The connection itself was checked out by #{Util.inspect_pid(client)} at location:
|
||||
|
||||
#{Exception.format_stacktrace(checkout_stack)}
|
||||
"""
|
||||
end
|
||||
|
||||
err = DBConnection.ConnectionError.exception(reason)
|
||||
pool_disconnect(err, false, state)
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp pool_checkin(reason, state) do
|
||||
checkin = fn pool_ref, _ -> Holder.checkin(pool_ref) end
|
||||
pool_done(reason, state, :checkin, false, checkin, &Holder.disconnect/2)
|
||||
end
|
||||
|
||||
defp pool_disconnect(err, keep_alive?, state) do
|
||||
disconnect = &Holder.disconnect/2
|
||||
pool_done(err, state, {:disconnect, err}, keep_alive?, disconnect, disconnect)
|
||||
end
|
||||
|
||||
defp pool_stop(err, state) do
|
||||
stop = &Holder.stop/2
|
||||
pool_done(err, state, {:stop, err}, false, stop, stop)
|
||||
end
|
||||
|
||||
defp pool_done(err, state, op, keep_alive?, done, stop_or_disconnect) do
|
||||
%{holder: holder, pool_ref: pool_ref, pre_checkin: pre_checkin, mod: original_mod} = state
|
||||
|
||||
if holder do
|
||||
{conn_mod, conn_state} = Holder.delete(holder)
|
||||
|
||||
case pre_checkin.(op, conn_mod, conn_state) do
|
||||
{:ok, ^original_mod, conn_state} ->
|
||||
Holder.put_state(pool_ref, conn_state)
|
||||
done.(pool_ref, err)
|
||||
|
||||
if keep_alive? do
|
||||
{:noreply, %{state | holder: nil}}
|
||||
else
|
||||
{:stop, {:shutdown, err}, state}
|
||||
end
|
||||
|
||||
{:disconnect, err, ^original_mod, conn_state} ->
|
||||
Holder.put_state(pool_ref, conn_state)
|
||||
stop_or_disconnect.(pool_ref, err)
|
||||
{:stop, {:shutdown, err}, state}
|
||||
end
|
||||
else
|
||||
{:stop, {:shutdown, err}, state}
|
||||
end
|
||||
end
|
||||
|
||||
defp start_poll(now, %{interval: interval} = state) do
|
||||
timeout = now + interval
|
||||
poll = :erlang.start_timer(timeout, self(), timeout, abs: true)
|
||||
%{state | poll: poll}
|
||||
end
|
||||
|
||||
defp timeout(time, %{queue: queue, timeout: timeout} = state) do
|
||||
case :queue.out(queue) do
|
||||
{{:value, {sent, from}}, queue} when sent + timeout < time ->
|
||||
drop(time - sent, from)
|
||||
timeout(time, %{state | queue: queue})
|
||||
|
||||
{_, _} ->
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
defp drop(delay, from) do
|
||||
message =
|
||||
"connection not available and request was dropped from queue after #{delay}ms. " <>
|
||||
"You can configure how long requests wait in the queue using :queue_target and " <>
|
||||
":queue_interval. See DBConnection.start_link/2 for more information"
|
||||
|
||||
err = DBConnection.ConnectionError.exception(message, :queue_timeout)
|
||||
Holder.reply_error(from, err)
|
||||
end
|
||||
|
||||
@prune_modules [:gen, GenServer, DBConnection, DBConnection.Holder, DBConnection.Ownership]
|
||||
|
||||
defp pruned_stacktrace(pid) do
|
||||
case Process.info(pid, :current_stacktrace) do
|
||||
{:current_stacktrace, stacktrace} ->
|
||||
Enum.drop_while(stacktrace, &match?({mod, _, _, _} when mod in @prune_modules, &1))
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
19
phoenix/deps/db_connection/lib/db_connection/pool.ex
Normal file
19
phoenix/deps/db_connection/lib/db_connection/pool.ex
Normal file
@@ -0,0 +1,19 @@
|
||||
defmodule DBConnection.Pool do
|
||||
@moduledoc false
|
||||
|
||||
@type pool :: GenServer.server()
|
||||
@type connection_metrics :: %{
|
||||
source: {:pool | :proxy, pid()},
|
||||
ready_conn_count: non_neg_integer(),
|
||||
checkout_queue_length: non_neg_integer()
|
||||
}
|
||||
|
||||
@callback disconnect_all(pool, interval :: term, options :: keyword) :: :ok
|
||||
|
||||
@callback checkout(pool, callers :: [pid], options :: keyword) ::
|
||||
{:ok, pool_ref :: term, module, checkin_time :: non_neg_integer() | nil,
|
||||
state :: term}
|
||||
| {:error, Exception.t()}
|
||||
|
||||
@callback get_connection_metrics(pool :: pool()) :: [connection_metrics()]
|
||||
end
|
||||
57
phoenix/deps/db_connection/lib/db_connection/query.ex
Normal file
57
phoenix/deps/db_connection/lib/db_connection/query.ex
Normal file
@@ -0,0 +1,57 @@
|
||||
defprotocol DBConnection.Query do
|
||||
@moduledoc """
|
||||
The `DBConnection.Query` protocol is responsible for preparing and
|
||||
encoding queries.
|
||||
|
||||
All `DBConnection.Query` functions are executed in the caller process which
|
||||
means it's safe to, for example, raise exceptions or do blocking calls as
|
||||
they won't affect the connection process.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Parse a query.
|
||||
|
||||
This function is called to parse a query term before it is prepared using a
|
||||
connection callback module.
|
||||
|
||||
See `DBConnection.prepare/3`.
|
||||
"""
|
||||
@spec parse(any, Keyword.t()) :: any
|
||||
def parse(query, opts)
|
||||
|
||||
@doc """
|
||||
Describe a query.
|
||||
|
||||
This function is called to describe a query after it is prepared using a
|
||||
connection callback module.
|
||||
|
||||
See `DBConnection.prepare/3`.
|
||||
"""
|
||||
@spec describe(any, Keyword.t()) :: any
|
||||
def describe(query, opts)
|
||||
|
||||
@doc """
|
||||
Encode parameters using a query.
|
||||
|
||||
This function is called to encode a query before it is executed using a
|
||||
connection callback module.
|
||||
|
||||
If this function raises `DBConnection.EncodeError`, then the query is
|
||||
prepared once again.
|
||||
|
||||
See `DBConnection.execute/3`.
|
||||
"""
|
||||
@spec encode(any, any, Keyword.t()) :: any
|
||||
def encode(query, params, opts)
|
||||
|
||||
@doc """
|
||||
Decode a result using a query.
|
||||
|
||||
This function is called to decode a result after it is returned by a
|
||||
connection callback module.
|
||||
|
||||
See `DBConnection.execute/3`.
|
||||
"""
|
||||
@spec decode(any, any, Keyword.t()) :: any
|
||||
def decode(query, result, opts)
|
||||
end
|
||||
47
phoenix/deps/db_connection/lib/db_connection/task.ex
Normal file
47
phoenix/deps/db_connection/lib/db_connection/task.ex
Normal file
@@ -0,0 +1,47 @@
|
||||
defmodule DBConnection.Task do
|
||||
@moduledoc false
|
||||
@name __MODULE__
|
||||
|
||||
require DBConnection.Holder
|
||||
|
||||
def run_child(mod, state, fun, opts) do
|
||||
arg = [fun, self(), opts]
|
||||
{:ok, pid} = Task.Supervisor.start_child(@name, __MODULE__, :init, arg)
|
||||
ref = Process.monitor(pid)
|
||||
_ = DBConnection.Holder.update(pid, ref, mod, state)
|
||||
{pid, ref}
|
||||
end
|
||||
|
||||
def init(fun, parent, opts) do
|
||||
try do
|
||||
Process.link(parent)
|
||||
catch
|
||||
:error, :noproc ->
|
||||
exit({:shutdown, :noproc})
|
||||
end
|
||||
|
||||
receive do
|
||||
{:"ETS-TRANSFER", holder, ^parent, {:checkin, ref, _extra}} ->
|
||||
Process.unlink(parent)
|
||||
pool_ref = DBConnection.Holder.pool_ref(pool: parent, reference: ref, holder: holder)
|
||||
checkout = {:via, __MODULE__, pool_ref}
|
||||
_ = DBConnection.run(checkout, make_fun(fun), [pool: __MODULE__] ++ opts)
|
||||
exit(:normal)
|
||||
end
|
||||
end
|
||||
|
||||
def checkout({:via, __MODULE__, pool_ref}, _callers, _opts) do
|
||||
{:ok, pool_ref, _mod = :unused, _idle_time = nil, _state = :unused}
|
||||
end
|
||||
|
||||
defp make_fun(fun) when is_function(fun, 1) do
|
||||
fun
|
||||
end
|
||||
|
||||
defp make_fun(mfargs) do
|
||||
fn conn ->
|
||||
{mod, fun, args} = mfargs
|
||||
apply(mod, fun, [conn | args])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,116 @@
|
||||
defmodule DBConnection.TelemetryListener do
|
||||
@moduledoc """
|
||||
A connection listener that emits telemetry events for connection and disconnection
|
||||
|
||||
It monitors connection processes and ensures that disconnection events are
|
||||
always emitted.
|
||||
|
||||
## Usage
|
||||
|
||||
Start the listener, and pass it under the `:connection_listeners` option when
|
||||
starting DBConnection:
|
||||
|
||||
{:ok, pid} = DBConnection.TelemetryListener.start_link()
|
||||
{:ok, _conn} = DBConnection.start_link(SomeModule, connection_listeners: [pid])
|
||||
|
||||
# Using a tag, which will be sent in telemetry metadata
|
||||
{:ok, _conn} = DBConnection.start_link(SomeModule, connection_listeners: {[pid], :my_tag})
|
||||
|
||||
# Or, with a Supervisor:
|
||||
Supervisor.start_link([
|
||||
{DBConnection.TelemetryListener, name: MyListener},
|
||||
DBConnection.child_spec(SomeModule, connection_listeners: {[MyListener], :my_tag})
|
||||
])
|
||||
|
||||
When using with Ecto, you can pass the `connection_listeners` option to Ecto, and we
|
||||
recommend passing the repository as the tag. In your supervision tree:
|
||||
|
||||
Supervisor.start_link([
|
||||
{DBConnection.TelemetryListener, name: MyApp.DBListener},
|
||||
{MyApp.Repo, connection_listeners: {[MyApp.DBListener], MyApp.Repo})
|
||||
])
|
||||
|
||||
## Telemetry events
|
||||
|
||||
### Connected
|
||||
|
||||
`[:db_connection, :connected]` - Executed after a connection is established.
|
||||
|
||||
#### Measurements
|
||||
|
||||
* `:count` - Always 1
|
||||
|
||||
#### Metadata
|
||||
|
||||
* `:pid` - The connection pid
|
||||
* `:tag` - The connection pool tag
|
||||
|
||||
### Disconnected
|
||||
|
||||
`[:db_connection, :disconnected]` - Executed after a disconnect.
|
||||
|
||||
#### Measurements
|
||||
|
||||
* `:count` - Always 1
|
||||
|
||||
#### Metadata
|
||||
|
||||
* `:pid` - The connection pid
|
||||
* `:tag` - The connection pool tag
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
|
||||
@doc "Starts a telemetry listener"
|
||||
@spec start_link(GenServer.options()) :: {:ok, pid()}
|
||||
def start_link(opts \\ []) do
|
||||
GenServer.start_link(__MODULE__, nil, opts)
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def init(nil) do
|
||||
{:ok, %{monitoring: %{}}}
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def handle_info({:connected, pid, tag}, state) do
|
||||
handle_connected(pid, tag, state)
|
||||
end
|
||||
|
||||
def handle_info({:connected, pid}, state) do
|
||||
handle_connected(pid, nil, state)
|
||||
end
|
||||
|
||||
def handle_info({:disconnected, pid, _}, state) do
|
||||
handle_disconnected(pid, state)
|
||||
end
|
||||
|
||||
def handle_info({:disconnected, pid}, state) do
|
||||
handle_disconnected(pid, state)
|
||||
end
|
||||
|
||||
def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do
|
||||
handle_disconnected(pid, state)
|
||||
end
|
||||
|
||||
defp handle_connected(pid, tag, state) do
|
||||
:telemetry.execute([:db_connection, :connected], %{count: 1}, %{tag: tag, pid: pid})
|
||||
ref = Process.monitor(pid)
|
||||
|
||||
{:noreply, put_in(state.monitoring[pid], {ref, tag})}
|
||||
end
|
||||
|
||||
defp handle_disconnected(pid, state) do
|
||||
case state.monitoring[pid] do
|
||||
# Already handled. We may receive two messages: one from monitor and one
|
||||
# from listener. For this reason, we need to handle both.
|
||||
nil ->
|
||||
{:noreply, state}
|
||||
|
||||
{ref, tag} ->
|
||||
Process.demonitor(ref, [:flush])
|
||||
:telemetry.execute([:db_connection, :disconnected], %{count: 1}, %{tag: tag, pid: pid})
|
||||
{:noreply, %{state | monitoring: Map.delete(state.monitoring, pid)}}
|
||||
end
|
||||
end
|
||||
end
|
||||
62
phoenix/deps/db_connection/lib/db_connection/util.ex
Normal file
62
phoenix/deps/db_connection/lib/db_connection/util.ex
Normal file
@@ -0,0 +1,62 @@
|
||||
defmodule DBConnection.Util do
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
Inspect a pid, including the process label if possible.
|
||||
"""
|
||||
def inspect_pid(pid) when is_pid(pid) do
|
||||
with :undefined <- get_label(pid),
|
||||
:undefined <- get_name(pid),
|
||||
:undefined <- get_initial_call(pid) do
|
||||
inspect(pid)
|
||||
else
|
||||
label_or_name_or_call -> "#{inspect(pid)} (#{inspect(label_or_name_or_call)})"
|
||||
end
|
||||
end
|
||||
|
||||
def inspect_pid(other), do: inspect(other)
|
||||
|
||||
defp get_name(pid) do
|
||||
try do
|
||||
Process.info(pid, :registered_name)
|
||||
rescue
|
||||
_ -> :undefined
|
||||
else
|
||||
{:registered_name, name} when is_atom(name) -> name
|
||||
_ -> :undefined
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Set a process label if `Process.set_label/1` is available.
|
||||
"""
|
||||
def set_label(label) do
|
||||
if function_exported?(Process, :set_label, 1) do
|
||||
Process.set_label(label)
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
# Get a process label if `:proc_lib.get_label/1` is available.
|
||||
defp get_label(pid) do
|
||||
if function_exported?(:proc_lib, :get_label, 1) do
|
||||
# Avoid a compiler warning if the function isn't
|
||||
# defined in your version of Erlang/OTP
|
||||
apply(:proc_lib, :get_label, [pid])
|
||||
else
|
||||
# mimic return value of
|
||||
# `:proc_lib.get_label/1` when none is set.
|
||||
# Don't resort to using `Process.info(pid, :dictionary)`,
|
||||
# as this is not efficient.
|
||||
:undefined
|
||||
end
|
||||
end
|
||||
|
||||
defp get_initial_call(pid) do
|
||||
case Process.info(pid, :initial_call) do
|
||||
{:initial_call, {mod, _, _}} -> mod
|
||||
_ -> :undefined
|
||||
end
|
||||
end
|
||||
end
|
||||
65
phoenix/deps/db_connection/lib/db_connection/watcher.ex
Normal file
65
phoenix/deps/db_connection/lib/db_connection/watcher.ex
Normal file
@@ -0,0 +1,65 @@
|
||||
defmodule DBConnection.Watcher do
|
||||
@moduledoc false
|
||||
@name __MODULE__
|
||||
|
||||
use GenServer
|
||||
|
||||
def start_link(_) do
|
||||
GenServer.start_link(__MODULE__, :ok, name: @name)
|
||||
end
|
||||
|
||||
def watch(supervisor, args) do
|
||||
GenServer.call(@name, {:watch, supervisor, args}, :infinity)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(:ok) do
|
||||
Process.flag(:trap_exit, true)
|
||||
{:ok, {%{}, %{}}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:watch, supervisor, args}, {caller_pid, _ref}, {caller_refs, started_refs}) do
|
||||
case DynamicSupervisor.start_child(supervisor, args) do
|
||||
{:ok, started_pid} ->
|
||||
Process.link(caller_pid)
|
||||
caller_ref = Process.monitor(caller_pid)
|
||||
started_ref = Process.monitor(started_pid)
|
||||
caller_refs = Map.put(caller_refs, caller_ref, {supervisor, started_pid, started_ref})
|
||||
started_refs = Map.put(started_refs, started_ref, {caller_pid, caller_ref})
|
||||
{:reply, {:ok, started_pid}, {caller_refs, started_refs}}
|
||||
|
||||
other ->
|
||||
{:reply, other, {caller_refs, started_refs}}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:DOWN, ref, _, _, _}, {caller_refs, started_refs}) do
|
||||
case caller_refs do
|
||||
%{^ref => {supervisor, started_pid, started_ref}} ->
|
||||
Process.demonitor(started_ref, [:flush])
|
||||
DynamicSupervisor.terminate_child(supervisor, started_pid)
|
||||
{:noreply, {Map.delete(caller_refs, ref), Map.delete(started_refs, started_ref)}}
|
||||
|
||||
%{} ->
|
||||
%{^ref => {caller_pid, caller_ref}} = started_refs
|
||||
Process.demonitor(caller_ref, [:flush])
|
||||
Process.exit(caller_pid, :kill)
|
||||
{:noreply, {Map.delete(caller_refs, caller_ref), Map.delete(started_refs, ref)}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info({:EXIT, _, _}, state) do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def terminate(_, {_, started_refs}) do
|
||||
for {_, {caller_pid, _}} <- started_refs do
|
||||
Process.exit(caller_pid, :kill)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user