Runtime Introspection — Design Note¶
Audience :class: note
Contributors and advanced readers evaluating future directions of
lifecore_ros2. This is a design note, not committed user
documentation. No code under src/lifecore_ros2/ exists for this feature
yet.
Status
Draft — gated on §4 (concurrency) and §5 (strict lifecycle contract) being green. See Prerequisite gates.
Intent¶
Provide a small, read-only API to answer three questions from outside the node:
Which components are currently registered with this
lifecore_ros2.LifecycleComponentNode?What is the lifecycle state of the node and of each component?
Which ROS resources (publishers, subscribers) does each component currently hold?
Goal: support diagnostics, debugging, and external orchestration without
forcing callers to reach into private attributes (_components,
_registration_open, _resources).
Proposed contract¶
Surface added to lifecore_ros2.LifecycleComponentNode. All methods are
read-only and side-effect free.
class LifecycleComponentNode:
def list_components(self) -> tuple[str, ...]:
"""Return the names of currently registered components.
Order is registration order. Returned tuple is a snapshot; it does
not track subsequent registrations.
"""
def get_component_state(self, name: str) -> State:
"""Return the rclpy lifecycle ``State`` of the named component.
Reads through to ``rclpy``; never returns a library-cached value.
Raises ``ComponentNotFoundError`` if the name is unknown.
"""
def is_registration_open(self) -> bool:
"""Return whether ``add_component`` would currently be accepted.
Equivalent to: "no lifecycle transition has occurred yet".
"""
Surface added to lifecore_ros2.LifecycleComponent. Optional, opt-in
per concrete component.
class LifecycleComponent:
def describe_resources(self) -> Mapping[str, str]:
"""Return a name -> ROS topic/type description of held resources.
Default implementation returns an empty mapping. Concrete components
(e.g. ``LifecyclePublisherComponent``) override to expose their
topic, message type, and QoS profile name. Must not return live
handles to the underlying rclpy objects.
"""
Return-type discipline¶
Snapshots only.
list_componentsreturns atuple, never the live internal collection.No live handles.
describe_resourcesreturns descriptions (strings / dataclasses), never the rclpy publisher or subscriber object.Lifecycle state is always read from rclpy at call time. No library field shadows the node’s lifecycle state machine.
Invariants preserved¶
Single source of truth. rclpy is the source of truth for lifecycle state; the node’s
_componentsregistry is the source of truth for the set of managed entities. Introspection reads through these — it never caches, mirrors, or precomputes.No parallel state machine. Reading
get_component_statemust not build any state outside whatrclpyalready exposes. (See Architecture — Core principle: native ROS 2 lifecycle semantics stay in control.)Transparent component state (lifecycle component contract): the introspection surface only exposes what is already conceptually visible; it does not promote private fields to public.
No ghost entries. Introspection must never observe a component that is not fully registered. Implementation reads under the existing
threading.RLockdocumented in Architecture — Concurrency Contract.Registration gate is read-only-friendly.
is_registration_openis a pure read; calling it never closes the gate or otherwise mutates state.Public API stability. New symbols are additive.
__all__inlifecore_ros2is extended, not reordered or pruned.
Prerequisite gates¶
This note assumes the following are already in place:
§4 — Architecture Concurrency Contract: defines the
threading.RLockand the single-threaded executor ADR. Introspection reuses that lock; it does not introduce a new synchronisation primitive.§5 — Architecture Strict direct-call contract: guarantees that observable state is coherent (no half-configured components leaking through a failed transition). Introspection’s correctness depends on this rollback guarantee.
§6 — Test coverage for lifecycle walks and concurrent registration: any introspection implementation reuses these tests as the baseline observable behavior.
Implementation must not be opened until these gates are still green at the time of work.
Open questions¶
These are explicitly unresolved. They must be answered in the implementation PR, not silently in code:
Component-side state accessor. Should
LifecycleComponentexpose astateproperty mirroringget_component_state(name), or should callers always go through the node? Tentative: through the node only, to avoid duplicating the same read in two places.Iteration vs. snapshot. Is
tuple[str, ...]sufficient, or should we offeritems() -> Mapping[str, LifecycleComponent]? Returning component instances exposes more surface; weigh against the “transparent component state” invariant.Resource description schema. Free-form
Mapping[str, str]vs. a typed dataclass (ResourceDescriptor(kind, topic, msg_type, qos))? Typed is friendlier but locks the schema early.Behavior during shutdown. After
on_shutdown, shouldlist_componentsreturn()or the last known set? Tentative: last known set, untildestroy_node; behavior afterdestroy_nodeis undefined.Error type. Reuse
lifecore_ros2.LifecoreErrorhierarchy with a newComponentNotFoundError, or rely onKeyError? Consistency with existingComponentNotAttachedErrorsuggests a typed error. Resolved by Dynamic Components — Design Note — a typedComponentNotFoundError(LifecoreError, KeyError)is introduced there and reused here.Thread-safety guarantee under spin. The current
RLockcovers registration; reads from a separate thread duringspinare believed safe but not exercised by tests. Confirmation requires a regression test in the implementation PR.
Non-goals¶
No write API. Nothing here removes, replaces, or reconfigures components. Runtime mutation belongs to the Dynamic components design note.
No event stream. Introspection is pull-based. Push notifications, subscriptions to lifecycle changes, and tracing belong to the Observability design note.
No new dependency.
pyproject.tomlis unchanged.No exposure of rclpy internals. Live handles to publishers, subscribers, or the state machine object are out of scope.
No ROS service / topic facade. Introspection is a Python API on the node instance, not a ROS-graph-level interface.
No promotion of private attributes.
_components,_registration_open,_resources, and_lockremain private.