11defmodule Phoenix.SessionProcess do
22 @ moduledoc """
3- Documentation for ` Phoenix.SessionProcess` .
3+ Main API for managing isolated session processes in Phoenix applications .
44
5- Add superviser to process tree
5+ This module provides a high-level interface for creating, managing, and communicating
6+ with dedicated GenServer processes for each user session. Each session runs in its own
7+ isolated process, enabling real-time session state management without external dependencies.
68
7- [
8- ...
9- {Phoenix.SessionProcess.Supervisor, []}
10- ]
9+ ## Features
1110
12- Add this after the `:fetch_session` plug to generate a unique session ID.
11+ - **Session Isolation**: Each user session runs in a dedicated GenServer process
12+ - **Automatic Cleanup**: TTL-based session expiration and garbage collection
13+ - **LiveView Integration**: Built-in support for monitoring LiveView processes
14+ - **High Performance**: 10,000+ sessions/second creation rate
15+ - **Zero Dependencies**: No Redis, databases, or external services required
16+ - **Comprehensive Telemetry**: Built-in observability for all operations
1317
14- plug :fetch_session
15- plug Phoenix.SessionProcess.SessionId
18+ ## Quick Start
1619
17- Start a session process with a session ID.
20+ ### 1. Add to Supervision Tree
1821
19- Phoenix.SessionProcess.start("session_id")
22+ Add the supervisor to your application's supervision tree in `lib/my_app/application.ex`:
2023
21- This will start a session process using the module defined with
24+ def start(_type, _args) do
25+ children = [
26+ # ... other children ...
27+ {Phoenix.SessionProcess.Supervisor, []}
28+ ]
2229
23- config :phoenix_session_process, session_process: MySessionProcess
30+ Supervisor.start_link(children, strategy: :one_for_one)
31+ end
32+
33+ ### 2. Configure Session ID Generation
34+
35+ Add the SessionId plug after `:fetch_session` in your router:
36+
37+ pipeline :browser do
38+ plug :accepts, ["html"]
39+ plug :fetch_session
40+ plug Phoenix.SessionProcess.SessionId # Add this
41+ # ... other plugs ...
42+ end
43+
44+ ### 3. Use in Controllers and LiveViews
45+
46+ defmodule MyAppWeb.PageController do
47+ use MyAppWeb, :controller
48+
49+ def index(conn, _params) do
50+ session_id = conn.assigns.session_id
51+
52+ # Start session process
53+ {:ok, _pid} = Phoenix.SessionProcess.start(session_id)
54+
55+ # Store data
56+ Phoenix.SessionProcess.cast(session_id, {:put, :user_id, 123})
57+
58+ # Retrieve data
59+ {:ok, state} = Phoenix.SessionProcess.call(session_id, :get_state)
60+
61+ render(conn, "index.html", state: state)
62+ end
63+ end
64+
65+ ## Configuration
66+
67+ Configure the library in `config/config.exs`:
68+
69+ config :phoenix_session_process,
70+ session_process: MyApp.SessionProcess, # Default session module
71+ max_sessions: 10_000, # Maximum concurrent sessions
72+ session_ttl: 3_600_000, # Session TTL (1 hour)
73+ rate_limit: 100 # Sessions per minute
74+
75+ ## Creating Custom Session Processes
2476
25- Or you can start a session process with a specific module.
77+ ### Basic Session Process
2678
27- Phoenix.SessionProcess.start("session_id", MySessionProcess)
28- # or
29- Phoenix.SessionProcess.start("session_id", MySessionProcess, arg)
79+ defmodule MyApp.SessionProcess do
80+ use Phoenix.SessionProcess, :process
3081
31- Check if a session process is started.
82+ @impl true
83+ def init(_init_arg) do
84+ {:ok, %{user_id: nil, cart: [], preferences: %{}}}
85+ end
3286
33- Phoenix.SessionProcess.started?("session_id")
87+ @impl true
88+ def handle_call(:get_user, _from, state) do
89+ {:reply, state.user_id, state}
90+ end
3491
35- Terminate a session process.
92+ @impl true
93+ def handle_cast({:set_user, user_id}, state) do
94+ {:noreply, %{state | user_id: user_id}}
95+ end
96+ end
3697
37- Phoenix.SessionProcess.terminate("session_id")
98+ ### With LiveView Integration
3899
39- Genserver call on a session process.
100+ defmodule MyApp.SessionProcessWithLiveView do
101+ use Phoenix.SessionProcess, :process_link
40102
41- Phoenix.SessionProcess.call("session_id", request)
103+ @impl true
104+ def init(_init_arg) do
105+ {:ok, %{user: nil, live_views: []}}
106+ end
42107
43- Genserver cast on a session process.
108+ # Automatically monitors LiveView processes
109+ # Sends :session_expired message when session terminates
110+ end
44111
45- Phoenix.SessionProcess.cast("session_id", request)
112+ ## API Overview
46113
47- List all session processes.
114+ ### Session Management
115+ - `start/1`, `start/2`, `start/3` - Start session processes
116+ - `started?/1` - Check if session exists
117+ - `terminate/1` - Stop session process
118+ - `find_session/1` - Find session by ID
48119
49- Phoenix.SessionProcess.list_session()
120+ ### Communication
121+ - `call/2`, `call/3` - Synchronous requests
122+ - `cast/2` - Asynchronous messages
123+
124+ ### Inspection
125+ - `list_session/0` - List all sessions
126+ - `session_info/0` - Get session count and modules
127+ - `session_stats/0` - Get memory and performance stats
128+ - `list_sessions_by_module/1` - Filter sessions by module
129+
130+ ## Error Handling
131+
132+ All operations return structured error tuples:
133+
134+ {:error, {:invalid_session_id, session_id}}
135+ {:error, {:session_limit_reached, max_sessions}}
136+ {:error, {:session_not_found, session_id}}
137+ {:error, {:timeout, timeout}}
138+
139+ Use `Phoenix.SessionProcess.Error.message/1` for human-readable errors.
140+
141+ ## Performance
142+
143+ Expected performance metrics:
144+ - Session Creation: 10,000+ sessions/sec
145+ - Memory Usage: ~10KB per session
146+ - Registry Lookups: 100,000+ lookups/sec
147+
148+ See the benchmarking guide at `bench/README.md` for details.
50149 """
51150
151+ @ doc """
152+ Starts a session process using the default configured module.
153+
154+ The session process is registered in the Registry and scheduled for automatic
155+ cleanup based on the configured TTL.
156+
157+ ## Parameters
158+ - `session_id` - Unique binary identifier for the session
159+
160+ ## Returns
161+ - `{:ok, pid}` - Session process started successfully
162+ - `{:error, {:already_started, pid}}` - Session already exists
163+ - `{:error, {:invalid_session_id, id}}` - Invalid session ID format
164+ - `{:error, {:session_limit_reached, max}}` - Maximum sessions exceeded
165+
166+ ## Examples
167+
168+ {:ok, pid} = Phoenix.SessionProcess.start("user_123")
169+ {:error, {:already_started, pid}} = Phoenix.SessionProcess.start("user_123")
170+ """
52171 @ spec start ( binary ( ) ) :: { :ok , pid ( ) } | { :error , term ( ) }
53172 defdelegate start ( session_id ) , to: Phoenix.SessionProcess.ProcessSupervisor , as: :start_session
54173
55174 @ doc """
56- Start a session process with a specific module.
175+ Starts a session process using a custom module.
176+
177+ This allows you to use a specific session process implementation instead of
178+ the default configured module.
179+
180+ ## Parameters
181+ - `session_id` - Unique binary identifier for the session
182+ - `module` - Module implementing the session process behavior
183+
184+ ## Returns
185+ - `{:ok, pid}` - Session process started successfully
186+ - `{:error, {:already_started, pid}}` - Session already exists
187+ - `{:error, {:invalid_session_id, id}}` - Invalid session ID format
188+ - `{:error, {:session_limit_reached, max}}` - Maximum sessions exceeded
57189
58190 ## Examples
59191
192+ {:ok, pid} = Phoenix.SessionProcess.start("user_123", MyApp.CustomSessionProcess)
193+
60194 iex> result = Phoenix.SessionProcess.start("valid_session", Phoenix.SessionProcess.DefaultSessionProcess)
61195 iex> match?({:ok, _pid}, result) or match?({:error, {:already_started, _pid}}, result)
62196 true
@@ -70,10 +204,30 @@ defmodule Phoenix.SessionProcess do
70204 as: :start_session
71205
72206 @ doc """
73- Start a session process with a specific module and initialization arguments.
207+ Starts a session process with a custom module and initialization arguments.
208+
209+ The initialization arguments are passed to the module's `init/1` callback,
210+ allowing you to set up initial state or configuration.
211+
212+ ## Parameters
213+ - `session_id` - Unique binary identifier for the session
214+ - `module` - Module implementing the session process behavior
215+ - `arg` - Initialization argument(s) passed to `init/1`
216+
217+ ## Returns
218+ - `{:ok, pid}` - Session process started successfully
219+ - `{:error, {:already_started, pid}}` - Session already exists
220+ - `{:error, {:invalid_session_id, id}}` - Invalid session ID format
221+ - `{:error, {:session_limit_reached, max}}` - Maximum sessions exceeded
74222
75223 ## Examples
76224
225+ # With map argument
226+ {:ok, pid} = Phoenix.SessionProcess.start("user_123", MyApp.SessionProcess, %{user_id: 123})
227+
228+ # With keyword list
229+ {:ok, pid} = Phoenix.SessionProcess.start("user_456", MyApp.SessionProcess, [debug: true])
230+
77231 iex> result = Phoenix.SessionProcess.start("valid_session_with_args", Phoenix.SessionProcess.DefaultSessionProcess, %{user_id: 123})
78232 iex> match?({:ok, _pid}, result) or match?({:error, {:already_started, _pid}}, result)
79233 true
@@ -87,21 +241,98 @@ defmodule Phoenix.SessionProcess do
87241 to: Phoenix.SessionProcess.ProcessSupervisor ,
88242 as: :start_session
89243
244+ @ doc """
245+ Checks if a session process is currently running.
246+
247+ ## Parameters
248+ - `session_id` - Unique binary identifier for the session
249+
250+ ## Returns
251+ - `true` - Session process exists and is running
252+ - `false` - Session process does not exist
253+
254+ ## Examples
255+
256+ {:ok, _pid} = Phoenix.SessionProcess.start("user_123")
257+ true = Phoenix.SessionProcess.started?("user_123")
258+ false = Phoenix.SessionProcess.started?("nonexistent")
259+ """
90260 @ spec started? ( binary ( ) ) :: boolean ( )
91261 defdelegate started? ( session_id ) ,
92262 to: Phoenix.SessionProcess.ProcessSupervisor ,
93263 as: :session_process_started?
94264
265+ @ doc """
266+ Terminates a session process.
267+
268+ This gracefully shuts down the session process and removes it from the Registry.
269+ Emits telemetry events for session stop.
270+
271+ ## Parameters
272+ - `session_id` - Unique binary identifier for the session
273+
274+ ## Returns
275+ - `:ok` - Session terminated successfully
276+ - `{:error, :not_found}` - Session does not exist
277+
278+ ## Examples
279+
280+ {:ok, _pid} = Phoenix.SessionProcess.start("user_123")
281+ :ok = Phoenix.SessionProcess.terminate("user_123")
282+ {:error, :not_found} = Phoenix.SessionProcess.terminate("user_123")
283+ """
95284 @ spec terminate ( binary ( ) ) :: :ok | { :error , :not_found }
96285 defdelegate terminate ( session_id ) ,
97286 to: Phoenix.SessionProcess.ProcessSupervisor ,
98287 as: :terminate_session
99288
100- @ spec call ( binary ( ) , any ( ) , :infinity | non_neg_integer ( ) ) :: { :ok , any ( ) } | { :error , term ( ) }
289+ @ doc """
290+ Makes a synchronous call to a session process.
291+
292+ Sends a synchronous request to the session process and waits for a response.
293+ The request is handled by the session process's `handle_call/3` callback.
294+
295+ ## Parameters
296+ - `session_id` - Unique binary identifier for the session
297+ - `request` - The request message to send
298+ - `timeout` - Maximum time to wait for response in milliseconds (default: 15,000)
299+
300+ ## Returns
301+ - Response from the session process's `handle_call/3` callback
302+ - `{:error, {:session_not_found, id}}` - Session does not exist
303+ - `{:error, {:timeout, timeout}}` - Request timed out
304+
305+ ## Examples
306+
307+ {:ok, _pid} = Phoenix.SessionProcess.start("user_123")
308+ {:ok, state} = Phoenix.SessionProcess.call("user_123", :get_state)
309+ {:ok, :pong} = Phoenix.SessionProcess.call("user_123", :ping, 5_000)
310+ """
311+ @ spec call ( binary ( ) , any ( ) , :infinity | non_neg_integer ( ) ) :: any ( )
101312 defdelegate call ( session_id , request , timeout \\ 15_000 ) ,
102313 to: Phoenix.SessionProcess.ProcessSupervisor ,
103314 as: :call_on_session
104315
316+ @ doc """
317+ Sends an asynchronous message to a session process.
318+
319+ Sends a fire-and-forget message to the session process. The message is handled
320+ by the session process's `handle_cast/2` callback. Does not wait for a response.
321+
322+ ## Parameters
323+ - `session_id` - Unique binary identifier for the session
324+ - `request` - The message to send
325+
326+ ## Returns
327+ - `:ok` - Message sent successfully
328+ - `{:error, {:session_not_found, id}}` - Session does not exist
329+
330+ ## Examples
331+
332+ {:ok, _pid} = Phoenix.SessionProcess.start("user_123")
333+ :ok = Phoenix.SessionProcess.cast("user_123", {:put, :user_id, 123})
334+ :ok = Phoenix.SessionProcess.cast("user_123", {:delete, :old_key})
335+ """
105336 @ spec cast ( binary ( ) , any ( ) ) :: :ok | { :error , term ( ) }
106337 defdelegate cast ( session_id , request ) ,
107338 to: Phoenix.SessionProcess.ProcessSupervisor ,
0 commit comments