improvement(Server): listeners can be dynamically added.
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing

This commit is contained in:
James Harton 2024-08-27 16:34:37 +12:00
parent fad27f142e
commit 9e22c1cca8
Signed by: james
GPG key ID: 90E82DAA13F624F4
3 changed files with 98 additions and 14 deletions

View file

@ -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 ->

6
test/support/dynamic.ex Normal file
View file

@ -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

View file

@ -0,0 +1,41 @@
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