Skip to content

PFCP async mode + coarse procedure gate (Gy update_credits pilot) - #46

Merged
next-nf merged 4 commits into
mainfrom
async-pfcp-step2
Jul 27, 2026
Merged

PFCP async mode + coarse procedure gate (Gy update_credits pilot)#46
next-nf merged 4 commits into
mainfrom
async-pfcp-step2

Conversation

@next-nf

@next-nf next-nf commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Step 2 of the non-blocking context I/O work. Stacked on #45 (async_m foundation) — base branch is async-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_creditssession_modification handler — off the blocking gen_statem:call. Everything except the pilot is behavior-preserving.

What's here

  • Async PFCP transportsmf_sx_node:send_request/2 (mirrors call/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/5 and a pure modify_session_result/2 decoder. Fixes a latent bug for free: the old gen_statem:call/2 used the 5s OTP default while the socket retries ~180s, so the context could exit(timeout) before the UPF answered.
  • Coarse procedure gate — while a PFCP procedure is in flight (async_pending non-empty), a gate field in the FSM state is busy and procedure-initiating events are postponed; 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 internal update_credits event, and gx/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.
  • Pilot conversionupdate_credits becomes a linear do([async_m || …]) procedure (issue → await → resume). The old throw-based error smuggling is dropped: the monad hands the error handler live Data, so handle_ctx_error/4 reads context from there.

Why the gate is mandatory

Every PFCP procedure threads the shared session PCtx. Two PFCP-touching procedures can't overlap — a second one reading PCtx at 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) and gtp_context_gate_SUITE (pure gate logic) — 9/9.
  • Three pgw_SUITE cases over the simulated UPF: async round-trip, gate-serializes-a-second-procedure (holding the reply via smf_test_sx_up:disable), and reentrancy-during-await.
  • Full pgw_SUITE 127/127 (124 + 3); default-profile rebar3 compile green.

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-await procedures.

🤖 Generated with Claude Code

Comment thread apps/smf_core/src/gtp_context.erl Outdated
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)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could is a cut

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread apps/smf_core/src/gtp_context.erl Outdated
Comment on lines +889 to +890
D0 <- async_m:get_data(),
#{pfcp := PCtx0, bearers := BearerMap, pcc := PCC0} = D0,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't these be merged into a single line?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merged: #{pfcp := PCtx0, bearers := BearerMap, pcc := PCC0} <- async_m:get_data() (3c39802).

Comment thread apps/smf_core/src/gtp_context.erl Outdated
Comment on lines +893 to +894
Issued <- async_m:lift(smf_pfcp_context:modify_session_async(PCC, [], #{}, BearerMap, PCtx0)),
Result <- await_modify(Issued),

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

single line merge?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merged: {PCtx, _, _} <- await_modify(Issued) (3c39802).

Comment thread apps/smf_core/src/gtp_context.erl Outdated
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};

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread apps/smf_core/src/gtp_context.erl Outdated
%% 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) ->

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

@next-nf
next-nf force-pushed the async-pfcp-step2 branch 2 times, most recently from 039f76e to 107ea65 Compare July 20, 2026 05:36
Comment thread apps/smf_core/src/smf_pfcp_context.erl
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant