Skip to content

SMF unified data-layer migration (E3): reshape smf_db to the document contract, add syn/CAS, session write-through #29

Description

@next-nf

Umbrella tracking issue for bringing SMF onto the org unified data-layer standard. SMF is the last and largest migration in the epic and is benchmark-gated (per the standard §7). This issue describes exactly what to do and the standard sections that apply. The narrow stub #3 (smf_aaa raw-ETS → smf_db) is Phase 2 of this plan.

Epic: #6
Org standard (authoritative): next-nf/next-nfplugins/nf-conventions/skills/nf-architecture/reference/database.md (skill: nf-conventions:nf-architecture). Section references below point into it.
Reference implementations (already migrated — use as templates): udr (reference impl), pcf, chf (chf Phase 1: next-nf/chf#13).


Why SMF is special (read first)

The standard has SMF-specific carve-outs:

  • §5.3 node-local carve-out — SMF's hot per-node runtime indices are node-local by design; they SKIP the syn cluster lock and use plain cas_put/dirty ops. Only cluster-wide identity entities (subscribers, sessions) use the lock.
  • §7 — SMF migrates last and is the largest migration; do not begin until chf's conformance passes (chf Phase 1 done).
  • §8.5 performance gate — before migrating the hot registry ops through smf_db, benchmark Mnesia-ram vs raw-ETS on those ops; the acceptable regression threshold must be documented and committed BEFORE migration begins.
  • P7 dirty-read fast path — even hot per-node registries use the API, but reads may be lock-free; the call site documents the concern.

Current state (what exists vs the gaps)

Already present:

  • An smf_db behaviour (apps/smf_core/src/smf_db.erl) with two backends: smf_db_ets (raw ETS) and smf_db_mnesia (scope=local→ETS, scope=global→Mnesia ram_copies dirty ops). Backend selected via application:get_env(smf_core, db_backend, …), cached in persistent_term.
  • The node-local hot registries already route through smf_db and are the §5.3 carve-out (correct to keep ETS-backed): gtp_context_reg (local bag — TEID/SEID/context-key index), gtp_path_reg, smf_socket_reg, smf_sx_node_reg, smf_local_pool_reg.

Gaps vs the standard (the work):

  1. smf_db is ETS-shaped, not the document contract. Its API is create/insert/lookup/select/fold/…no version-CAS, no binary keys, no collections, no update/3 functional CAS. The standard's §2.2 contract is the 11 generic callbacks (child_spec/1, ensure_collection/2, get/2{ok,Doc,Vsn}, put/3, cas_put/4, delete/2, take/2, find/2, find_by/3, fold/4, count/2) + the §2.3 facade (update/3, create/3, ready/0, await_ready/1).
  2. No syn, no with_entity, no CAS. smf_cluster only coordinates Mnesia startup. The standard's §5.1 Layer-1 lock does not exist. syn is not a dep.
  3. Cluster-wide state is unsafe. gtp_path_db:cas_restart_counter/3 is a home-grown read-modify-write over dirty_read/dirty_write with no serialization (a race under the current backend). gtp_context_global (IMSI/IMEI registration) and smf_global are cluster-wide with no lock/CAS.
  4. Session state is process-only (biggest gap). The PDU-session/bearer/tunnel/PFCP/PCC state (#context/#tunnel/#bearer/#pfcp_ctx/#pcc_ctx) lives solely in gtp_context gen_statem heaps — no write-through to any store, no recovery (in-flight sessions are lost on node restart). These records are the cluster-wide "entities" the document store is meant to hold (P2/P3).
  5. smf_aaa_session_reg still uses raw ETS (6 ets:* sites) — bypasses smf_db entirely (issue Route smf_aaa raw-ETS through smf_db (benchmark-gated) #3).
  6. No §8.5 microbenchmark. bench_SUITE:contexts_at_scale is an integration benchmark; it does NOT isolate ets:lookup vs smf_db:lookup latency on the hot path.
  7. Minor: gtp_context_reg update/4 has a raw ets:insert bypass (~line 75) — fix to go through smf_db.

Prerequisites — must land first (Blocked by)

Independent (do NOT block this migration): #4 (OTEL), #5 (HTTP app split), #7 (Diameter request_errors).


Plan (phased — each phase is an independently testable deliverable)

Phase 0 — Performance-gate harness (§8.5) — GATES all hot-path work

  • Write a microbenchmark (extend apps/smf_core/test/bench_SUITE.erl) comparing raw ets:lookup/insert vs smf_db:lookup/write on both backends for the hot registry ops (gtp_context_reg:lookup, smf_aaa_session_reg:lookup), reported µs/op at P50/P99.
  • Document + commit the acceptable regression threshold before any hot-path migration (§8.5 mandate).
  • Accept: benchmark case committed; threshold doc committed; baseline numbers recorded.

Phase 1 — Reshape smf_db to the org document contract (§2.2, §2.3, P2, P9, §8.1)

  • Implement the 11 generic callbacks + facade update/3/create/3/ready/0/await_ready/1; version-as-CAS-metadata (get{ok,Doc,Vsn}); binary-keyed map documents in named collections; schema_version + upgrade-on-read (§4.5). Mnesia generic envelope per §3.1; disc-schema-at-boot (lesson from pcf/chf).
  • Decide extend-in-place vs parallel-module-then-swap; keep the node-local fast path (P7; §5.3 — no syn).
  • Add the backend-agnostic conformance suite (§8.1) on Mnesia ram+disc. Port from udr/pcf/chf.
  • Accept: conformance green on both Mnesia tiers; error taxonomy §6.1; dialyzer clean.

Phase 2 — Route smf_aaa_session_reg through smf_db, benchmark-gated (this is issue #3)

  • Replace the 6 raw ets:* in smf_aaa_session_reg.erl with smf_db; keep smf_aaa_session_seq atomic counter as raw ETS (exception class — atomic increment is not a db op; document it).
  • Gate on the Phase-0 threshold.
  • Accept (from Route smf_aaa raw-ETS through smf_db (benchmark-gated) #3): no raw ets:* in smf_aaa (except the documented counter); works on both backends; hot-path latency within the committed bound or documented.

Phase 3 — Cluster-wide CAS safety + syn with_entity (§5.1, P4)

  • Add syn dep and smf_cluster:with_entity(Scope, Key, Fun) (the standard's Layer-1 lock) — port the shape from udr/pcf/chf <comp>_cluster.
  • Fix gtp_path_db:cas_restart_counter to use update/3 CAS (+ with_entity for the multi-step procedure).
  • gtp_context_global IMSI/IMEI registration and smf_global: wrap cluster-wide mutations in with_entity + CAS.
  • Accept: no home-grown dirty read-modify-write on cluster-wide tables; multi-node CT proving the restart-counter/registration races are serialized.

Phase 4 — Session/bearer document schema + write-through + recovery (P2, P3, §4.4, §6.4) — LARGEST

  • Define the session document schema (binary-keyed maps + schema_version): decide which #context/#tunnel/#bearer/#pfcp_ctx/#pcc_ctx fields form which aggregate/collection (P3 — one aggregate = one document; e.g. session, bearer).
  • Per-aggregate accessor modules (§4.4): from_doc/1/to_doc/1, invariant Funs, upgrade-on-read.
  • Write-through: persist session state changes through smf_db (global tables) from the gtp_context gen_statem.
  • Recovery: read state back after node restart; with_entity for the subscriber/session entity.
  • Readiness gate (§6.4): SMF rejects signaling until smf_db reports ready.
  • Depends on Replace jsx with OTP json; binary-keyed payloads #6 (binary payloads) + Migrate ~31 hand-written classic records to OTP-29 native records #2 (records).
  • Accept: a session survives a node restart (recovery test); document conformance; readiness gate in place.

Biggest risk

The §8.5 perf gate: gtp_context_reg and smf_aaa_session_reg are on the per-packet hot path (every GTP-C message, every AAA request). If Mnesia-ram adds unacceptable lookup latency vs raw ETS, the approach for those tables needs revisiting (they may stay ETS-backed under the §5.3 carve-out). This is why Phase 0's microbenchmark + committed threshold is the first deliverable and gates everything downstream.


Checklist (§9 of the standard, SMF-adapted)

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions