From 43bba8a449cf7a0c5e1698f8ee3b6b6c30cbe0fc Mon Sep 17 00:00:00 2001 From: Brendan Ball Date: Thu, 16 Dec 2021 14:39:28 +0100 Subject: [PATCH 1/2] implement a configurable superivision tree Currently Kadabra starts an application which is problematic in some failure scenarios that can result in the entire BEAM node being restarted. Starting an application in a library doesn't put the library user in control. This fixes it in a backwards compatible way by allowing the library user to decide exactly how their application should behave when Kadabra crashes. --- lib/application.ex | 7 +------ lib/connection_supervisor.ex | 24 ++++++++++++++++++++++++ lib/kadabra.ex | 17 ++++++++++++----- lib/supervisor.ex | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 lib/connection_supervisor.ex create mode 100644 lib/supervisor.ex diff --git a/lib/application.ex b/lib/application.ex index 6599e6a..2ea41d9 100644 --- a/lib/application.ex +++ b/lib/application.ex @@ -4,11 +4,6 @@ defmodule Kadabra.Application do use Application def start(_type, _args) do - children = [ - {Registry, keys: :unique, name: Registry.Kadabra}, - {Task.Supervisor, name: Kadabra.Tasks} - ] - - Supervisor.start_link(children, strategy: :one_for_one, name: :kadabra) + Kadabra.Supervisor.start_link(name: :kadabra) end end diff --git a/lib/connection_supervisor.ex b/lib/connection_supervisor.ex new file mode 100644 index 0000000..7635a5e --- /dev/null +++ b/lib/connection_supervisor.ex @@ -0,0 +1,24 @@ +defmodule Kadabra.ConnectionSupervisor do + @moduledoc false + + use DynamicSupervisor + + def start_link(args) do + instance_name = Keyword.fetch!(args, :instance_name) + DynamicSupervisor.start_link(__MODULE__, nil, name: name(instance_name)) + end + + @impl true + def init(_init_arg) do + DynamicSupervisor.init(strategy: :one_for_one) + end + + def start_child(instance_name, opts) do + spec = {Kadabra.ConnectionPool, opts} + DynamicSupervisor.start_child(name(instance_name), spec) + end + + defp name(instance_name) do + :"Kadabra.ConnectionSupervisor.#{instance_name}" + end +end diff --git a/lib/kadabra.ex b/lib/kadabra.ex index 10b4558..f3e3ba2 100644 --- a/lib/kadabra.ex +++ b/lib/kadabra.ex @@ -92,6 +92,8 @@ defmodule Kadabra do @type uri :: charlist | String.t() + @type instance_name :: atom() + @doc ~S""" Opens a new connection. @@ -109,17 +111,22 @@ defmodule Kadabra do def open(uri, opts \\ []) def open(uri, opts) when is_binary(uri) do + open(:kadabra, uri, opts) + end + + def open(uri, opts) when is_list(uri) do + uri |> to_string() |> open(opts) + end + + @spec open(instance_name, uri, conn_opts) :: {:ok, pid} | {:error, term} + def open(instance_name, uri, opts) when is_binary(uri) do pool_opts = [ uri: URI.parse(uri), pid: self(), opts: opts ] - Supervisor.start_child(:kadabra, {Kadabra.ConnectionPool, pool_opts}) - end - - def open(uri, opts) when is_list(uri) do - uri |> to_string() |> open(opts) + Kadabra.ConnectionSupervisor.start_child(instance_name, pool_opts) end @doc ~S""" diff --git a/lib/supervisor.ex b/lib/supervisor.ex new file mode 100644 index 0000000..cafff38 --- /dev/null +++ b/lib/supervisor.ex @@ -0,0 +1,36 @@ +defmodule Kadabra.Supervisor do + @moduledoc ~S""" + Supervisor to be used by the library user to add it to the desired supervision tree. + This allows the library user to be more in control of how failures are managed. + When using this, you probably don't want the Kadabra Application to be started. + You can accomplish this by specifying in your dependency: + + ```elixir + {:kadabra, app: false} + ``` + + You can add this to your supervision tree with the following spec: + + ```elixir + {Kadabra.Supervisor, name: :custom_kadabra_supervisor} + ``` + """ + + use Supervisor + + def start_link(args) do + name = Keyword.fetch!(args, :name) + Supervisor.start_link(__MODULE__, name, name: :"Kadabra.Supervisor.#{name}") + end + + @impl true + def init(name) do + children = [ + {Registry, keys: :unique, name: Registry.Kadabra}, + {Task.Supervisor, name: Kadabra.Tasks}, + {Kadabra.ConnectionSupervisor, instance_name: name} + ] + + Supervisor.init(children, strategy: :one_for_one) + end +end From 779d323d8b046225068838fedfbea7b33d36204d Mon Sep 17 00:00:00 2001 From: Brendan Ball Date: Tue, 11 Jan 2022 07:58:27 +0100 Subject: [PATCH 2/2] remove Application It's not possible to include the Application in a release without starting it automatically. --- lib/application.ex | 9 --------- mix.exs | 3 +-- 2 files changed, 1 insertion(+), 11 deletions(-) delete mode 100644 lib/application.ex diff --git a/lib/application.ex b/lib/application.ex deleted file mode 100644 index 2ea41d9..0000000 --- a/lib/application.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Kadabra.Application do - @moduledoc false - - use Application - - def start(_type, _args) do - Kadabra.Supervisor.start_link(name: :kadabra) - end -end diff --git a/mix.exs b/mix.exs index dbf7cb8..4f30c0b 100644 --- a/mix.exs +++ b/mix.exs @@ -31,8 +31,7 @@ defmodule Kadabra.Mixfile do def application do [ - extra_applications: [:logger, :ssl], - mod: {Kadabra.Application, []} + extra_applications: [:logger, :ssl] ] end