ci: harden flaky suite checks (opcua A&C, action-status, diagnostic_bridge, operation_handlers, type_info, docs linkcheck)#504
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR makes the secured OPC-UA Alarms & Conditions integration path deterministic under load by eliminating a startup ordering race between the OPC-UA poller’s one-shot alarm reporting and DDS discovery of the fault_manager ReportFault service.
Changes:
- Add a bounded (10s) best-effort wait for
/fault_manager/report_faultdiscovery before starting the OPC-UA poller. - Warn when the fault sink is not discovered within the timeout so operators understand alarms may be dropped until it appears.
bburda
reviewed
Jul 6, 2026
bburda
left a comment
Collaborator
There was a problem hiding this comment.
Additional findings outside the diff:
list_faultsinros2_medkit_action_status_bridge/test/test_integration.test.pystill doesreturn future.result().faultswith no None guard. This PR adds that guard to the identical method in the diagnostic_bridge test. On a 5s ListFaults timeoutfuture.result()is None, soNone.faultsraises AttributeError insidewait_for_fault's loop instead of retrying. It is not part of this diff, but the same fix applies.
The secured Alarms and Conditions integration test flaked on loaded runners because the plugin started its poller before the ReportFault service client had matched fault_manager over DDS. AlarmCondition notifications are one-shot and the report is a fire-and-forget async request, so one emitted before that match is dropped with no retry and the alarm never surfaces. Wait (bounded) for the fault sink to be discovered before starting the poller so the first report cannot be lost.
The wait_for_service timeout was duplicated as a literal 10s in the warning text, so changing it would leave the log stale. Extract it into a named constant and build the message from it, keeping value and text in sync. No behaviour change.
…ault The aborted-goal integration test published the ABORTED status before the bridge had resolved this node's name over DDS, so under discovery lag the fault source froze to the action-name fallback and the reporting_sources assertion flaked. Wait for the bridge's subscription with a resolved node name before publishing, so the first report resolves the server FQN.
The ros.org REP pages intermittently exceed the linkcheck read timeout in CI (rep-0149 hit a 30s read timeout), failing the docs build on a transient external blip. Add the host to linkcheck_ignore and bump linkcheck_retries.
ListExecutionsReturnsTrackedActionGoal flaked under load when a graph-event refresh_cache() reconciled the manually seeded component away between the seed and the handler read, so list_executions returned ENTITY_NOT_FOUND. Pin discovery.refresh_debounce_ms at its 60s maximum so only the startup refresh runs for the test's lifetime, and gate seeding on that refresh (cache generation advance) plus service discovery rather than a fixed sleep.
A diagnostic published before the bridge subscription matched (volatile QoS drop), or before the bridge's ReportFault client discovered fault_manager (send_report drops a not-ready fire-and-forget request), was lost with no retry, so a single publish plus a fixed sleep raced both discovery hops. Wait for the bridge subscription to match this publisher, then re-emit the stimulus and poll until the fault lands.
An operation is listed by name as soon as it is discovered, before its interface type is resolved and type_info is populated, so reading the operation list once raced type resolution and intermittently found no schema. Add wait_for_operation_type_info and use it in the action and service type_info checks.
…second line The three docstrings added for the deterministic diagnostic_bridge test kept their summary on the opening line, which ament_pep257 flags as D213. Move each summary to the second line (blank first line) to match the style already used in the sibling bridge test docstrings.
The executor thread allocates the shared response in GenericClient::create_response() while the caller releases the last shared_ptr after future::get(); libstdc++ future happens-before is invisible to TSan, so it flags the control-block refcount decrement and delete. Surfaced by test_operation_handlers once the service call runs deterministically. Matches the rclcpp allocation frame only.
unsubscribe() and the subscribe() replace path destroyed the GenericSubscription while the MultiThreadedExecutor could still be dispatching its callback, so an executor worker freed the closure's captured strings and unloaded the type support under an in-flight deserialize (TSan use-after-free). Retire subscriptions to a graveyard and destroy them only in shutdown(), after the executor has stopped.
test_operation_handlers intermittently races on the GenericClient response buffer the std::future contract hands us after get(); all racing writes live in rclcpp/libstdc++. The adjacent librclcpp create_response frame is often unsymbolized, so match our reliably-symbolized OperationManager::call_service frame instead.
The deferred-destruction change hung gateway shutdown on the lyrical build-and-test leg: retired subscriptions stayed registered with the executor, so spin() did not return and the peer gateway was SIGKILLed (-9), failing test_peer_aggregation's exit-code check. Restore the original teardown; the trigger-subscriber TSan race needs a shutdown-safe fix instead. Keeps the call_service TSan suppression.
Skip only the one slow REP URL instead of the whole ros.org/reps host so other REP links stay checked, and revert the now-inert retries bump.
Return an empty list when the ListFaults call times out, mirroring the guard added to the diagnostic_bridge integration test.
f7b0ff6 to
3d7e521
Compare
bburda
previously approved these changes
Jul 7, 2026
Drop the 10s wait_for_service in set_context. Buffer report/clear dispatches (order-preserving, bounded to 256) and flush them on each poll once the fault_manager service is matched, so a late sink still receives the one-shot alarm without stalling startup.
The suppression must match our call_service frame, not a library scope: called_from_lib:librclcpp.so does not catch the report (TSan reports it with our stack as primary, librclcpp is not a caller) and fails sanitizer-tsan. Drop the stale fixed-at-root note; that trigger-subscriber fix was reverted.
3d7e521 to
51c2f9f
Compare
bburda
approved these changes
Jul 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR hardens the shared test suite's flaky checks so they pass deterministically.
Each runs in the common CI suite, so every one was intermittently failing CI on
unrelated open-core PRs this cycle. None is a widened timeout or a re-run; each
addresses the root-cause race.
Hardened checks (one-line root cause each)
emitted before the
ReportFaultclient matched fault_manager over DDS, so itwas dropped and the alarm never surfaced.
was published before the bridge had resolved the server node FQN, freezing the
fault source to the action-name fallback.
refresh_cache()reconciled the manually seeded component away between the seedand the handler read, so
list_executionsreturned ENTITY_NOT_FOUND.subscription matched (volatile QoS drop), or before the bridge's
ReportFaultclient discovered fault_manager (
send_reportdrops a not-ready fire-and-forgetrequest), was lost with no retry.
listed by name before its interface type resolves, so its
type_infoschema wasintermittently absent when the assertion ran.