228 lines
6.1 KiB
Elixir
228 lines
6.1 KiB
Elixir
defmodule Ecto.Repo.Supervisor do
|
|
@moduledoc false
|
|
use Supervisor
|
|
require Logger
|
|
|
|
@defaults [timeout: 15000, pool_size: 10]
|
|
@integer_url_query_params ["timeout", "pool_size", "idle_interval"]
|
|
|
|
@doc """
|
|
Starts the repo supervisor.
|
|
"""
|
|
def start_link(repo, otp_app, adapter, opts) do
|
|
name = Keyword.get(opts, :name, repo)
|
|
sup_opts = if name, do: [name: name], else: []
|
|
Supervisor.start_link(__MODULE__, {name, repo, otp_app, adapter, opts}, sup_opts)
|
|
end
|
|
|
|
@doc """
|
|
Retrieves the runtime configuration.
|
|
"""
|
|
def init_config(type, repo, otp_app, opts) do
|
|
config = Application.get_env(otp_app, repo, [])
|
|
config = [otp_app: otp_app] ++ (@defaults |> Keyword.merge(config) |> Keyword.merge(opts))
|
|
config = Keyword.put_new_lazy(config, :telemetry_prefix, fn -> telemetry_prefix(repo) end)
|
|
|
|
case repo_init(type, repo, config) do
|
|
{:ok, config} ->
|
|
{url, config} = Keyword.pop(config, :url)
|
|
url_config = parse_url(url || "")
|
|
|
|
url_config =
|
|
if is_list(config[:ssl]) and url_config[:ssl] == true do
|
|
Logger.warning(
|
|
"ignoring `ssl=true` parameter in URL because `ssl` is already set in the configuration: #{inspect(config[:ssl])}"
|
|
)
|
|
|
|
Keyword.delete(url_config, :ssl)
|
|
else
|
|
url_config
|
|
end
|
|
|
|
{:ok, Keyword.merge(config, url_config)}
|
|
|
|
:ignore ->
|
|
:ignore
|
|
end
|
|
end
|
|
|
|
defp telemetry_prefix(repo) do
|
|
repo
|
|
|> Module.split()
|
|
|> Enum.map(&(&1 |> Macro.underscore() |> String.to_atom()))
|
|
end
|
|
|
|
defp repo_init(type, repo, config) do
|
|
if Code.ensure_loaded?(repo) and function_exported?(repo, :init, 2) do
|
|
repo.init(type, config)
|
|
else
|
|
{:ok, config}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Retrieves the compile time configuration.
|
|
"""
|
|
def compile_config(_repo, opts) do
|
|
otp_app = Keyword.fetch!(opts, :otp_app)
|
|
adapter = opts[:adapter]
|
|
|
|
unless adapter do
|
|
raise ArgumentError, "missing :adapter option on use Ecto.Repo"
|
|
end
|
|
|
|
if Code.ensure_compiled(adapter) != {:module, adapter} do
|
|
raise ArgumentError,
|
|
"adapter #{inspect(adapter)} was not compiled, " <>
|
|
"ensure it is correct and it is included as a project dependency"
|
|
end
|
|
|
|
behaviours =
|
|
for {:behaviour, behaviours} <- adapter.__info__(:attributes),
|
|
behaviour <- behaviours,
|
|
do: behaviour
|
|
|
|
unless Ecto.Adapter in behaviours do
|
|
raise ArgumentError,
|
|
"expected :adapter option given to Ecto.Repo to list Ecto.Adapter as a behaviour"
|
|
end
|
|
|
|
{otp_app, adapter, behaviours}
|
|
end
|
|
|
|
@doc """
|
|
Parses an Ecto URL allowed in configuration.
|
|
|
|
The format must be:
|
|
|
|
"ecto://username:password@hostname:port/database?ssl=true&timeout=1000"
|
|
|
|
"""
|
|
def parse_url(""), do: []
|
|
|
|
def parse_url(url) when is_binary(url) do
|
|
info = URI.parse(url)
|
|
|
|
if is_nil(info.host) do
|
|
raise Ecto.InvalidURLError, url: url, message: "host is not present"
|
|
end
|
|
|
|
if is_nil(info.path) or not (info.path =~ ~r"^/([^/])+$") do
|
|
raise Ecto.InvalidURLError, url: url, message: "path should be a database name"
|
|
end
|
|
|
|
destructure [username, password], info.userinfo && String.split(info.userinfo, ":")
|
|
"/" <> database = info.path
|
|
|
|
url_opts = [
|
|
scheme: info.scheme,
|
|
username: username,
|
|
password: password,
|
|
database: database,
|
|
port: info.port
|
|
]
|
|
|
|
url_opts = put_hostname_if_present(url_opts, info.host)
|
|
query_opts = parse_uri_query(info)
|
|
|
|
for {k, v} <- url_opts ++ query_opts,
|
|
not is_nil(v),
|
|
do: {k, if(is_binary(v), do: URI.decode(v), else: v)}
|
|
end
|
|
|
|
defp put_hostname_if_present(keyword, "") do
|
|
keyword
|
|
end
|
|
|
|
defp put_hostname_if_present(keyword, hostname) when is_binary(hostname) do
|
|
Keyword.put(keyword, :hostname, hostname)
|
|
end
|
|
|
|
defp parse_uri_query(%URI{query: nil}),
|
|
do: []
|
|
|
|
defp parse_uri_query(%URI{query: query} = url) do
|
|
query
|
|
|> URI.query_decoder()
|
|
|> Enum.reduce([], fn
|
|
{"ssl", "true"}, acc ->
|
|
[{:ssl, true}] ++ acc
|
|
|
|
{"ssl", "false"}, acc ->
|
|
[{:ssl, false}] ++ acc
|
|
|
|
{key, value}, acc when key in @integer_url_query_params ->
|
|
[{String.to_atom(key), parse_integer!(key, value, url)}] ++ acc
|
|
|
|
{key, value}, acc ->
|
|
[{String.to_atom(key), value}] ++ acc
|
|
end)
|
|
end
|
|
|
|
defp parse_integer!(key, value, url) do
|
|
case Integer.parse(value) do
|
|
{int, ""} ->
|
|
int
|
|
|
|
_ ->
|
|
raise Ecto.InvalidURLError,
|
|
url: url,
|
|
message: "cannot parse value `#{value}` for parameter `#{key}` as an integer"
|
|
end
|
|
end
|
|
|
|
@doc false
|
|
def tuplet(name, opts) do
|
|
adapter_meta = Ecto.Repo.Registry.lookup(name)
|
|
|
|
if opts[:stacktrace] || Map.get(adapter_meta, :stacktrace) do
|
|
{:current_stacktrace, stacktrace} = :erlang.process_info(self(), :current_stacktrace)
|
|
{adapter_meta, Keyword.put(opts, :stacktrace, stacktrace)}
|
|
else
|
|
{adapter_meta, opts}
|
|
end
|
|
end
|
|
|
|
## Callbacks
|
|
|
|
@doc false
|
|
def init({name, repo, otp_app, adapter, opts}) do
|
|
case init_config(:supervisor, repo, otp_app, opts) do
|
|
{:ok, opts} ->
|
|
:telemetry.execute(
|
|
[:ecto, :repo, :init],
|
|
%{system_time: System.system_time()},
|
|
%{repo: repo, opts: opts}
|
|
)
|
|
|
|
{:ok, child, meta} = adapter.init([repo: repo] ++ opts)
|
|
|
|
# Normalize name to atom, ignore via/global names
|
|
name = if is_atom(name), do: name, else: nil
|
|
cache = Ecto.Query.Planner.new_query_cache(name)
|
|
meta = Map.merge(meta, %{repo: repo, cache: cache})
|
|
child_spec = wrap_child_spec(child, [name, adapter, meta])
|
|
Supervisor.init([child_spec], strategy: :one_for_one, max_restarts: 0)
|
|
|
|
:ignore ->
|
|
:ignore
|
|
end
|
|
end
|
|
|
|
def start_child({mod, fun, args}, name, adapter, meta) do
|
|
case apply(mod, fun, args) do
|
|
{:ok, pid} ->
|
|
meta = Map.merge(meta, %{pid: pid, adapter: adapter})
|
|
Ecto.Repo.Registry.associate(self(), name, meta)
|
|
{:ok, pid}
|
|
|
|
other ->
|
|
other
|
|
end
|
|
end
|
|
|
|
defp wrap_child_spec(%{start: start} = spec, args) do
|
|
%{spec | start: {__MODULE__, :start_child, [start | args]}}
|
|
end
|
|
end
|