async_m monad + driver: non-blocking context I/O foundation - #45
Merged
Conversation
This was referenced Jul 21, 2026
This was referenced Jul 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Foundation for non-blocking context I/O — step 1 of a larger effort to stop the context gen_statem from blocking in selective receives on external calls (Gx/Gy/PFCP/GTP). Blocking there is a latent reentrant-deadlock generator: a mid-session Gx CCR-U blocks the FSM, the PCRF pushes an RAR for the same session, and if the CCA is gated behind the RAA the context can't answer it → deadlock. Same shape applies to PFCP reports, GTP Delete Session, OCS RAR.
This PR adds only the transport foundation. Nothing is rewired yet, so there is no behavior change — the new machinery is inert until a consumer parks a request id.
What's here
async_m— a hand-rolled erlando monad (state + error + suspension) whose value isfun((State, Data) -> {Result, State, Data}),Result :: {ok,_} | {error,_} | {await, ReqId, Conts}.>>=short-circuits on error and, onawait, captures the rest of the do-block as a continuation list. Built on the erlandodo/error_mstack already used across these modules — no new dependency.run_async/5,handle_reply/4,resume/2: parks suspended computations inData.async_pendingkeyed by request id and resumes them when the matching reply arrives.resumereverses the outermost-first continuation list so the innermost continuation gets the raw reply first, then folds outward.async_apply/1— spawns a monitored worker with a tagged monitor; success delivers{'$async_reply', ReqId, {ok, Result}}, a crash surfaces as a taggedDOWNmapped to{error, {worker_down, Reason}}.gtp_contextwiring — anasync_pendingslot, two guarded reply-dispatch info clauses plus a normal-exit ignore clause, and anasync_dispatch/4that keeps the existingthrow:#ctx_err{}→handle_ctx_errorbridge alive across suspensions (so a throw from a resumed step still produces its protocol error reply).Why a monad / continuations
Once calls become non-blocking, a multi-step procedure (send request → await reply → send next) can no longer live on one call stack. The continuation — the "where was I" — has to be reified. The monad makes procedures read as linear
do([async_m || ...])blocks while the process stays fully responsive between steps, which is exactly what lets a reentrant RAR be serviced while a CCR-U is outstanding.Tests
async_m_SUITE: 16/16 — monad laws, three-way bind dispatch, suspend/resume ordering (incl. nested and multi-suspend), empty-continuation resume, and the worker success/crash paths.pgw_SUITE: 124/124 — unchanged, proving no regression.Scope / follow-ups
Next steps build on this: PFCP async mode (the only always-blocking interface), the establishment chain, terminate, and the per-context procedure gate. Two deferred robustness notes for the first real consumer: a symmetric ignore clause for a stale
$async_reply, and error-channel conversion for procedures that commit compensable side effects.Enables the async plumbing that #22 (UE-requested bearer resource modification) and #23 (overriding PCRF QoS on Modify Bearer Command) need. Does not close them.
🤖 Generated with Claude Code