Skip to content

Add non-blocking BBMD registration API; drop the self-poll fallback from #66 - #67

Merged
iain-anderson merged 2 commits into
v1.2-iotech-branchfrom
fix/bbmd-async-registration-api
Sep 23, 2026
Merged

iain-anderson merged 2 commits into
v1.2-iotech-branchfrom
fix/bbmd-async-registration-api

Conversation

@bcorrion

Copy link
Copy Markdown

Summary

Follow-up to #66. That PR's fallback -- polling bip_receive() directly when the normal pthread_cond_timedwait() signal never arrives -- fixed bacnet-sim's real deadlock, but Copilot correctly flagged two problems with it after merge: it can race an already-running receive thread for datagrams on the same socket, and any ordinary NPDU it wins that race for is silently dropped instead of reaching the caller's normal dispatch path (bip_receive() only invokes bvlc_handler(), never the caller's npdu_handler()).

Rather than further narrow that race, this removes it entirely and gives the one caller that actually needs non-blocking behavior (bacnet-sim, single-threaded, calling this synchronously from main() before it has any receive loop running) a real non-blocking API instead, which needs no fallback because it never blocks in the first place.

  • bvlc_register_with_bbmd() reverts to a plain bounded pthread_cond_timedwait() -- unchanged in behavior from before Fix bvlc_register_with_bbmd() reporting FAILED on real success #66's self-poll fallback existed (the mutex/cond static-initializer fix from Fix bvlc_register_with_bbmd() reporting FAILED on real success #66 is kept; that was a genuine, independent lifecycle bug). This is correct for any caller with its own receive loop already running before it registers -- confirmed directly against device-bacnet-c-private's receive_data(), which starts its thread before ever calling register_as_foreign_device() -- and is the only remaining consumer of this function.
  • New bvlc_register_with_bbmd_async() + bvlc_bbmd_registration_status(): sends the request and returns immediately; the caller starts its own receive loop right after and polls the status once it's running. Never itself reads the socket, so it can never compete with anything else that does.
  • New dlenv_register_as_foreign_device_async() and dlenv_maintenance_timer_async() mirror the existing blocking pair for a caller using the async API throughout -- covering both the initial registration and periodic TTL renewal, so a single-threaded caller doesn't hit the same blocking wait on renewal that it avoided at startup.

Test plan

🤖 Generated with Claude Code

bvlc_register_with_bbmd()'s self-poll fallback (added to fix bacnet-sim's
registration deadlock) had two real problems Copilot correctly flagged:
it can race an already-running receive thread for datagrams on the same
socket, and any ordinary NPDU it wins that race for is silently dropped
instead of reaching the caller's normal dispatch path. Rather than try
to further narrow that race, remove it: bacnet-sim (the only known
single-threaded caller) is moving to a new non-blocking API instead,
which needs no such fallback because it never blocks in the first place.

- bvlc_register_with_bbmd() reverts to a plain bounded
  pthread_cond_timedwait(), unchanged in behavior from before the
  self-poll fallback existed. This is correct for any caller with its
  own receive loop already running before it registers -- confirmed
  against device-bacnet-c-private's receive_data(), which does exactly
  that -- and is the only remaining consumer of this function.
- New bvlc_register_with_bbmd_async() + bvlc_bbmd_registration_status():
  sends the request and returns immediately; the caller starts its own
  receive loop right after and polls the status once it's running. Never
  itself reads the socket, so it can never compete with anything else
  that does.
- New dlenv_register_as_foreign_device_async() and
  dlenv_maintenance_timer_async() mirror the existing blocking pair for
  a caller using the async API throughout -- covering both the initial
  registration and periodic TTL renewal, so a single-threaded caller
  doesn't hit the same blocking wait on renewal that it avoided at
  startup.

Verified against a real running mini-bbmd with a bacnet-sim built from
this patched stack: initial registration logs "SUCCEEDED to Register
with BBMD" (previously "FAILED" before any of these fixes existed), and
with BACNET_BBMD_TIMETOLIVE forced to 5s, four consecutive renewal
cycles over 15s each succeeded -- independently confirmed by mini-bbmd's
own log showing four separate RegisterForeignDevice events 5 seconds
apart, matching bacnet-sim's four "SUCCEEDED" logs exactly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved moderate findings remain around BDT setup preservation, async success reporting, and non-BIP API availability.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 Medium severity · 1 Low severity

Open (3)
What changed in this PR

This pull request adds non-blocking BBMD registration and renewal APIs while removing the socket-polling fallback from synchronous registration.

Changes:

  • Adds asynchronous BBMD registration and status APIs.
  • Adds asynchronous datalink registration and maintenance helpers.
  • Restores synchronous registration to condition-variable waiting.
File Summary
src/​bacnet/​datalink/​dlenv.h Declares asynchronous datalink APIs.
src/​bacnet/​datalink/​dlenv.c Implements asynchronous registration and renewal.
src/​bacnet/​basic/​bbmd/​h_bbmd.h Declares asynchronous BBMD APIs and status reporting.
src/​bacnet/​basic/​bbmd/​h_bbmd.c Implements asynchronous registration and restores blocking wait behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/bacnet/datalink/dlenv.c Outdated
Comment thread src/bacnet/datalink/dlenv.c Outdated
Comment thread src/bacnet/basic/bbmd/h_bbmd.h Outdated
- dlenv_register_as_foreign_device_async() was entirely inside
  #if defined(BACDL_BIP), so it didn't exist at all for BACDL_MSTP/
  BACDL_BIP6 builds despite being declared unconditionally in the
  header -- an undefined reference for any non-BIP consumer. Match
  dlenv_register_as_foreign_device()'s existing pattern: guard the body,
  not the signature/return, so it's a real no-op (returns 0) outside
  BACDL_BIP instead of not existing.
- dlenv_maintenance_timer_async() always returned true once the TTL
  countdown hit zero, regardless of whether
  dlenv_register_as_foreign_device_async() actually sent anything (it
  returns 0 if unconfigured, -1 if the send failed). A caller polling
  bvlc_bbmd_registration_status() on a false positive would wait
  forever for a status that will never change. Return type is now int,
  matching dlenv_register_as_foreign_device_async()'s own contract
  exactly, and callers (bacnet-sim's main.c) updated to match: a
  negative return reports failure immediately (nothing to poll for),
  same as a hard failure at startup.
- Reworded a stale comment in h_bbmd.h referring to
  bvlc_register_with_bbmd()'s "self-polling fallback" -- that code path
  was removed by this same PR, so the comment described something that
  no longer exists in the function it was describing.

Verified against a real running mini-bbmd: initial registration still
logs "SUCCEEDED to Register with BBMD", and with
BACNET_BBMD_TIMETOLIVE forced to 5s, four consecutive renewal cycles
over 15s still succeeded, cross-confirmed by mini-bbmd's own log
showing four separate RegisterForeignDevice events 5 seconds apart.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Unresolved async state, routing, configuration, and documentation issues remain.

Review effort: Lite
Findings: None

Resolved since last review (3)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Correlate BBMD registration results with the pending request

src/​bacnet/​basic/​bbmd/​h_bbmd.c:1217

This new status API is not scoped to an async registration request: the BVLC_RESULT handler updates bbmd_reg for every result, including results received before any registration call. A late reply from a previous renewal or an unrelated BBMD operation can therefore make this report SUCCEEDED/FAILED for the current request, after which the real reply is ignored because the handler only writes while the state is UNSET. Track a pending registration/generation and correlate the reply before publishing this status.

@iain-anderson iain-anderson left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@iain-anderson
iain-anderson merged commit a46c262 into v1.2-iotech-branch Sep 23, 2026
1 check passed
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.

3 participants