Sprint 6 - Unified callback gating¶
Objective. Extract activation-gating logic into a shared, reusable utility. Remove duplication across publisher, subscriber, service, client, and timer components.
Deliverable. Activation gating is consistent everywhere; no duplicate logic.
—
Decisions locked (planning complete)¶
Architecture:
New module
lifecore_ros2/core/activation_gating.pyholds the shared primitive:def require_active(is_active: bool, *, component_name: str) -> None: """Raise RuntimeError if not active."""
LifecycleComponent.require_active()is a convenience façade that calls the primitive withself._is_activeandself._name. No logic of its own.@when_activedefault-raise path is refactored to call the primitive instead of its own inline raise. Drop paths (when_not_active=Noneandwhen_not_active=callable) are unchanged.LifecycleServiceServerComponent._on_request_wrapperreplaces itsif not self._is_active:guard withtry: self.require_active() / except RuntimeError:. Warning log and annotated default response are preserved exactly.No raw
if not self._is_active:check remains in any component file outsideLifecycleComponentinternals.is_activeremains the existing property. Nois_active()method added.Node-level “fully active” terminology stays out of scope.
Inactive policy per site (preserved exactly):
Site |
Inactive policy |
|---|---|
|
|
|
|
|
silent drop + debug log |
|
silent drop + debug log |
|
warning log + default response |
Components covered¶
All five gating sites delegate to the shared primitive:
LifecyclePublisherComponentpublish pathLifecycleSubscriberComponentmessage callbackLifecycleServiceServerComponentrequest callbackLifecycleServiceClientComponentcall / call_async / wait_for_serviceLifecycleTimerComponenttick callback
—
Validation¶
[x]
require_active(False, component_name="x")raisesRuntimeError("Component 'x' is not active").[x]
require_active(True, component_name="x")returnsNone.[x]
LifecycleComponent.require_active()delegates to the primitive without added logic.[x]
@when_activedefault-raise path produces the same error message as before.[x] No raw
if not self._is_active:check remains in component files.[x] Service-server inactive behavior (warning log + annotated default response) is preserved.
[x] Gated callbacks (subscriber, timer) silently drop when inactive.
[x] Existing tests pass without semantic changes.
—
Risks and mitigation¶
Risk: behavior-preserving refactor changes semantics. Keep old behavior as the golden standard and compare component-by-component.
Risk: overgeneralized gating hides component-specific behavior. Keep the shared utility small; component-specific inactive behavior stays in the component.
—
Dependencies¶
Requires: publisher, subscriber, timer, service, and client component surfaces.
Requires: error handling rules from Sprint 2.
Requires: testing support from Sprint 3.
—
Scope boundaries¶
In scope:
core/activation_gating.pywith therequire_activeprimitiveLifecycleComponent.require_active()façade@when_activedefault-raise path refactored to the primitiveLifecycleServiceServerComponent._on_request_wrapperrefactored totry/except RuntimeErroronself.require_active()behavior-preserving unit and regression tests
Out of scope:
node-level “fully active” terminology
is_active()method (is_activeremains a property)new gating modes or conditional application-specific gating
changes to
when_not_active=None/when_not_active=callablepathsexample updates (public lifecycle semantics unchanged)
performance work unless a regression is measured
—
Success signal¶
[x]
require_active(),@when_active, and the service-server inactive handler all rely on the same shared activation check.[x] Each component preserves its existing inactive policy.
[x] No raw
if not self._is_active:remains outsideLifecycleComponentinternals.[x] Ruff, Pyright, and pytest are green.