Current section

Files

Jump to
phoenix_session_process lib phoenix session_process.ex
Raw

lib/phoenix/session_process.ex

defmodule Phoenix.SessionProcess do
@moduledoc """
Documentation for `Phoenix.SessionProcess`.
Add superviser to process tree
[
...
{Phoenix.SessionProcess.Supervisor, []}
]
Add this after the `:fetch_session` plug to generate a unique session ID.
plug :fetch_session
plug Phoenix.SessionProcess.SessionId
Start a session process with a session ID.
Phoenix.SessionProcess.start("session_id")
This will start a session process using the module defined with
config :phoenix_session_process, session_process: MySessionProcess
Or you can start a session process with a specific module.
Phoenix.SessionProcess.start("session_id", MySessionProcess)
# or
Phoenix.SessionProcess.start("session_id", MySessionProcess, arg)
Check if a session process is started.
Phoenix.SessionProcess.started?("session_id")
Terminate a session process.
Phoenix.SessionProcess.terminate("session_id")
Genserver call on a session process.
Phoenix.SessionProcess.call("session_id", request)
Genserver cast on a session process.
Phoenix.SessionProcess.cast("session_id", request)
List all session processes.
Phoenix.SessionProcess.list_session()
"""
@spec start(binary()) :: {:ok, pid()} | {:error, term()}
defdelegate start(session_id), to: Phoenix.SessionProcess.ProcessSupervisor, as: :start_session
@doc """
Start a session process with a specific module.
## Examples
iex> result = Phoenix.SessionProcess.start("valid_session", Phoenix.SessionProcess.DefaultSessionProcess)
iex> match?({:ok, _pid}, result) or match?({:error, {:already_started, _pid}}, result)
true
iex> Phoenix.SessionProcess.start("invalid@session", Phoenix.SessionProcess.DefaultSessionProcess)
{:error, {:invalid_session_id, "invalid@session"}}
"""
@spec start(binary(), atom()) :: {:ok, pid()} | {:error, term()}
defdelegate start(session_id, module),
to: Phoenix.SessionProcess.ProcessSupervisor,
as: :start_session
@doc """
Start a session process with a specific module and initialization arguments.
## Examples
iex> result = Phoenix.SessionProcess.start("valid_session_with_args", Phoenix.SessionProcess.DefaultSessionProcess, %{user_id: 123})
iex> match?({:ok, _pid}, result) or match?({:error, {:already_started, _pid}}, result)
true
iex> result = Phoenix.SessionProcess.start("valid_session_with_list", Phoenix.SessionProcess.DefaultSessionProcess, [debug: true])
iex> match?({:ok, _pid}, result) or match?({:error, {:already_started, _pid}}, result)
true
"""
@spec start(binary(), atom(), any()) :: {:ok, pid()} | {:error, term()}
defdelegate start(session_id, module, arg),
to: Phoenix.SessionProcess.ProcessSupervisor,
as: :start_session
@spec started?(binary()) :: boolean()
defdelegate started?(session_id),
to: Phoenix.SessionProcess.ProcessSupervisor,
as: :session_process_started?
@spec terminate(binary()) :: :ok | {:error, :not_found}
defdelegate terminate(session_id),
to: Phoenix.SessionProcess.ProcessSupervisor,
as: :terminate_session
@spec call(binary(), any(), :infinity | non_neg_integer()) :: {:ok, any()} | {:error, term()}
defdelegate call(session_id, request, timeout \\ 15_000),
to: Phoenix.SessionProcess.ProcessSupervisor,
as: :call_on_session
@spec cast(binary(), any()) :: :ok | {:error, term()}
defdelegate cast(session_id, request),
to: Phoenix.SessionProcess.ProcessSupervisor,
as: :cast_on_session
@spec list_session() :: [{binary(), pid()}, ...]
def list_session() do
Registry.select(Phoenix.SessionProcess.Registry, [
{{:":$1", :":$2", :_}, [], [{{:":$1", :":$2"}}]}
])
end
@doc """
Get session information including count and modules.
"""
@spec session_info() :: %{count: integer(), modules: list(module())}
def session_info() do
sessions = list_session()
modules =
sessions
|> Enum.map(fn {_session_id, pid} ->
case Registry.lookup(Phoenix.SessionProcess.Registry, pid) do
[{_, module}] -> module
_ -> Phoenix.SessionProcess.Config.session_process()
end
end)
|> Enum.uniq()
%{
count: length(sessions),
modules: modules
}
end
@doc """
Get all session IDs for a specific module.
"""
@spec list_sessions_by_module(module()) :: [binary()]
def list_sessions_by_module(module) do
Registry.select(Phoenix.SessionProcess.Registry, [
{{:"$1", :"$2", :"$_"}, [], [{{:"$1", :"$2", :"$_"}}]}
])
|> Enum.filter(fn {_session_id, _pid, mod} -> mod == module end)
|> Enum.map(fn {session_id, _pid, _mod} -> session_id end)
end
@doc """
Check if a session exists and return its PID if it does.
"""
@spec find_session(binary()) :: {:ok, pid()} | {:error, :not_found}
def find_session(session_id) do
case Phoenix.SessionProcess.ProcessSupervisor.session_process_pid(session_id) do
nil -> {:error, :not_found}
pid -> {:ok, pid}
end
end
@doc """
Get session statistics including process count and memory usage.
"""
@spec session_stats() :: %{
total_sessions: integer(),
memory_usage: integer(),
avg_memory_per_session: integer()
}
def session_stats() do
sessions = list_session()
total_sessions = length(sessions)
memory_usage =
if total_sessions > 0 do
sessions
|> Enum.map(fn {_session_id, pid} ->
case :erlang.process_info(pid, :memory) do
{:memory, memory} -> memory
_ -> 0
end
end)
|> Enum.sum()
else
0
end
avg_memory = if total_sessions > 0, do: div(memory_usage, total_sessions), else: 0
%{
total_sessions: total_sessions,
memory_usage: memory_usage,
avg_memory_per_session: avg_memory
}
end
defmacro __using__(:process) do
quote do
use GenServer
def start_link(opts) do
arg = Keyword.get(opts, :arg, %{})
name = Keyword.get(opts, :name)
GenServer.start_link(__MODULE__, arg, name: name)
end
def get_session_id() do
current_pid = self()
Registry.select(Phoenix.SessionProcess.Registry, [
{{:":$1", :":$2", :_}, [{:==, :":$2", current_pid}], [{{:":$1", :":$2"}}]}
])
|> Enum.at(0)
|> elem(0)
end
end
end
defmacro __using__(:process_link) do
quote do
use GenServer
def start_link(opts) do
args = Keyword.get(opts, :args, %{})
name = Keyword.get(opts, :name)
GenServer.start_link(__MODULE__, args, name: name)
end
def get_session_id() do
current_pid = self()
Registry.select(Phoenix.SessionProcess.Registry, [
{{:":$1", :":$2", :_}, [{:==, :":$2", current_pid}], [{{:":$1", :":$2"}}]}
])
|> Enum.at(0)
|> elem(0)
end
def handle_cast({:monitor, pid}, state) do
new_state =
state |> Map.update(:__live_view__, [pid], fn views -> [pid | views] end)
Process.monitor(pid)
{:noreply, new_state}
end
def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do
new_state =
state
|> Map.update(:__live_view__, [], fn views -> views |> Enum.filter(&(&1 != pid)) end)
{:noreply, new_state}
end
def terminate(_reason, state) do
state
|> Map.get(:__live_view__, [])
|> Enum.each(&Process.send_after(&1, :session_expired, 0))
end
end
end
end