[events/EventBus] TDD: EventBus protocol and implementations lack unsubscribe() — handler references prevent garbage collection #10354

Open
opened 2026-04-18 09:06:24 +00:00 by HAL9000 · 0 comments
Owner

Metadata

  • Commit: feat(events): add unsubscribe() to EventBus protocol and implementations
  • Branch: fix/events-eventbus-unsubscribe

Background and Context

The EventBus protocol (src/cleveragents/infrastructure/events/protocol.py) and both implementations (ReactiveEventBus, LoggingEventBus) lack an unsubscribe() method. Once a handler is registered via subscribe(), it cannot be removed. This causes memory leaks in long-running processes because handler callables (typically bound methods) hold references to their owning objects, preventing garbage collection even after the owning component is shut down.

Summary

Write a failing test (tagged @tdd_issue, @tdd_issue_N, @tdd_expected_fail) that verifies both ReactiveEventBus and LoggingEventBus support unsubscription.

Scenario

@tdd_issue @tdd_issue_N @tdd_expected_fail
Scenario: ReactiveEventBus supports unsubscribing a handler
  Given a ReactiveEventBus instance
  And a handler registered for PLAN_CREATED events
  When the handler is unsubscribed
  And a PLAN_CREATED event is emitted
  Then the handler is NOT called

@tdd_issue @tdd_issue_N @tdd_expected_fail
Scenario: LoggingEventBus supports unsubscribing a handler
  Given a LoggingEventBus instance
  And a handler registered for PLAN_CREATED events
  When the handler is unsubscribed
  And a PLAN_CREATED event is emitted
  Then the handler is NOT called

@tdd_issue @tdd_issue_N @tdd_expected_fail
Scenario: Unsubscribed handler does not prevent garbage collection
  Given a ReactiveEventBus instance
  And a component with a bound method handler registered for PLAN_CREATED events
  When the handler is unsubscribed
  And the component reference is deleted
  Then the component is garbage collected (no strong references remain in the bus)

Expected Failure Reason

The tests will fail because neither ReactiveEventBus nor LoggingEventBus has an unsubscribe() method. Calling bus.unsubscribe(event_type, handler) will raise AttributeError.

Fix Path

  1. Add unsubscribe() to the EventBus protocol in protocol.py
  2. Implement unsubscribe() in ReactiveEventBus and LoggingEventBus
  3. Consider using weakref for handler storage to automatically clean up when owning objects are garbage collected

Example implementation:

def unsubscribe(self, event_type: EventType, handler: Callable[[DomainEvent], None]) -> None:
    handlers = self._subscriptions.get(event_type, [])
    try:
        handlers.remove(handler)
    except ValueError:
        pass  # Handler was not registered; silently ignore

Expected Behavior

Tests tagged @tdd_issue, @tdd_issue_N, @tdd_expected_fail exist and fail before the fix is applied (raising AttributeError on unsubscribe() call), and pass after the fix is applied.

Acceptance Criteria

  • Test is written and tagged with @tdd_issue, @tdd_issue_N, @tdd_expected_fail
  • Test fails before the fix is applied (AttributeError on unsubscribe call)
  • Test passes after the fix is applied
  • Garbage collection test verifies no strong references remain after unsubscribe

Subtasks

  • Write failing BDD scenario for ReactiveEventBus.unsubscribe()
  • Write failing BDD scenario for LoggingEventBus.unsubscribe()
  • Write failing garbage collection scenario
  • Tag all scenarios with @tdd_issue, @tdd_issue_N, @tdd_expected_fail
  • Confirm tests fail with AttributeError before fix

Definition of Done

This issue is closed when:

  1. All three test scenarios are written and tagged correctly
  2. Tests fail with AttributeError before the fix is applied
  3. Tests pass after the fix (see blocked-by bug issue) is applied
  4. A PR is reviewed and merged to main

References

  • src/cleveragents/infrastructure/events/protocol.pyEventBus protocol (no unsubscribe())
  • src/cleveragents/infrastructure/events/reactive.pyReactiveEventBus.subscribe() (no corresponding unsubscribe)
  • src/cleveragents/infrastructure/events/logging_bus.pyLoggingEventBus.subscribe() (no corresponding unsubscribe)

Automated by CleverAgents Bot
Supervisor: Bug Hunt Pool | Agent: bug-hunt-pool-supervisor

## Metadata - **Commit**: `feat(events): add unsubscribe() to EventBus protocol and implementations` - **Branch**: `fix/events-eventbus-unsubscribe` ## Background and Context The `EventBus` protocol (`src/cleveragents/infrastructure/events/protocol.py`) and both implementations (`ReactiveEventBus`, `LoggingEventBus`) lack an `unsubscribe()` method. Once a handler is registered via `subscribe()`, it cannot be removed. This causes memory leaks in long-running processes because handler callables (typically bound methods) hold references to their owning objects, preventing garbage collection even after the owning component is shut down. ## Summary Write a failing test (tagged `@tdd_issue`, `@tdd_issue_N`, `@tdd_expected_fail`) that verifies both `ReactiveEventBus` and `LoggingEventBus` support unsubscription. ### Scenario ```gherkin @tdd_issue @tdd_issue_N @tdd_expected_fail Scenario: ReactiveEventBus supports unsubscribing a handler Given a ReactiveEventBus instance And a handler registered for PLAN_CREATED events When the handler is unsubscribed And a PLAN_CREATED event is emitted Then the handler is NOT called @tdd_issue @tdd_issue_N @tdd_expected_fail Scenario: LoggingEventBus supports unsubscribing a handler Given a LoggingEventBus instance And a handler registered for PLAN_CREATED events When the handler is unsubscribed And a PLAN_CREATED event is emitted Then the handler is NOT called @tdd_issue @tdd_issue_N @tdd_expected_fail Scenario: Unsubscribed handler does not prevent garbage collection Given a ReactiveEventBus instance And a component with a bound method handler registered for PLAN_CREATED events When the handler is unsubscribed And the component reference is deleted Then the component is garbage collected (no strong references remain in the bus) ``` ### Expected Failure Reason The tests will fail because neither `ReactiveEventBus` nor `LoggingEventBus` has an `unsubscribe()` method. Calling `bus.unsubscribe(event_type, handler)` will raise `AttributeError`. ### Fix Path 1. Add `unsubscribe()` to the `EventBus` protocol in `protocol.py` 2. Implement `unsubscribe()` in `ReactiveEventBus` and `LoggingEventBus` 3. Consider using `weakref` for handler storage to automatically clean up when owning objects are garbage collected Example implementation: ```python def unsubscribe(self, event_type: EventType, handler: Callable[[DomainEvent], None]) -> None: handlers = self._subscriptions.get(event_type, []) try: handlers.remove(handler) except ValueError: pass # Handler was not registered; silently ignore ``` ## Expected Behavior Tests tagged `@tdd_issue`, `@tdd_issue_N`, `@tdd_expected_fail` exist and fail before the fix is applied (raising `AttributeError` on `unsubscribe()` call), and pass after the fix is applied. ## Acceptance Criteria - [ ] Test is written and tagged with `@tdd_issue`, `@tdd_issue_N`, `@tdd_expected_fail` - [ ] Test fails before the fix is applied (AttributeError on unsubscribe call) - [ ] Test passes after the fix is applied - [ ] Garbage collection test verifies no strong references remain after unsubscribe ## Subtasks - [ ] Write failing BDD scenario for `ReactiveEventBus.unsubscribe()` - [ ] Write failing BDD scenario for `LoggingEventBus.unsubscribe()` - [ ] Write failing garbage collection scenario - [ ] Tag all scenarios with `@tdd_issue`, `@tdd_issue_N`, `@tdd_expected_fail` - [ ] Confirm tests fail with `AttributeError` before fix ## Definition of Done This issue is closed when: 1. All three test scenarios are written and tagged correctly 2. Tests fail with `AttributeError` before the fix is applied 3. Tests pass after the fix (see blocked-by bug issue) is applied 4. A PR is reviewed and merged to `main` ## References - `src/cleveragents/infrastructure/events/protocol.py` — `EventBus` protocol (no `unsubscribe()`) - `src/cleveragents/infrastructure/events/reactive.py` — `ReactiveEventBus.subscribe()` (no corresponding unsubscribe) - `src/cleveragents/infrastructure/events/logging_bus.py` — `LoggingEventBus.subscribe()` (no corresponding unsubscribe) --- **Automated by CleverAgents Bot** Supervisor: Bug Hunt Pool | Agent: bug-hunt-pool-supervisor
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#10354
No description provided.