From f28e4e428147c5be8ecc273366bd222cb1ae98c6 Mon Sep 17 00:00:00 2001 From: James Harton Date: Tue, 27 Aug 2024 16:34:37 +1200 Subject: [PATCH] improvement(Server): listeners can be dynamically added. --- lib/wayfarer/server.ex | 65 +++++++++++++++++++++------ test/support/dynamic.ex | 6 +++ test/wayfarer/server/dynamic_test.exs | 59 ++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 14 deletions(-) create mode 100644 test/support/dynamic.ex create mode 100644 test/wayfarer/server/dynamic_test.exs diff --git a/lib/wayfarer/server.ex b/lib/wayfarer/server.ex index 55ebf37..9640fce 100644 --- a/lib/wayfarer/server.ex +++ b/lib/wayfarer/server.ex @@ -98,7 +98,12 @@ defmodule Wayfarer.Server do |> Keyword.merge(opts) |> Keyword.put(:module, __MODULE__) - Server.child_spec(opts) + default = %{ + id: __MODULE__, + start: {Wayfarer.Server, :start_link, [opts]} + } + + Supervisor.child_spec(default, []) end @doc false @@ -116,6 +121,24 @@ defmodule Wayfarer.Server do end end + @type listener_options :: unquote(Options.option_typespec(Dsl.Listener.schema())) + + @doc """ + Add a listener to an already running server. + + If the listener fails to start for any reason, then this function will return + an error, otherwise it will block until the listener is ready to accept + requests. + + """ + @spec add_listener(module, listener_options) :: :ok | {:error, any} + def add_listener(module, options) do + with {:ok, options} <- Options.validate(options, Dsl.Listener.schema()) do + {:via, Registry, {Wayfarer.Server.Registry, module}} + |> GenServer.call({:add_listener, options}) + end + end + @doc false @spec target_status_change( {module, :http | :https, IP.Address.t(), :socket.port_number(), @@ -177,26 +200,40 @@ defmodule Wayfarer.Server do {:noreply, state} end + @doc false + @impl true + @spec handle_call(any, GenServer.from(), map) :: {:reply, any, map} + def handle_call({:add_listener, listener}, _from, state) do + {:reply, start_listener(listener, state), state} + end + defp start_listeners(listeners, state) do listeners |> Enum.reduce_while({:ok, state}, fn listener, success -> - listener = Keyword.put(listener, :module, state.module) - - case DynamicSupervisor.start_child(Listener.DynamicSupervisor, {Listener, listener}) do - {:ok, pid} -> - Process.link(pid) - {:cont, success} - - {:error, {:already_started, pid}} -> - Process.link(pid) - {:cont, success} - - {:error, reason} -> - {:halt, {:error, reason}} + case start_listener(listener, state) do + {:ok, _} -> {:cont, success} + {:error, reason} -> {:halt, {:error, reason}} end end) end + defp start_listener(listener, state) do + listener = Keyword.put(listener, :module, state.module) + + case DynamicSupervisor.start_child(Listener.DynamicSupervisor, {Listener, listener}) do + {:ok, pid} -> + Process.link(pid) + {:ok, pid} + + {:error, {:already_started, pid}} -> + Process.link(pid) + {:ok, pid} + + {:error, reason} -> + {:error, reason} + end + end + defp start_targets(targets, state) do targets |> Enum.reduce_while({:ok, state}, fn target, success -> diff --git a/test/support/dynamic.ex b/test/support/dynamic.ex new file mode 100644 index 0000000..b63a623 --- /dev/null +++ b/test/support/dynamic.ex @@ -0,0 +1,6 @@ +defmodule Support.Dynamic do + @moduledoc """ + An empty server for testing dynamic proxy configuration. + """ + use Wayfarer.Server, targets: [], listeners: [], routing_table: [] +end diff --git a/test/wayfarer/server/dynamic_test.exs b/test/wayfarer/server/dynamic_test.exs new file mode 100644 index 0000000..f73fe15 --- /dev/null +++ b/test/wayfarer/server/dynamic_test.exs @@ -0,0 +1,59 @@ +defmodule Wayfarer.Server.DynamicTest do + use ExUnit.Case, async: true + + alias Wayfarer.{Listener, Server, Target} + use Support.PortTracker + use Support.HttpRequest + import IP.Sigil + + defmodule DynamicServer1 do + use Wayfarer.Server + end + + defmodule DynamicServer2 do + use Wayfarer.Server + end + + setup do + start_supervised!(Server.Supervisor) + start_supervised!(Listener.Supervisor) + start_supervised!(Target.Supervisor) + start_supervised!(DynamicServer1) + start_supervised!(DynamicServer2) + + :ok + end + + describe "Server.add_listener/2" do + test "a listener can be dynamically added to a server" do + port = random_port() + + assert {:ok, _pid} = + Server.add_listener(DynamicServer1, + scheme: :http, + address: ~i"127.0.0.1", + port: port + ) + + assert {:ok, %{status: 502}} = request(:http, ~i"127.0.0.1", port, host: "www.example.com") + end + + test "the same listener cannot be added to two servers" do + port = random_port() + + assert {:ok, _pid} = + Server.add_listener(DynamicServer1, + scheme: :http, + address: ~i"127.0.0.1", + port: port + ) + + assert {:error, _} = + Server.add_listener(DynamicServer2, + scheme: :http, + address: ~i"127.0.0.1", + port: port + ) + end + end +end