Skip to content

feat(nfheartbeat): add NF heartbeat runner - #48

Open
Niahh wants to merge 1 commit into
free5gc:mainfrom
Niahh:feat/nf-heartbeat
Open

feat(nfheartbeat): add NF heartbeat runner#48
Niahh wants to merge 1 commit into
free5gc:mainfrom
Niahh:feat/nf-heartbeat

Conversation

@Niahh

@Niahh Niahh commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Hello,

This PR is part of a series of PRs that will also modify the NRF, the other SBI NFs and the configuration files.
free5gc/ausf#67
free5gc/nrf#95
I will create PR for the other NFs after these design is accepted.

This PR addresses the following:

  • No SBI NF sends the periodic NF heart-beat of TS 29.510 clause 5.2.2.3, so with the NRF-side enforcement added by the companion NRF PR every profile would be SUSPENDED then dropped as stale
  • The loop each NF needs is identical: same PATCH, same interval ownership, same recovery and same shutdown ordering. So I decided to add it here to share the change.

Presentation of the changes

New nfheartbeat package

A single Runner drives the NF heart-beat toward the NRF. Each NF supplies only its transport, through the Registrar interface:

type Registrar interface {
	UpdateNFInstance(ctx context.Context, patchItems []models.PatchItem) (
		models.NrfNfManagementNfProfile, *models.ProblemDetails, error)
	RegisterNFInstance(ctx context.Context) (int32, error)
}

The Runner decides when to heart-beat or re-register, the Registrar carries it out, so the SBI clients, the OAuth2 token handling and the NF profile stay on the NF side. PatchItems() returns the body from clause 5.2.2.3.2 (replace /nfStatus REGISTERED) so no NF has to spell it out.

Start launches the loop after a successful registration and takes the heartBeatTimer the NRF assigned there. Wait blocks until the goroutine has exited, which is the guarantee the deregistration relies on: no heart-beat PATCH and no re-registration PUT can reach the NRF afterwards.

Interval ownership

The NRF owns the interval. The value from the registration response is adopted, and every 200 heart-beat answer carrying a new one re-arms the ticker. A 204 carries nothing and the current interval stands, and so does a 200 whose heartBeatTimer is 0: the int32 model cannot tell an explicit 0 from an absent field, so 0 must not read as "disable" the way the legacy openapi nrf/service.go helper reads it.

While the NRF has assigned nothing, the interval comes from the fallbackTimer callback each NF passes, typically its config getter, and then from DefaultTimer. An NRF-assigned value above MaxTimer is capped, so a misbehaving NRF cannot park the heart-beat for hours.

DefaultTimer (10), MinTimer (1) and MaxTimer (3600) are exported: they match the NRF defaults and the heartBeatTimer bounds of the NRF profile validation, and NF config validators are expected to stay within them. A fallback longer than the NRF default would cross the deadline past which the NRF suspends a silent instance.

Recovery

The loop recovers from NRF-side profile loss, which the NRF PR can now cause on purpose (dropDelay):

  • A 404 on the heart-beat means the NRF no longer holds the profile: the NF re-registers immediately. The 404 is recognised in any of the shapes a Registrar may deliver it, as ProblemDetails or as an openapi.GenericOpenAPIError by value or by pointer.
  • 3 consecutive failures of any kind also trigger a re-registration. This covers the OAuth2 case where the token request fails before the PATCH can even observe the 404.
  • The failure counter is only cleared when the heart-beat succeeds or the re-registration does.
  • A panic inside a heart-beat attempt is contained and counted as one failure, so it costs one heart-beat instead of silently ending them all. A panic on the re-registration path is contained too, and the loop machinery itself has a last-resort recover so an escaping panic cannot crash the NF process.

Shutdown wins over recovery: a tick that fires while the context is already cancelled is skipped, a re-registration is skipped for the same reason, and a Start that raced the shutdown does nothing.

New dependency

github.com/free5gc/openapi becomes a direct dependency of util. The package needs models for the PatchItem body and the NF profile, and openapi.GenericOpenAPIError to classify the 404. openapi does not depend on util, so there is no cycle.

Testing

  • Unit tests cover the interval resolution (NRF value over fallback over default, non-positive values, cap), the tick state machine (200 adopting a timer, 204 keeping it, the three 404 shapes, failure accumulation and the threshold, panicking attempts counting as failures), the constructor validation, and the lifecycle: second Start ignored, Start on a cancelled context, tick and re-registration skipped on shutdown, Wait returning when the heart-beat never started.
  • TestWaitCoversInFlightTick pins the guarantee deregistration relies on: Wait does not return while a PATCH is still in flight, even after the shutdown was signalled.
  • The NRF is a fake Registrar, so the suite needs no network, and the timing-dependent cases run on the testing/synctest fake clock rather than real sleeps.

This work is sponsored by Free Mobile!

Drive the periodic NF heartbeat toward the NRF from a shared package,
per 3GPP TS 29.510 clause 5.2.2.3.2: adopt the interval carried by NRF
answers, re-register on 404 or after consecutive failures, and wait for
the loop at shutdown so no heartbeat lands after the deregistration.
Each NF supplies only its transport through the Registrar interface.
@Niahh

Niahh commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Hey team, any thoughts on this new feature ?

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