Skip to content

feat(webui): send periodic NF heartbeat to NRF - #233

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

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

Conversation

@Niahh

@Niahh Niahh commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Hello,

This PR is part of a series of PRs that will also modify the other SBI NFs and the configuration file. It depends on the util shared nfheartbeat package, and on the NRF PR enforcing the procedure on the other side.

free5gc/nrf#95

It addresses the following:

  • The webconsole registers with the NRF as an AF and never heartbeats, so with the new NRF-side enforcement (TS 29.510 clause 5.2.2.3) its profile would be SUSPENDED then dropped as stale. The sweep that suspends silent instances filters on nfStatus and lastHeartBeat alone, with no nfType condition, so an AF profile goes stale exactly like an SBI NF one
  • The heartBeatTimer assigned by the NRF in the registration response is ignored
  • The NFRegister retry loop runs on context.Background() and sleeps 2s between attempts without watching any context, so a shutdown during an NRF outage waits it out, and a heartbeat or re-registration could land on the NRF after the NFDeregister

Presentation of the changes

heartbeat feature

After a successful registration, the webconsole starts the heartbeat runner from util/nfheartbeat, which sends the NFUpdate PATCH of clause 5.2.2.3.2 (replace /nfStatus REGISTERED) at the current interval. The loop, the interval ownership, the recovery and the panic containment live in the shared package; the webconsole supplies only the transport, through a small nrfRegistrar adapter. The NRF calls of webui_context are package level rather than methods on a consumer service, so the adapter carries no state of its own:

  • UpdateNFInstance is the new SendUpdateNFInstance.
  • RegisterNFInstance is the existing SendNFRegistration, and returns the heartBeatTimer the NRF assigned.

Behaviour that follows from the shared runner: the NRF owns the interval (adopted from the registration response, re-armed by every 200 answer carrying a new value, a 204 leaves it as is), a 404 on the heartbeat re-registers immediately, 3 consecutive failures of any kind do too.

A new optional nfHeartBeatTimer configuration IE serves as fallback while the NRF has not assigned an interval.

The heartbeat starts from the registration goroutine in Start, once the registration has succeeded and IsRegistered is set: a profile that was never registered has nothing to keep alive. Runner.Start is a no-op once its context is done, so that goroutine racing a shutdown is safe.

On shutdown the ordering matters: Terminate cancels the application context, which is what stops the loop, then waits for the heartbeat goroutine to exit before sending the NFDeregister, inside the existing IsRegistered guard. Nothing else cancels that context, so the cancel has to come first or the wait would never return.

SendUpdateNFInstance

New call for the NFUpdate PATCH. It returns the raw error alongside any ProblemDetails, so the runner can read the GenericOpenAPIError status and classify a 404 even when the NRF answers without a problem body.

It honours the caller's context. GetTokenCtx takes no parent, so the token request itself stays uncancelable; transplanting the token into the caller's context lets at least the PATCH observe a shutdown.

Registration hardening

  • SendNFRegistration takes a context instead of using context.Background(), and the 2s retry wait is interruptible, so a shutdown during an NRF outage returns immediately instead of sleeping through it. RetrySendNFRegistration forwards it. The MaxRetryAttempts bound is kept: the webconsole deliberately gives up and runs with limited functionality rather than retrying forever.
  • The response handling was extracted into processRegisterResponse, which adopts the heartBeatTimer along with the oauth2 custom info.
  • The OAuth2 custom info is only applied on the startup registration, hence the new applyOAuth2 argument. OAuth2Required is read concurrently by the request handlers once the web server is running, so a re-registration from the heartbeat goroutine may not write it: if the NRF flips the setting later, the webconsole logs a warning asking for a restart instead of racing the handlers.
  • The Location header is no longer parsed, and NfInstanceID is no longer written from it. NFRegister is a PUT on the instance ID the webconsole chose (clause 6.1.3.2.2), so the NRF echoes that ID back and the parsing could only reproduce it or corrupt it. The generated client fills Location only on a 201, so every re-registration against an NRF that still held the profile answered 200 and set the instance ID to the empty string. That same branch also never broke out of the retry loop, so even a successful first registration always sent a second PUT before the 200 path ended it.

Other changes

  • webui_context.Init returns an error now, since it builds the heartbeat runner; NewApp forwards it.
  • WebuiApp carries the context.Context and context.CancelFunc the heartbeat runs on. Terminate cancels it.

There is no nil-client guard to add here: the webconsole builds its single NFManagementClient once in Init, so it is never nil.

New configuration IE (optional):

# fallback heartbeat interval in seconds (1~3600), used only until the NRF
# assigns one; the NRF value always takes precedence
nfHeartBeatTimer: 10

Testing

Neither webui_context nor backend/factory had tests. This PR adds the harness along with the heartbeat coverage. The NRF calls read package globals, so the harness owns those globals for the duration of a test and restores them afterwards; no mocking framework is introduced.

  • Unit tests cover the wiring rather than the runner internals, which the util PR covers: registration seeding the timer, the loop sending the PATCH through the real transport, the 404 handshake re-registering with a PUT and re-arming on the interval the re-registration returned, and the fallback interval coming from the config.
  • SendUpdateNFInstance is tested for the 200 with profile, the 204, the ProblemDetails extraction and the missing nrfUri; SendNFRegistration for the 201 and 200 handling, the retry until success, the return on context cancellation, and the exhausted MaxRetryAttempts; SendDeregisterNFInstance for the 204 and the 404 with its ProblemDetails.
  • Config tests cover the default and the range validation.
  • The webui_context tests read unexported state, so they are in-package and named *_internal_test.go, which is what the repository testpackage linter expects; the factory test is external, in package factory_test.
  • The NRF is mocked with gock, promoted from an indirect to a direct dependency, so the suite needs no network, and the timing-dependent cases run on the testing/synctest fake clock rather than real sleeps.
{
    _id: ObjectId('6ab15cd924a2cfb3f3677ed8'),
    heartBeatTimer: 10,
    plmnList: [ { mcc: '208', mnc: '93' } ],
    customInfo: { AfType: 'webconsole', oauth2: false },
    nfInstanceId: 'aeb27f21-2d6a-45a9-9824-a734e3ec5e6d',
    nfType: 'AF',
    nfStatus: 'REGISTERED',
    lastHeartBeat: '2026-09-21T16:35:47Z'
  }

This work is sponsored by Free Mobile!

Send an NFUpdate PATCH with nfStatus REGISTERED at the interval the NRF
returns, per 3GPP TS 29.510 clause 5.2.2.3.2. Re-adopt heartBeatTimer
from every answer. Re-register on 404 or after three consecutive
failures.

The webconsole registers as an AF, and the NRF sweep that suspends
silent instances filters on nfStatus and lastHeartBeat alone, so the AF
profile goes stale like any other without this.

The heartbeat loop lives in the util nfheartbeat package; the webui
context only supplies the PATCH and re-registration transport.

The nfHeartBeatTimer config option only sets the fallback interval. The
NRF value always wins.

SendNFRegistration takes a context and an applyOAuth2 flag. The retry
wait is interruptible, so a shutdown during an NRF outage no longer
sleeps through it, and OAuth2Required is written only by the startup
registration: the request handlers read it concurrently, so the
re-registration from the heartbeat goroutine must not race them.

Also drop the instance ID parsed from the register response Location:
the generated client only fills it on 201, so a re-registration against
an NRF that still holds the profile was overwriting NfInstanceID with an
empty string. That branch also never broke out of the retry loop, so a
successful registration always sent a second PUT.

Init returns an error now, since it builds the heartbeat runner, and the
app carries a context that Terminate cancels before waiting for the
heartbeat goroutine and deregistering.
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