Skip to content

fix: simplification of sse - #127

Merged
HardMax71 merged 1 commit into
mainfrom
fix/sse-lifecycle
Feb 1, 2026
Merged

fix: simplification of sse#127
HardMax71 merged 1 commit into
mainfrom
fix/sse-lifecycle

Conversation

@HardMax71

@HardMax71 HardMax71 commented Feb 1, 2026

Copy link
Copy Markdown
Owner

Summary by cubic

Simplified the SSE stack by removing the Kafka→Redis bridge and custom shutdown manager. SSE consumers now start via DI, events are routed straight to Redis, and the SSE service uses simple streams with sse-starlette handling disconnects.

  • Refactors

    • Start a UnifiedConsumer pool in the DI provider; route events to Redis via SSERedisBus.
    • Added SSERedisBus.SSE_ROUTED_EVENTS and bus.route_domain_event for Kafka→Redis routing.
    • Removed SSEKafkaRedisBridge and SSEShutdownManager; deleted related tests.
    • Simplified SSEService: no heartbeats/shutdown flow; uses Redis subscription loop and cleans up on generator close.
    • Removed SSE health models and /api/v1/events/health route.
    • Trimmed SSEControlEvent (removed HEARTBEAT, SHUTDOWN, ERROR) and related fields in SSEExecutionEventData.
    • Updated app lifespan to no longer manage the bridge; NotificationService + Redis bus handle startup.
  • Migration

    • Remove any client usage of /api/v1/events/health.
    • Stop relying on HEARTBEAT/SHUTDOWN/ERROR control events; these are no longer emitted.
    • Update clients using SSEExecutionEventData: grace_period and error fields are removed; message is only for “subscribed”.

Written for commit 5b0226e. Summary will update on new commits.

Summary by CodeRabbit

  • Refactor

    • Consolidated Server-Sent Events (SSE) infrastructure by removing the separate bridge component and integrating event routing directly into the Redis bus.
    • Simplified SSE lifecycle management by eliminating the shutdown manager in favor of streamlined consumer orchestration.
  • Bug Fixes

    • Removed the health monitoring endpoint that was not being actively used.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Feb 1, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This pull request refactors the SSE subsystem by removing the health-check endpoint, eliminating the Kafka-Redis bridge and shutdown manager components, and consolidating consumer and routing logic directly into the provider and service layers. The initialization sequence and event routing are streamlined.

Changes

Cohort / File(s) Summary
SSE Health Removal
backend/app/api/routes/sse.py, backend/app/domain/enums/sse.py, backend/app/domain/sse/models.py, backend/app/domain/sse/__init__.py, backend/app/schemas_pydantic/sse.py
Removes health endpoint, SSEHealthStatus enum, SSEHealthDomain model, and related response schemas from domain and API layers.
SSE Bridge & Shutdown Manager Deletion
backend/app/services/sse/kafka_redis_bridge.py, backend/app/services/sse/sse_shutdown_manager.py
Removes entire bridge component (consumer pool, lifecycle, event routing) and shutdown manager (phases, connection tracking, graceful shutdown orchestration).
Provider & Lifecycle Consolidation
backend/app/core/providers.py, backend/app/core/dishka_lifespan.py
Replaces two-stage bridge initialization with consolidated bus setup that builds and manages consumer pool directly; updates DI container and lifecycle hooks.
SSE Service & Bus Refactoring
backend/app/services/sse/sse_service.py, backend/app/services/sse/redis_bus.py
Introduces SSERedisSubscription-based streaming and moves event routing logic to SSERedisBus via new route_domain_event method and SSE_ROUTED_EVENTS constant; simplifies service constructor dependencies.
Test Updates
backend/tests/e2e/core/test_dishka_lifespan.py, backend/tests/e2e/test_sse_routes.py, backend/tests/e2e/services/sse/test_partitioned_event_router.py, backend/tests/unit/services/sse/test_kafka_redis_bridge.py, backend/tests/unit/services/sse/test_sse_service.py, backend/tests/unit/services/sse/test_shutdown_manager.py, backend/tests/unit/services/sse/test_sse_shutdown_manager.py
Removes health-related tests; refactors bridge and shutdown manager tests to use UnifiedConsumer and EventDispatcher directly; simplifies SSEService instantiation in unit tests; deletes test files for removed components.

Sequence Diagram(s)

sequenceDiagram
    participant Provider as SSE Provider
    participant Bus as SSERedisBus
    participant Consumer as UnifiedConsumer
    participant Kafka as Kafka
    participant Redis as Redis
    participant Client as SSE Client

    Note over Provider,Redis: Initialization Phase
    Provider->>Bus: create async bus
    Provider->>Consumer: build consumer pool
    Provider->>Consumer: start consumers with topics
    Consumer->>Kafka: subscribe to WEBSOCKET_GATEWAY topics
    
    Note over Consumer,Redis: Event Processing Phase
    Kafka->>Consumer: deliver domain event
    Consumer->>Bus: route_domain_event(event)
    Bus->>Redis: publish to execution channel
    Redis->>Client: deliver SSE event
    
    Note over Provider,Redis: Shutdown Phase
    Provider->>Consumer: stop all consumers
    Bus->>Redis: close connection
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 No health checks to hoax,
Bridges and managers—gone like smoke!
The bus routes events with grace,
Consumers start at lightning pace,
SSE now flows straight and clean,
Simplest streaming ever seen! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the pull request, which is to simplify the SSE (Server-Sent Events) architecture by removing the Kafka-Redis bridge layer and shutdown manager components.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/sse-lifecycle

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@sonarqubecloud

sonarqubecloud Bot commented Feb 1, 2026

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 18 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="backend/app/core/providers.py">

<violation number="1" location="backend/app/core/providers.py:451">
P2: Starting the SSE consumers before the `try/finally` means a partial startup failure will leak already-started consumers (they're never stopped). Wrap the start in a try/except that stops any started consumers before re-raising.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread backend/app/core/providers.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/app/services/sse/sse_service.py (2)

135-162: ⚠️ Potential issue | 🟡 Minor

Missing connection metrics for notification stream.

The execution stream tracks connections via metrics.increment_sse_connections("executions") and metrics.decrement_sse_connections("executions"), but the notification stream has no equivalent metrics tracking. This creates an observability gap.

📊 Proposed fix to add metrics tracking
     async def create_notification_stream(self, user_id: str) -> AsyncGenerator[dict[str, Any], None]:
         subscription: SSERedisSubscription | None = None
+        self.metrics.increment_sse_connections("notifications")
         try:
             subscription = await self.sse_bus.open_notification_subscription(user_id)
             self.logger.info("Notification subscription opened", extra={"user_id": user_id})
 
             while True:
                 redis_msg = await subscription.get(RedisNotificationMessage)
                 if not redis_msg:
                     continue
 
                 notification = NotificationResponse(
                     notification_id=redis_msg.notification_id,
                     channel=NotificationChannel.IN_APP,
                     status=redis_msg.status,
                     subject=redis_msg.subject,
                     body=redis_msg.body,
                     action_url=redis_msg.action_url,
                     created_at=redis_msg.created_at,
                     read_at=None,
                     severity=redis_msg.severity,
                     tags=redis_msg.tags,
                 )
                 yield {"event": "notification", "data": notification.model_dump_json()}
         finally:
             if subscription is not None:
                 await asyncio.shield(subscription.close())
+            self.metrics.decrement_sse_connections("notifications")
             self.logger.info("Notification stream closed", extra={"user_id": user_id})

33-38: ⚠️ Potential issue | 🔴 Critical

EXECUTION_COMPLETED is intentionally excluded—stream waits for RESULT_STORED after completion. However, EXECUTION_CANCELLED is also excluded but not handled by the result processor, which causes streams to hang indefinitely on cancellation.

The result processor registers handlers for EXECUTION_COMPLETED, EXECUTION_FAILED, and EXECUTION_TIMEOUT, which all publish RESULT_STORED (or RESULT_FAILED on error), closing the stream as intended. However, EXECUTION_CANCELLED is published when executions are cancelled but is only handled by the coordinator to clean up queuing state—it does not trigger result storage. This means SSE streams will never receive a terminal event and will hang indefinitely.

Either add EXECUTION_CANCELLED to TERMINAL_EVENT_TYPES, or register a handler in the result processor to publish a result event when executions are cancelled.

🤖 Fix all issues with AI agents
In `@backend/app/core/dishka_lifespan.py`:
- Around line 78-94: The NotificationService resolution is happening in Phase 1
which triggers the get_sse_redis_bus provider (which starts SSERedisBus
consumers via await asyncio.gather(...)) before event schemas are registered;
move the container.get(NotificationService) call out of the Phase 1
asyncio.gather and perform it after initialize_event_schemas() in Phase 2 so
schemas are registered before consumers start, ensuring
schema_registry.deserialize_event() and its _get_event_class_by_id lookup can
succeed.

In `@backend/app/core/providers.py`:
- Around line 440-457: The provider get_sse_redis_bus can leak running consumer
tasks if one c.start() raises because asyncio.gather aborts; modify startup to
track started consumers from _build_sse_consumers and start them in a safe way
(either start sequentially in a for loop or gather while capturing results) so
you can catch exceptions, stop any already-started consumers with await
asyncio.gather(*[c.stop() for c in started], return_exceptions=True) in the
except block, then re-raise the error; keep the existing finally that stops all
consumers to cover normal exit. Ensure you reference SSERedisBus,
CONSUMER_GROUP_SUBSCRIPTIONS[GroupId.WEBSOCKET_GATEWAY], consumers, c.start()
and c.stop() when implementing the fix.

In `@backend/app/services/sse/redis_bus.py`:
- Around line 93-108: The logger.error call in route_domain_event contains an
extra blank line before the closing parenthesis; open the route_domain_event
method, find the try/except block where publish_event is called and the
logger.error invocation (logger.error(..., exc_info=True,)), and remove the
stray blank line so the closing parenthesis directly follows the exc_info
argument, keeping the existing arguments and exc_info=True intact; ensure no
other formatting changes are made to publish_event or route_domain_event.
🧹 Nitpick comments (1)
backend/app/services/sse/sse_service.py (1)

88-111: Consider reducing log level for routine message receipt.

Logging at INFO level for every received Redis message (line 93-96) may generate excessive log volume in production. Consider using DEBUG level for routine message processing.

♻️ Proposed change
-                self.logger.info(
+                self.logger.debug(
                     "Received Redis message for execution",
                     extra={"execution_id": execution_id, "event_type": str(msg.event_type)},
                 )

Comment thread backend/app/core/dishka_lifespan.py
Comment thread backend/app/core/providers.py
Comment thread backend/app/services/sse/redis_bus.py
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.

1 participant