Sprint 8 - Concurrency infrastructure¶
- Status:
Archived / Completed
- Completed in:
Unknown
- Outcome:
See sprint body.
- Follow-ups:
See docs/planning/backlog.rst if applicable.
Objective. Support safe multi-threaded callback execution and formalize the callback-group contract.
Deliverable. Applications can use multi-threaded executors without manual choreography beyond documented ROS 2 callback-group choices.
—
Decisions already made¶
The library should support ROS 2 callback groups without hiding ROS 2 executor semantics.
Callback groups are borrowed resources when supplied by the application.
Component lifecycle state must remain coherent when callbacks and lifecycle transitions overlap.
Concurrent transitions should fail deterministically instead of racing.
Scope¶
The sprint covers callback-group binding, documented concurrency expectations, and a safety audit of lifecycle state access.
Verify or update that:
component registration and lifecycle transitions are protected
lifecycle state reads are coherent
concurrent transitions fail deterministically
callbacks cannot corrupt component lifecycle state
Thread-safe cleanup expectations¶
Clarify what happens when cleanup, deactivation, or destruction overlaps with in-flight callbacks. Prefer documented constraints and deterministic errors over implicit best effort.
To decide during sprint planning¶
Decisions resolved (2026-05-07):
C1 — Callback-group helper API.
LifecycleComponentNode.get_or_create_callback_group(component_name, group_type=None)
is implemented. The helper is idempotent per (component_name, group_type) pair.
The caller-owned pattern remains fully supported: a component constructed with an
explicit callback_group uses that group as-is.
C2 — Default group type.
MutuallyExclusiveCallbackGroup is the default. ReentrantCallbackGroup must be
requested explicitly. Rationale: the default must protect component-internal state
against accidental reentrancy under MultiThreadedExecutor; intra-component
parallelism is an application decision, not a framework surprise.
C3 — ``_is_active`` locking.
_is_active is protected by _active_lock: threading.Lock on every
LifecycleComponent instance. All reads in the public API (is_active property,
require_active(), @when_active gate) and all writes in the framework entry
points (on_activate, on_deactivate, on_cleanup, on_shutdown,
on_error, _rollback_failed_configure) acquire _active_lock.
The concurrency contract does not depend on CPython’s GIL.
In-flight callback policy.
At the @when_active gate, _is_active is snapshotted under _active_lock
at gate entry. A callback in flight when deactivation commits will complete its
current execution but will not be re-invoked. No runtime fence is provided; this
is a “drop at next gate” policy, documented here and in component docstrings.
—
Validation¶
[x] Callback group creation is idempotent per component name.
[x] Different component names receive distinct groups when requested.
[x] Component constructors pass callback groups to ROS resource creation.
[x] Concurrent lifecycle transitions fail with the expected typed error.
[x] Component activation state remains coherent across threads.
[x] Deactivate prevents new gated callbacks while respecting in-flight work.
—
Risks and mitigation¶
Risk: lock contention. Document the intended executor model and measure only if a real bottleneck appears.
Risk: callbacks trigger lifecycle transitions. Enforce the existing reentrant-transition rules and add regression tests.
Risk: use-after-cleanup behavior. Keep cleanup and callback gating explicit; do not promise arbitrary concurrent destruction unless tests prove it.
—
Dependencies¶
Requires: Sprint 6 gating semantics.
Requires: Sprint 7 cleanup ownership semantics.
Requires: Sprint 3 testing helpers.
Requires: existing error handling contract.
—
Scope boundaries¶
In scope:
callback group helper design
component constructor support
concurrency contract documentation
focused concurrency tests
Out of scope:
custom executor implementation
lock-free data structures
arbitrary multi-executor topology support
—
Success signal¶
[x] Multi-threaded use has a documented, test-backed contract.
[x] Callback groups are easier to wire without hiding ROS 2 semantics.
[x] Existing lifecycle guarantees remain intact.
—
Shipped — 2026-05-08¶
Test coverage: 13 new tests (10 callback-group helper + 3 _is_active thread-safety
regressions), on top of 6 pre-existing concurrent-transition tests.
Decisions locked:
C1:
LifecycleComponentNode.get_or_create_callback_group(component_name, group_type=None)— idempotent per(name, type)pair, protected by the existingthreading.RLock.C2: Default group type is
MutuallyExclusiveCallbackGroup;ReentrantCallbackGroupmust be requested explicitly.C3:
_is_activeprotected by_active_lock: threading.Lockon everyLifecycleComponent; contract is GIL-independent.In-flight policy: “drop at next gate” — a callback that passed the
@when_activegate before deactivation commits completes its current execution and is not re-invoked.
Commitments:
src/lifecore_ros2/core/lifecycle_component_node.py:_callback_groupsregistry,get_or_create_callback_grouphelper.src/lifecore_ros2/core/lifecycle_component.py:_active_lock, all_is_activeread/write sites wrapped,@when_activegate snapshot,_on_deactivatein-flight policy documented in docstring.tests/core/test_callback_group_helper.py: 10 helper tests (idempotency, type conflict, thread safety).tests/core/test_regression_active_lock.py: 3 tests (in-flight visibility, post-deactivate coherence, post-cleanup coherence).docs/architecture.rst: thread-safety table extended, new_is_activethread safety and in-flight policy subsection, invariant entries updated.docs/patterns.rst: newget_or_create_callback_grouppattern entry.Class docstrings updated on both
LifecycleComponentNodeandLifecycleComponent.
Next phases ready for:
Observability (Sprint 9) — concurrency contract is now explicit and tested.
Health status (Sprint 10) —
_is_activereads are thread-safe.Any
MultiThreadedExecutorapplication — callback group contract is documented.