PFCP async mode + coarse procedure gate (Gy update_credits pilot) - #46
Conversation
| Issued <- async_m:lift(smf_pfcp_context:modify_session_async(PCC, [], #{}, BearerMap, PCtx0)), | ||
| Result <- await_modify(Issued), | ||
| {PCtx, _, _} = Result, | ||
| async_m:modify_data(fun(D) -> D#{pfcp => PCtx, pcc => PCC} end) |
There was a problem hiding this comment.
Done: async_m:modify_data(_#{pfcp => PCtx, pcc => PCC}) — verified it compiles and runs (erlando cut scopes to the map-update, not the enclosing call) (3c39802).
| D0 <- async_m:get_data(), | ||
| #{pfcp := PCtx0, bearers := BearerMap, pcc := PCC0} = D0, |
There was a problem hiding this comment.
can't these be merged into a single line?
There was a problem hiding this comment.
Merged: #{pfcp := PCtx0, bearers := BearerMap, pcc := PCC0} <- async_m:get_data() (3c39802).
| Issued <- async_m:lift(smf_pfcp_context:modify_session_async(PCC, [], #{}, BearerMap, PCtx0)), | ||
| Result <- await_modify(Issued), |
There was a problem hiding this comment.
Merged: {PCtx, _, _} <- await_modify(Issued) (3c39802).
| gate_result(async_m:run_async(M, Ok, Err, State, Data), State, Data). | ||
|
|
||
| %% Rewrite a gen_statem result so State.gate reflects whether a PFCP procedure is still in flight. | ||
| gate_result({next_state, S, D}, _S0, _D0) -> {next_state, S#{gate => gate_of(D)}, D}; |
There was a problem hiding this comment.
Why is it necessary to copy the gate state into the FSM state? The gate check above could instead access async_pending directly, coulnd't it?
There was a problem hiding this comment.
Removed. There is no gate field anymore — gate_result/3, gate_of/1 and run_gated/5 are gone. With async_pending in the State map (PR #45), the gate clauses read it directly and the park/drain of the registry is itself the state-term change that re-fires postponed events, so nothing is copied (3c39802).
| %% Coarse procedure gate: postpone procedure-initiating events while a PFCP | ||
| %% procedure is still in flight (async_pending non-empty). Re-delivered once | ||
| %% gate flips back to idle (see gate_result/3). | ||
| handle_event(internal, {session, {update_credits, _}, _}, #{gate := busy}, _Data) -> |
There was a problem hiding this comment.
| handle_event(internal, {session, {update_credits, _}, _}, #{gate := busy}, _Data) -> | |
| handle_event(internal, {session, {update_credits, _}, _}, #{async_pending := AsyncP}, _Data) whem map_size(AsyncP) /= 0 -> |
Note: putting async_pending into data instead of putting into state seems to be a mistake in the async PR.
There was a problem hiding this comment.
Done. Moved the async_pending registry from Data into the gen_statem State (PR #45, 84eed78) so mutating it is a state-term change — which is what makes gen_statem re-deliver postponed events. The gate now guards on it directly instead of a copied flag: #{async_pending := P}, _Data) when map_size(P) =/= 0 -> {keep_state_and_data, [postpone]} (3c39802).
039f76e to
107ea65
Compare
107ea65 to
5d5090f
Compare
Step 2 of the non-blocking context I/O work. Stacked on #45 (
async_mfoundation) — base branch isasync-m-step1, so this PR's diff is only the step-2 changes. Review/merge #45 first.Makes mid-session PFCP non-blocking, and proves it by converting one pilot procedure — the Gy
update_credits→session_modificationhandler — off the blockinggen_statem:call. Everything except the pilot is behavior-preserving.What's here
smf_sx_node:send_request/2(mirrorscall/2, mints a request id, delivers the reply as{'$async_reply', ReqId, Reply}reusing async_m monad + driver: non-blocking context I/O foundation #45's info clause; socket untouched) +smf_pfcp_context:modify_session_async/5and a puremodify_session_result/2decoder. Fixes a latent bug for free: the oldgen_statem:call/2used the 5s OTP default while the socket retries ~180s, so the context couldexit(timeout)before the UPF answered.async_pendingnon-empty), agatefield in the FSM state isbusyand procedure-initiating events arepostponed; the busy→idle flip on drain re-fires them. This reproduces today's one-procedure-at-a-time serialization non-blockingly (so a reentrant RAR/report is serviced during the await instead of deadlocking). Gated set: GTP requests, the internalupdate_creditsevent, andgx/gy RAR/ASR/pfcp_timer(all of which mutate the shared PCtx). Coarse first; a later step refines toward per-resource concurrency and answer-RAA-immediately-queue-the-procedure.update_creditsbecomes a lineardo([async_m || …])procedure (issue → await → resume). The oldthrow-based error smuggling is dropped: the monad hands the error handler liveData, sohandle_ctx_error/4readscontextfrom there.Why the gate is mandatory
Every PFCP procedure threads the shared session
PCtx. Two PFCP-touching procedures can't overlap — a second one readingPCtxat issue time, before the first's resume writes back, loses the update. The old blocking code enforced this implicitly; going async requires the gate to keep it.Tests
smf_pfcp_context_SUITE(decode) andgtp_context_gate_SUITE(pure gate logic) — 9/9.pgw_SUITEcases over the simulated UPF: async round-trip, gate-serializes-a-second-procedure (holding the reply viasmf_test_sx_up:disable), and reentrancy-during-await.pgw_SUITE127/127 (124 + 3); default-profilerebar3 compilegreen.Review outcome
Whole-branch review returned YES-with-fixes; all Important findings fixed in this branch: gate now covers RAR/ASR/timer (closing a real serialization regression), and the
disable-based tests have failure-safe teardown. Two Minors deferred as non-bugs: an error-semantics improvement on an untested branch, and resume-re-park gate-wrapping that only matters for future multi-awaitprocedures.🤖 Generated with Claude Code