Skip to content

feat(events_bus,api): EventBus lifecycle + layer guard + public-API surface lock#167

Open
Gradata wants to merge 1 commit intopr/dualwrite-atomicity-2026-05-02from
pr/api-surface-and-threads-2026-05-02
Open

feat(events_bus,api): EventBus lifecycle + layer guard + public-API surface lock#167
Gradata wants to merge 1 commit intopr/dualwrite-atomicity-2026-05-02from
pr/api-surface-and-threads-2026-05-02

Conversation

@Gradata
Copy link
Copy Markdown
Owner

@Gradata Gradata commented May 2, 2026

Summary

Closes the last 3 council blockers from council_2026-05-02T11-59-00.md: thread lifecycle, layer-violation discipline, public API surface lock.

Stacks on #164 — please merge that one first.

Changes

EventBus lifecycle

  • New EventBus.close() / shutdown() — idempotent drain semantics. Drains in-flight async handlers, clears listeners, rejects late on()/emit() (DEBUG log, never raised).
  • WeakSet of live buses + single atexit handler so background gradata-bus* threads cannot outlive the process even if a caller forgets close().
  • Brain.close() now drains the EventBus before re-encrypting the database.

Layer-violation guard

  • tests/test_layer_enforcement.py ast-walks src/gradata/, classifies every upward import (L0 → L1 / L1 → L2 etc.), fails the build on any new unflagged violation.
  • Pre-existing edges in _core.py (DEFERRED — >50-line refactor) and _scoped_brain.py (LAZY-IMPORT-OK) are catalogued in ALLOWED_UPWARD_IMPORTS with rationale.
  • contrib/patterns/orchestrator.py lost its eager Layer-1 import.

Public API surface lock

  • gradata/__init__.py__all__ locked to the 25-name documented surface (Brain, BrainContext, Lesson, LessonState, ScopedBrain, 9 exception classes, 6 enhancement helpers, __version__).
  • tests/test_public_api_surface.py — golden test fails on any drift in either direction.

Test plan

  • pytest tests/test_eventbus_lifecycle.py tests/test_layer_enforcement.py tests/test_public_api_surface.py5 passed.
  • pytest tests/test_brain.py tests/test_middleware_core.py tests/test_rule_pipeline.py tests/test_retrieval_fusion.py73 passed.
  • pyright src/ — 0 errors, 27 warnings (unchanged baseline).

Layering check

All upward imports — introduced or pre-existing — are catalogued in ALLOWED_UPWARD_IMPORTS with classification (PUBLIC BARREL / LAZY-IMPORT-OK / DEFERRED). Net change: orchestrator.py loses a real upward edge; no new edges introduced.

Risk

  • EventBus.close() runs from Brain.close(). Code that touches Brain after close sees emit/on/off as no-ops — intended contract.
  • atexit handler is best-effort (2s timeout/bus). Hung handlers in the pool aren't force-killed; logged.

Council references

  • council_2026-05-02T11-59-00.md
  • council_2026-05-02T12-24-08.md (autonomous-mode policy)

…urface lock

Council v4 (council_2026-05-02T11-59-00.md) flagged three remaining
production blockers after PR1+PR2: thread-lifecycle leaks in EventBus,
layer-violation discipline, and an undefined public API surface that
escapes SemVer guarantees.

What
- src/gradata/events_bus.py
  - Added EventBus.close()/shutdown() with idempotent drain semantics:
    drain in-flight async handlers, clear listeners, reject late on()/
    emit() calls (logged at DEBUG, not raised).
  - Tracks every live EventBus in a WeakSet and registers a single
    atexit handler so background threads cannot outlive the process if
    a caller forgets to close.
  - Single threading.Lock guards listeners + closed flag.
- src/gradata/brain.py
  - Brain.close() now drains the EventBus before re-encrypting the
    database; covers the typical `with Brain(...) as b:` lifecycle.
- src/gradata/__init__.py
  - Locked __all__ to the 25-name documented public surface
    (Brain, BrainContext, Lesson, LessonState, ScopedBrain, the
    9 exception classes, the 6 enhancement helpers, and __version__).
- src/gradata/contrib/patterns/orchestrator.py
  - Removed eager Layer 1 import from Layer 0 module; remaining cross-
    layer access is via lazy function-local imports documented in
    test_layer_enforcement.ALLOWED_UPWARD_IMPORTS.
- tests/test_eventbus_lifecycle.py — 3 tests covering concurrent
  subscribe/unsubscribe, executor wait-on-close + late-submit rejection,
  and 100-cycle Brain init/close with active_count() leak detection.
- tests/test_layer_enforcement.py — ast-walks src/gradata/, classifies
  every upward import; fails the build on any new unflagged violation.
  Pre-existing _core.py and _scoped_brain.py edges are catalogued as
  DEFERRED (>50-line refactor) or LAZY-IMPORT-OK.
- tests/test_public_api_surface.py — golden test that fails the build
  if gradata.__all__ drifts from the locked set in either direction.

Why
- EventBus had no shutdown — Brain.close() left ThreadPoolExecutor
  workers alive. Long-running test suites accumulated threads, and
  process-exit could hit "thread still running at interpreter
  shutdown" in noisy environments.
- Layer-violation drift is silent: every accidental
  Layer-0->Layer-1 import was a future cycle waiting to happen.
- Without an __all__ lock, every implicit export becomes a SemVer
  contract by accident.

Test plan
- pytest tests/test_eventbus_lifecycle.py tests/test_layer_enforcement.py
  tests/test_public_api_surface.py — 5 passed.
- pytest tests/test_brain.py tests/test_middleware_core.py
  tests/test_rule_pipeline.py tests/test_retrieval_fusion.py — 73 passed.
- pyright src/ — 0 errors, 27 warnings (unchanged baseline).

Layering check
- All upward imports introduced or pre-existing are catalogued in
  tests/test_layer_enforcement.ALLOWED_UPWARD_IMPORTS with a
  classification (PUBLIC BARREL / LAZY-IMPORT-OK / DEFERRED).
- Net change: orchestrator.py loses a real upward edge; nothing new.

Risk
- EventBus.close() is now called from Brain.close(). Any code path that
  uses Brain after close() will see emit/on/off become no-ops. This is
  the intended contract.
- atexit handler is best-effort (timeout 2s per bus). Hung handlers in
  the pool will not be force-killed; they get logged.

Council references
- council_2026-05-02T11-59-00.md
- council_2026-05-02T12-24-08.md (autonomous-mode policy)

Stacks on #164.
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2c00426a-62a6-450a-9d45-d1e76c902d90

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pr/api-surface-and-threads-2026-05-02

Comment @coderabbitai help to get the list of available commands and usage tips.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants