Skip to content

feat(nfm): enforce the NF heart-beat procedure - #95

Open
Niahh wants to merge 5 commits into
free5gc:mainfrom
Niahh:feat/nf-heartbeat
Open

feat(nfm): enforce the NF heart-beat procedure#95
Niahh wants to merge 5 commits into
free5gc:mainfrom
Niahh:feat/nf-heartbeat

Conversation

@Niahh

@Niahh Niahh commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Hello,

This PR is part of a series of PRs that will also modify the SBI NFs and the configuration file. The util PR adds the shared heart-beat runner those NFs use to answer the enforcement introduced here.

free5gc/util#48
free5gc/ausf#67

It addresses the following:

  • On shutdown, the NRF drops the whole NfProfile collection, which does not behave well in a cloud setup
  • The validityPeriod is hardcoded at 100s
  • Enforcement of the heartBeatTimer and implementation of TS 29.510 clause 5.2.2.3 (Rel-18)

Presentation of the changes

NfProfile collection drop on shutdown

The NRF no longer touches the registry on shutdown: profiles outlive the process that accepted them, and with several replicas sharing one database, a single terminating pod must not deregister the whole network. Stale instances are handled by the heart-beat feature below instead. The registration counter that only served the shutdown wait (NfRegistNum, AddNfRegister, DelNfRegister, waitNfDeregister) goes with it.

Validity Period

Discovery responses now advertise a validityPeriod of timer * suspendFactor instead of the hardcoded 100s, so a consumer honouring it stops caching results longer than the NRF can vouch for the instances' liveness.

Heart-beat feature

The current behavior is to echo the heartBeatTimer back to the registering NF without enforcing it.

In a deployment scenario where an NF does not deregister properly and gets killed, we end up with stale profiles in the database. The 3GPP spec indicates that the NRF can set an NF's status to SUSPENDED when it has not been heard from within a configured period of time. Registration and every NFUpdate stamp a lastHeartBeat on the profile document. A sweeper ticks once per heart-beat interval and moves instances silent for timer * suspendFactor seconds to SUSPENDED, takes them out of discovery, and notifies their subscribers.

Each instance is claimed with an atomic findOneAndUpdate, so replicas sharing a database notify disjoint sets. The deadline uses the configured timer, never the stored profile's, which an NF could inflate.

Any heart-beat lifts a suspension, including a load-only NFUpdate that does not write nfStatus itself, or a suspended instance that kept heart-beating would stay SUSPENDED forever. The suspendedAt stamp is cleared with it, so the drop sweep never sees a stale stamp on a live instance.

Deregistration of suspended instances is opt-in, via dropDelay. When it is set and an NF stays SUSPENDED for more than dropDelay seconds, its profile is removed with the same effects as NFDeregister (notifications, urilist clean-up, OAuth cert removal). Since an NF started without a pinned nfInstanceId registers under a new one each boot, this is what keeps stale records from accumulating. When dropDelay is not set, the NRF suspends but never deletes.

Several guards keep the drop sweep from claiming a live instance: the delay counts from suspendedAt, an instance with a fresh lastHeartBeat is never claimed, findOneAndDelete claims atomically so subscribers hear DEREGISTERED once, and the sweep stays quiet for one suspension deadline plus one interval after startup, since while the NRF was down no instance could lift its suspension. Beyond that, an NF re-registers on a heart-beat 404, so an instance dropped while still alive is back in the registry on its next heart-beat.

  • The NRF now owns the heart-beat interval: the registration response always carries the value the NRF enforces (configured or default), and registering NFs reset their timer from it. The NF's proposal is ignored, and a patch trying to set heartBeatTimer is rejected.
  • The interval is re-stamped on every heart-beat. It is baked into the profile at registration while the sweeps use the current configuration, so lowering the timer would otherwise leave registered NFs ticking at the old interval and flapping into SUSPENDED.
  • Discovery now excludes SUSPENDED instances at the query level.
  • lastHeartBeat and suspendedAt are sweep bookkeeping and are stripped from the NFUpdate and NFGet responses.
  • Both sweeps claim documents by nfStatus plus timestamp on every tick, so the two matching indexes are created at startup. Creation is idempotent and a missing index only costs performance, so a failure is logged and not fatal.

Patch validation

validateNfProfilePatch guards paths, but a whole-document op (path "", RFC 6901) matches no path guard, so an NF could rename its own nfInstanceId or set its own heartBeatTimer through one. The applied result is now compared against the stored profile before it is persisted, and the update is rejected with a 400 when an NRF-owned field changed.

UpdateNFInstanceProcedure also answers 404 when the instance was deregistered between the patch write and the read-back, instead of failing on a nil document.

New configuration IE:

heartbeat: # NF Heart-Beat procedure, refer to TS 29.510 clause 5.2.2.3
  timer: 10         # interval in seconds advertised to registering NFs (1~3600)
  suspendFactor: 2  # missed intervals before an instance is SUSPENDED (2~10)
  dropDelay: 3600   # opt-in: seconds in SUSPENDED before the profile is deregistered (60~604800)

The block itself can be omitted. timer and suspendFactor are optional and default to 10 and 2. dropDelay has no default: leaving it unset disables deregistration entirely. When set, config load rejects a value at or below timer * suspendFactor, since that would deregister instances the moment they are suspended.

Testing

  • Unit tests cover the patch validation rules (immutable nfInstanceId, NRF-owned heartBeatTimer, case normalization, copy source), the whole-document pointer invariants, the byte-exact nfStatusPatched classification, the config defaults and range/cross-field validation, and the startup grace boundary.
  • The sweeps themselves are MongoDB-bound and have no unit coverage; verified manually against a local core: suspension after silence, discovery exclusion, recovery on the next heart-beat, and drop with DEREGISTERED notifications after the delay.

This work is sponsored by Free Mobile!

@Niahh
Niahh force-pushed the feat/nf-heartbeat branch 2 times, most recently from 21b7465 to 2b4d860 Compare August 5, 2026 16:47
Implement TS 29.510 clause 5.2.2.3: track the last heart-beat per
profile, suspend instances silent past timer * suspendFactor and,
when dropDelay is set, deregister them after that many more seconds
in SUSPENDED. The sweeps claim instances atomically, so replicas
sharing a database notify disjoint sets, and any heart-beat lifts a
suspension. The NRF now owns the interval: advertised at
registration, rejected on patch. Suspended instances leave discovery
and validityPeriod tracks the suspension deadline instead of a
hardcoded 100.

Deregistration stays opt-in, waits out a startup grace and requires
a stale heart-beat; an NF re-registers on a heart-beat 404, so an
instance dropped while still alive returns on its next heart-beat.
Remove the shutdown collection drop: profiles outlive the process,
and a terminating replica must not deregister the network.
Niahh added 4 commits August 7, 2026 13:25
A whole-document op (path "") never matches the path guards in
validateNfProfilePatch, so an NF could rename its own nfInstanceId or
set its own heartBeatTimer. Check the applied result against the
stored profile before persisting.
The interval is baked into the profile at registration while the sweeps
use the current configuration: lowering the timer left registered NFs
ticking at the old interval and flapping into SUSPENDED. Stamp the
configured value on every heart-beat so the 200 response carries it.
Both sweeps claim documents by nfStatus plus timestamp on every tick;
without an index each claim scans the collection.
A revived instance kept its old stamp; the drop sweep only tolerates
that because it also requires a stale lastHeartBeat.
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