Current section

Files

Jump to
membrane_rtc_engine_sip lib sip port_allocator.ex
Raw

lib/sip/port_allocator.ex

defmodule Membrane.RTC.Engine.Endpoint.SIP.PortAllocator do
@moduledoc false
use GenServer
require Logger
@default_port_range {21_000, 21_100}
@spec start_link(term()) :: GenServer.on_start()
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@spec get_port() :: {:ok, pos_integer()} | {:error, :no_available_port}
def get_port() do
GenServer.call(__MODULE__, :get_port)
end
@spec free_ports(pid()) :: :ok
def free_ports(port_owner) do
GenServer.cast(__MODULE__, {:free_ports, port_owner})
end
@impl true
def init(_opts) do
{from, to} = Application.get_env(:membrane_rtc_engine_sip, :port_range, @default_port_range)
state = %{
available: from..to |> Enum.to_list() |> :queue.from_list(),
in_use: %{}
}
{:ok, state}
end
@impl true
def handle_call(:get_port, {pid, _tag} = from, state) do
case :queue.out(state.available) do
{{:value, port}, available} ->
state = %{state | available: available}
case :gen_udp.open(port) do
{:ok, socket} ->
:ok = :gen_udp.close(socket)
state = %{state | in_use: Map.update(state.in_use, pid, [port], &[port | &1])}
{:reply, {:ok, port}, state}
{:error, reason} ->
Logger.warning("Opening port #{port} failed with reason: #{reason}")
handle_call(:get_port, from, state)
end
{:empty, _available} ->
{:reply, {:error, :no_available_port}, state}
end
end
@impl true
def handle_cast({:free_ports, pid}, state) do
{ports, in_use} = Map.pop(state.in_use, pid, [])
available =
Enum.reduce(ports, state.available, fn elem, acc ->
:queue.in(elem, acc)
end)
state = %{state | available: available, in_use: in_use}
{:noreply, state}
end
end