|
40 | 40 | start_link/2, |
41 | 41 | start_link/3, |
42 | 42 | stop/1, |
43 | | - stats/1 |
| 43 | + stats/1, |
| 44 | + handoff/2, |
| 45 | + handoff/3 |
44 | 46 | ]). |
45 | 47 |
|
46 | 48 | %% Internal exports |
@@ -89,6 +91,8 @@ start_link(Id, Mode) -> |
89 | 91 | %% - max_connections: Maximum connections per context (default: 100) |
90 | 92 | %% - app_module: Python module containing ASGI/WSGI app |
91 | 93 | %% - app_callable: Python callable name (e.g., "app", "application") |
| 94 | +%% - setup_code: Binary Python code to execute after context creation |
| 95 | +%% (useful for setting up protocol factory) |
92 | 96 | %% |
93 | 97 | %% @param Id Unique identifier for this context |
94 | 98 | %% @param Mode Context mode (auto, subinterp, worker) |
@@ -141,6 +145,33 @@ stats(Ctx) when is_pid(Ctx) -> |
141 | 145 | {error, timeout} |
142 | 146 | end. |
143 | 147 |
|
| 148 | +%% @doc Hand off a file descriptor to this reactor context. |
| 149 | +%% |
| 150 | +%% The context takes ownership of the FD and will handle I/O events. |
| 151 | +%% This is the main entry point for handing off accepted connections. |
| 152 | +%% |
| 153 | +%% @param Fd The raw file descriptor (from inet:getfd/1) |
| 154 | +%% @param ClientInfo Map with connection metadata (addr, port, type, etc.) |
| 155 | +-spec handoff(integer(), map()) -> ok | {error, term()}. |
| 156 | +handoff(Fd, ClientInfo) when is_integer(Fd), is_map(ClientInfo) -> |
| 157 | + %% Get a reactor context from the pool or use default |
| 158 | + case whereis(py_reactor_context_default) of |
| 159 | + undefined -> |
| 160 | + {error, no_reactor_context}; |
| 161 | + Ctx -> |
| 162 | + handoff(Ctx, Fd, ClientInfo) |
| 163 | + end. |
| 164 | + |
| 165 | +%% @doc Hand off a file descriptor to a specific reactor context. |
| 166 | +%% |
| 167 | +%% @param Ctx The reactor context pid |
| 168 | +%% @param Fd The raw file descriptor (from inet:getfd/1) |
| 169 | +%% @param ClientInfo Map with connection metadata |
| 170 | +-spec handoff(pid(), integer(), map()) -> ok | {error, term()}. |
| 171 | +handoff(Ctx, Fd, ClientInfo) when is_pid(Ctx), is_integer(Fd), is_map(ClientInfo) -> |
| 172 | + Ctx ! {fd_handoff, Fd, ClientInfo}, |
| 173 | + ok. |
| 174 | + |
144 | 175 | %% ============================================================================ |
145 | 176 | %% Process loop |
146 | 177 | %% ============================================================================ |
@@ -169,6 +200,19 @@ init(Parent, Id, Mode, Opts) -> |
169 | 200 | AppModule = maps:get(app_module, Opts, undefined), |
170 | 201 | AppCallable = maps:get(app_callable, Opts, undefined), |
171 | 202 |
|
| 203 | + %% Execute setup code if specified (e.g., set protocol factory) |
| 204 | + SetupCode = maps:get(setup_code, Opts, undefined), |
| 205 | + case SetupCode of |
| 206 | + undefined -> ok; |
| 207 | + _ when is_binary(SetupCode) -> |
| 208 | + case py_nif:context_exec(Ref, SetupCode) of |
| 209 | + ok -> ok; |
| 210 | + {error, Reason} -> |
| 211 | + error_logger:error_msg( |
| 212 | + "py_reactor_context setup_code failed: ~p~n", [Reason]) |
| 213 | + end |
| 214 | + end, |
| 215 | + |
172 | 216 | %% Initialize app in Python context if specified |
173 | 217 | case AppModule of |
174 | 218 | undefined -> ok; |
@@ -214,6 +258,15 @@ loop(State) -> |
214 | 258 | {select, FdRes, _Ref, ready_output} -> |
215 | 259 | handle_write_ready(FdRes, State); |
216 | 260 |
|
| 261 | + %% Async completion signal from Python |
| 262 | + %% Sent when an async task (e.g., ASGI app) completes and response is ready |
| 263 | + %% Accept both atom and binary forms since Python sends binaries |
| 264 | + {write_ready, Fd} -> |
| 265 | + handle_async_write_ready(Fd, State); |
| 266 | + |
| 267 | + {<<"write_ready">>, Fd} -> |
| 268 | + handle_async_write_ready(Fd, State); |
| 269 | + |
217 | 270 | %% Control messages |
218 | 271 | {stop, From, MRef} -> |
219 | 272 | cleanup(State), |
@@ -265,8 +318,11 @@ handle_fd_handoff(Fd, ClientInfo, State) -> |
265 | 318 | %% Register FD for monitoring |
266 | 319 | case py_nif:reactor_register_fd(Ref, Fd, self()) of |
267 | 320 | {ok, FdRef} -> |
| 321 | + %% Inject reactor_pid into client_info for async signaling |
| 322 | + ClientInfoWithPid = ClientInfo#{reactor_pid => self()}, |
| 323 | + |
268 | 324 | %% Initialize Python protocol handler |
269 | | - case py_nif:reactor_init_connection(Ref, Fd, ClientInfo) of |
| 325 | + case py_nif:reactor_init_connection(Ref, Fd, ClientInfoWithPid) of |
270 | 326 | ok -> |
271 | 327 | %% Store connection info |
272 | 328 | ConnInfo = #{ |
@@ -319,6 +375,16 @@ handle_read_ready(FdRes, State) -> |
319 | 375 | }, |
320 | 376 | loop(NewState); |
321 | 377 |
|
| 378 | + {ok, <<"async_pending">>} -> |
| 379 | + %% Async task submitted (e.g., ASGI app running as task) |
| 380 | + %% Don't reselect - wait for {write_ready, Fd} signal from Python |
| 381 | + %% Increment request count since task was accepted |
| 382 | + error_logger:info_msg("Received async_pending for fd=~p~n", [Fd]), |
| 383 | + NewState = State#state{ |
| 384 | + total_requests = State#state.total_requests + 1 |
| 385 | + }, |
| 386 | + loop(NewState); |
| 387 | + |
322 | 388 | {ok, <<"close">>} -> |
323 | 389 | %% Close connection |
324 | 390 | close_connection(Fd, FdRes, State); |
@@ -370,6 +436,31 @@ handle_write_ready(FdRes, State) -> |
370 | 436 | loop(State) |
371 | 437 | end. |
372 | 438 |
|
| 439 | +%% ============================================================================ |
| 440 | +%% Async Write Ready Handler |
| 441 | +%% ============================================================================ |
| 442 | + |
| 443 | +%% @private |
| 444 | +%% Handle async completion signal from Python. |
| 445 | +%% This is sent when an async task (like an ASGI app) has completed |
| 446 | +%% and the response buffer is ready to be written. |
| 447 | +handle_async_write_ready(Fd, State) -> |
| 448 | + #state{connections = Conns} = State, |
| 449 | + |
| 450 | + error_logger:info_msg("handle_async_write_ready called for fd=~p~n", [Fd]), |
| 451 | + |
| 452 | + case maps:get(Fd, Conns, undefined) of |
| 453 | + #{fd_ref := FdRef} -> |
| 454 | + %% Response buffer is ready, trigger write selection |
| 455 | + error_logger:info_msg("Triggering write selection for fd=~p~n", [Fd]), |
| 456 | + py_nif:reactor_select_write(FdRef), |
| 457 | + loop(State); |
| 458 | + undefined -> |
| 459 | + %% Connection not found (may have been closed), ignore |
| 460 | + error_logger:warning_msg("Connection not found for fd=~p~n", [Fd]), |
| 461 | + loop(State) |
| 462 | + end. |
| 463 | + |
373 | 464 | %% ============================================================================ |
374 | 465 | %% Connection Management |
375 | 466 | %% ============================================================================ |
|
0 commit comments