Lifecycle Manager
dc_lifecycle_manager is DC's nav2-style manager for the nodes that have a meaningful
deactivated state. It walks each managed node through the ROS 2 managed-node state
machine (configure,
activate, deactivate, cleanup, shutdown), watches a bond heartbeat on every node
it has activated, and can bring the whole managed set up automatically at launch. See
Concepts for the general nav2_util
LifecycleNode background this page builds on.
The Bridge (dc_bridge) is deliberately not one of the managed nodes
(ADR-0006): it
has no meaningful deactivated state, so its readiness is a launch-ordering problem
instead — see the boundary and autostart flow
below.
What it manages
The set of managed nodes is the node_names parameter, walked in list order for
bring-up (configure then activate, each transition applied to every node before the
next one starts) and in reverse order for teardown. In every params file this repo
ships — dc_bringup.launch.py's inline lifecycle_manager_params and both E2E harness
params files — that list is:
lifecycle_manager_dc:
ros__parameters:
node_names: ["measurement_server"]
transitions: [configure, activate]
measurement_server is the only node under lifecycle management today.
group_server, when enabled, is launched as a plain Node alongside it — not added to
node_names — so it is not lifecycle-managed. Nothing stops a future node_names entry
from adding it if it ever needs a deactivated state; the manager is already list-driven
for exactly that reason.
The boundary and the autostart flow
dc_bringup.launch.py brings the pipeline up in a fixed order
(ADR-0006) so that no
Record can be emitted before the Bridge can accept it. The Bridge, its Vector Shipper
child, and the bridge_ready_gate process that polls the Bridge's ~/ready
(std_srvs/Trigger) service all run outside lifecycle_manager_dc — only once the
gate exits 0 does the lifecycle manager exist at all in the launch graph.
flowchart TB
subgraph outside["Outside the lifecycle manager (ADR-0006)"]
direction TB
vector["Vector<br/>(Shipper child process)"]
bridge["dc_bridge<br/>(plain node, launch respawn)"]
gate["bridge_ready_gate<br/>(polls ~/ready)"]
bridge -- "spawns and supervises" --> vector
bridge -- "~/ready (std_srvs/Trigger)" --> gate
end
subgraph managed["Managed by lifecycle_manager_dc"]
direction TB
lm["lifecycle_manager_dc<br/>(autostart=true by default)"]
meas["measurement_server<br/>(only entry in node_names)"]
lm -- "configure, then activate" --> meas
meas -- "bond heartbeat (10 Hz)" --> lm
end
gate -- "exit 0: Bridge ready" --> lm
gate -. "exit != 0: never ready" .-> abort["Shutdown(reason=...)<br/>whole launch aborts"]
- Bridge first.
dc_bridgestarts as a plain node and spawns Vector as a supervised child process. - Readiness gate.
bridge_ready_gateblocks until~/readyanswerssuccess=True(Vector accepting connections on its ingest socket), or its owntimeout_s(default 120 s) expires. - Activation, only on success.
dc_bringup.launch.pyregisters anOnProcessExithandler on the gate: exit0startslifecycle_manager_dc; any other exit code shuts the whole launch down (Shutdown(reason=...)) instead of leaving collection nodes running against a Bridge that never came up. - Autostart.
dc_bringup.launch.py'sautostartlaunch argument defaults toTrueand is always passed through explicitly — overriding the code's own default offalsefor a manager launched some other way. Withautostart=true,lifecycle_manager_dccalls its ownstartup()as soon as it has constructed lifecycle service clients for every managed node, with no external service call needed.autostart=falseleaves the managed set inUnconfigureduntil something calls the manager's~/manage_nodesservice (nav2_msgs/srv/ManageLifecycleNodes, commandSTARTUP).
State transitions and bond heartbeats
Each managed node moves through the same five primary states nav2 uses. The manager's
five service-level operations (startup, shutdown, reset, pause, resume) are
each just a sequence of these per-node transitions applied across every managed node —
pause/resume reuse the same deactivate/activate edges startup uses, not
separate ones.
A bond (10 Hz heartbeat, bond_timeout — 10 s in dc_bringup.launch.py, 4 s
default otherwise) is created for a node the moment it activates and torn down the
moment it deactivates. The manager polls every bond every 200 ms; a missed heartbeat
past bond_timeout is treated as that node having crashed.
stateDiagram-v2
[*] --> Unconfigured
Unconfigured --> Inactive: configure
Inactive --> Active: activate / resume
Active --> Inactive: deactivate / pause
Inactive --> Unconfigured: cleanup
Unconfigured --> Finalized: shutdown
Finalized --> [*]
Active --> Unconfigured: bond heartbeat lost -> reset(hard_reset=true)
Unconfigured --> Active: node reachable again within\nbond_respawn_max_duration -> startup()
A lost heartbeat does not just deactivate the one node that crashed — checkBondConnections
hard-resets every managed node (deactivate then cleanup, continuing past
per-node failures since hard_reset=true) and clears all bonds, on the principle that a
half-alive managed set is worse than a fully torn-down one. If
attempt_respawn_reconnection (default true) is set, a 1 s-period timer then polls
whether every managed node's lifecycle service is reachable again:
- Reachable within
bond_respawn_max_duration(default 10 s): the manager callsstartup()again — a fullconfigure+activatepass — and resumes normal operation, bonds included. - Still unreachable once
bond_respawn_max_durationelapses: the manager gives up and leaves the managed setUnconfigured. Recovery from there needs an explicitSTARTUPcall to~/manage_nodes(or a relaunch).
~/is_active (std_srvs/srv/Trigger) reports whether the managed set is currently
Active, and a diagnostic_updater entry surfaces the same status on /diagnostics.