Skip to content

Fix/e2e ready crictl - #136

Merged
HardMax71 merged 7 commits into
mainfrom
fix/e2e-ready-crictl
Feb 5, 2026
Merged

Fix/e2e ready crictl#136
HardMax71 merged 7 commits into
mainfrom
fix/e2e-ready-crictl

Conversation

@HardMax71

@HardMax71 HardMax71 commented Feb 5, 2026

Copy link
Copy Markdown
Owner

Summary by cubic

Pre-pulls the Python runtime into K3s to stabilize e2e runs, and simplifies the event pipeline by removing EventStore, using Mongo-backed EventRepository for persistence, and unifying Avro handling (including DLQ).

  • E2E: Pre-pull docker.io/library/python:3.11-slim via k3s crictl before tests.

  • Event pipeline:

    • Removed EventStore; events are stored via EventRepository with idempotent store_event (ignores duplicates).
    • Producer persists to MongoDB before publishing; KafkaEventService no longer writes an extra audit copy.
    • Simplified SchemaRegistryManager: lazy schema usage; decode via serializer.decode_message; removed initialize_schemas and SCHEMA_REGISTRY_AUTH.
    • Broker decoder now uses serializer + DomainEventAdapter; renamed domain_event_adapter to DomainEventAdapter.
    • DLQ: message body is Avro-encoded DomainEvent; DLQ metadata moves to headers; updated manager, retry, and subscriber.
    • ExecutionService and ReplayService use EventRepository and DomainEventAdapter; added event_types filter to get_execution_events.
    • Updated DI, tests, and config; removed EventStore code/tests; tweaked test CPU/timeout settings.

Written for commit 839bdaa. Summary will update on new commits.

Summary by CodeRabbit

  • Refactor

    • Migrated event storage architecture from EventStore to EventRepository with enhanced batch operations and event-type filtering capabilities.
    • Simplified event serialization by standardizing on DomainEventAdapter for consistent deserialization.
    • Streamlined schema registry to remove legacy schema discovery and authentication logic.
    • Refactored dead-letter queue handling to use header-based metadata instead of JSON parsing.
  • Bug Fixes

    • Enabled idempotent event storage to prevent duplicate entries.

@coderabbitai

coderabbitai Bot commented Feb 5, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR replaces the EventStore abstraction with EventRepository usage throughout the codebase, renames domain_event_adapter to DomainEventAdapter for consistency, simplifies SchemaRegistryManager by removing complex schema discovery logic, updates DLQ handling to use header-based metadata instead of JSON body parsing, and modifies service layers accordingly.

Changes

Cohort / File(s) Summary
EventStore Removal and Repository Migration
backend/app/events/event_store.py, backend/app/db/repositories/event_repository.py, backend/app/services/execution_service.py, backend/app/services/event_replay/replay_service.py
EventStore class entirely removed; EventRepository expanded with store_events_batch, get_execution_events (with new event_types filter parameter), and consistent use of DomainEventAdapter for deserialization. ExecutionService and EventReplayService updated to use EventRepository instead of EventStore.
DomainEventAdapter Renaming
backend/app/domain/events/typed.py, backend/app/domain/events/__init__.py, backend/app/db/repositories/admin/admin_events_repository.py, backend/app/db/repositories/event_repository.py, backend/app/events/broker.py, backend/app/services/kafka_event_service.py, backend/tests/unit/domain/events/test_event_schema_coverage.py, backend/tests/unit/events/test_schema_registry_manager.py
Renamed TypeAdapter instance from domain_event_adapter to DomainEventAdapter and updated all usages across repository, broker, and test files for consistency.
Schema Registry Simplification
backend/app/events/schema/schema_registry.py, backend/app/core/providers.py
Removed complex schema discovery, ID mapping, authentication, and multi-method deserialization from SchemaRegistryManager; kept only serialize_event with simplified Avro schema handling. Updated EventProvider to return synchronous SchemaRegistryManager instead of asynchronous initialization.
DLQ Handler Refactoring
backend/app/dlq/manager.py, backend/app/events/handlers.py
Removed parse_kafka_message and parse_dlq_body helper methods; DLQ messages now constructed from header-based metadata instead of JSON body parsing. DLQ event serialization now uses schema registry path with event metadata in Kafka headers.
Producer and Service Updates
backend/app/events/core/producer.py, backend/app/core/providers.py, backend/app/services/kafka_event_service.py
UnifiedProducer now accepts EventRepository and persists events before publishing; KafkaEventService no longer accepts EventRepository; MessagingProvider updated to pass event_repository to UnifiedProducer.
Event Broker Changes
backend/app/events/broker.py
Removed public create_avro_decoder function; introduced inline avro_decoder inside create_broker that decodes via serializer and validates with DomainEventAdapter.
Infrastructure and Configuration
backend/app/main.py, backend/app/settings.py, .github/actions/e2e-ready/action.yml, backend/config.test.toml
Removed event_store subscriber registration from broker wiring; removed SCHEMA_REGISTRY_AUTH setting; added K3s image pre-pull step in e2e action; increased CPU request and decreased execution timeout in test config.
Test Updates
backend/tests/e2e/events/test_event_store.py, backend/tests/e2e/core/test_dishka_lifespan.py, backend/tests/e2e/core/test_container.py, backend/tests/e2e/dlq/test_dlq_manager.py, backend/tests/e2e/events/test_schema_registry_real.py, backend/tests/e2e/events/test_schema_registry_roundtrip.py, backend/tests/unit/services/pod_monitor/test_monitor.py
Removed EventStore test file and test_event_store_available test; updated remaining tests to use DomainEventAdapter validation and serializer.decode_message for DLQ/schema registry paths; removed FakeEventRepository usage from KafkaEventService tests.

Sequence Diagram

sequenceDiagram
    participant Client
    participant KafkaEventService
    participant UnifiedProducer
    participant EventRepository
    participant SchemaRegistry
    participant Kafka
    participant MongoDB

    Client->>KafkaEventService: publish_event(payload)
    KafkaEventService->>KafkaEventService: Build DomainEvent<br/>via DomainEventAdapter.validate_python()
    KafkaEventService->>UnifiedProducer: produce(event)
    
    UnifiedProducer->>EventRepository: store_event(event)
    EventRepository->>MongoDB: Insert event document
    MongoDB-->>EventRepository: event_id
    EventRepository-->>UnifiedProducer: event_id
    
    UnifiedProducer->>SchemaRegistry: serialize_event(event)
    SchemaRegistry-->>UnifiedProducer: serialized bytes
    
    UnifiedProducer->>Kafka: Publish to topic<br/>with headers (event_type, context)
    Kafka-->>UnifiedProducer: ack
    UnifiedProducer-->>KafkaEventService: done
    KafkaEventService-->>Client: response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • PR #132: Directly modifies UnifiedProducer and core/providers.py with overlapping dependency injection changes.
  • PR #126: Directly modifies dlq/manager.py and DLQ message handling with overlapping serialization and header-based metadata changes.
  • PR #84: Refactors the overall event system to unified DomainEvent/Avro model with overlapping changes across schema registry, DLQ, producer/consumer wiring, and multiple shared modules.

Poem

🐰 The warren cheers as EventStore takes its final bow,
EventRepository now handles the events, and how!
DomainEventAdapter stands proud, capitalized with care,
Schemas simplified, DLQ headers float through the air,
A refactor complete, our pipeline shines bright!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Fix/e2e ready crictl' is vague and does not clearly convey the scope or nature of the changes. While 'e2e ready' and 'crictl' relate to the E2E workflow changes, the title obscures a much larger refactoring affecting event storage, schema registry, and multiple service implementations across the codebase. Consider a more descriptive title that captures the primary intent, such as 'Migrate from EventStore to EventRepository' or 'Refactor event handling and schema registry integration' to accurately reflect the substantial architectural changes in this PR.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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/e2e-ready-crictl

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 5, 2026

Copy link
Copy Markdown

@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: 1

Caution

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

⚠️ Outside diff range comments (2)
backend/app/events/handlers.py (1)

305-345: ⚠️ Potential issue | 🟠 Major

Harden DLQ header parsing to prevent handler crashes and infinite retry loops.

The handler lacks error handling for header parsing. If any header value is None, v.decode() raises AttributeError. If failed_at is missing or malformed, datetime.fromisoformat(headers["failed_at"]) raises KeyError or ValueError. Invalid retry_count or status values raise ValueError in int() or DLQMessageStatus() constructors. Since ack_policy=AckPolicy.ACK requires successful handler completion to acknowledge the message, any parsing failure leaves the message unacknowledged, triggering indefinite retries—making the DLQ handler itself a dead-letter victim.

Add error handling for all header parsing operations with sensible fallbacks:

🛠️ Suggested hardening
-        headers = {k: v.decode() for k, v in (raw.headers or [])}
+        def _decode_header(value: bytes | None) -> str:
+            return value.decode(errors="replace") if value is not None else ""
+
+        headers = {k: _decode_header(v) for k, v in (raw.headers or [])}
+
+        def _parse_failed_at(value: str | None) -> datetime:
+            if not value:
+                return datetime.now(timezone.utc)
+            try:
+                dt = datetime.fromisoformat(value)
+            except ValueError:
+                return datetime.now(timezone.utc)
+            return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
+
+        failed_at = _parse_failed_at(headers.get("failed_at"))
+
+        try:
+            retry_count = int(headers.get("retry_count", "0"))
+        except ValueError:
+            retry_count = 0
+
+        try:
+            status = DLQMessageStatus(headers.get("status", "pending"))
+        except ValueError:
+            status = DLQMessageStatus("pending")
@@
-            retry_count=int(headers.get("retry_count", "0")),
-            failed_at=datetime.fromisoformat(headers["failed_at"]),
-            status=DLQMessageStatus(headers.get("status", "pending")),
+            retry_count=retry_count,
+            failed_at=failed_at,
+            status=status,
backend/app/db/repositories/event_repository.py (1)

64-76: ⚠️ Potential issue | 🟠 Major

Batch insert lacks idempotency — duplicates will raise an exception.

Unlike store_event, store_events_batch uses insert_many without ordered=False or DuplicateKeyError handling. If any event in the batch already exists, the entire operation fails. This inconsistency could cause issues if batch inserts are retried.

🛠️ Proposed fix to add idempotent batch handling
     async def store_events_batch(self, events: list[DomainEvent]) -> list[str]:
         if not events:
             return []
         now = datetime.now(timezone.utc)
         docs = []
         for event in events:
             data = event.model_dump(exclude_none=True)
             data.setdefault("stored_at", now)
             docs.append(EventDocument(**data))
-        await EventDocument.insert_many(docs)
+        try:
+            await EventDocument.insert_many(docs, ordered=False)
+        except DuplicateKeyError:
+            self.logger.debug(f"Some events in batch already stored, duplicates skipped")
         add_span_attributes(**{"events.batch.count": len(events)})
         self.logger.info(f"Stored {len(events)} events in batch")
         return [event.event_id for event in events]
🤖 Fix all issues with AI agents
In `@backend/app/events/core/producer.py`:
- Around line 86-95: The headers dict passed to inject_trace_context must be all
strings; convert the DLQMessageStatus enum to a string by wrapping
DLQMessageStatus.PENDING with str() so it matches the dict[str, str] contract
(update the headers construction where inject_trace_context is called and where
DLQMessageStatus.PENDING is currently used).
🧹 Nitpick comments (3)
.github/actions/e2e-ready/action.yml (1)

35-37: Make the pre-pulled runtime image configurable to prevent drift.

Hard-coding python:3.11-slim can silently fall out of sync with the runtime image used by workers. Consider a configurable input so CI stays aligned when the runtime image changes.

♻️ Suggested refactor
 inputs:
   image-tag:
     description: 'GHCR image tag (e.g., sha-abc1234)'
     required: true
+  runtime-image:
+    description: 'Container image to pre-pull into k3s for executions'
+    required: false
+    default: 'docker.io/library/python:3.11-slim'
@@
-    - name: Pre-pull test runtime image into K3s
+    - name: Pre-pull test runtime image into K3s
       shell: bash
-      run: sudo k3s crictl pull docker.io/library/python:3.11-slim
+      run: sudo k3s crictl pull ${{ inputs.runtime-image }}
backend/tests/unit/domain/events/test_event_schema_coverage.py (1)

86-102: Test method uses PascalCase — consider snake_case for consistency.

The test method test_DomainEventAdapter_covers_all_types uses PascalCase for the class name in the method name. While this accurately reflects the class being tested, Python convention and most test frameworks prefer snake_case for method names (e.g., test_domain_event_adapter_covers_all_types).

The implementation itself is correct — validating that all EventType values are recognized by the adapter.

♻️ Optional: rename to snake_case
-    def test_DomainEventAdapter_covers_all_types(self) -> None:
-        """The DomainEventAdapter TypeAdapter must handle all EventTypes."""
+    def test_domain_event_adapter_covers_all_types(self) -> None:
+        """The DomainEventAdapter TypeAdapter must handle all EventTypes."""
backend/app/events/broker.py (1)

19-21: Consider adding error handling for malformed messages.

The decoder currently lacks error handling. If decode_message or validate_python fails (e.g., corrupted Avro, schema mismatch, or invalid event data), the exception will propagate directly. Depending on FastStream's consumer configuration, this could cause message reprocessing loops or consumer crashes.

Consider wrapping in try/except to handle validation failures gracefully (e.g., logging and sending to DLQ):

💡 Suggested error handling pattern
     async def avro_decoder(msg: StreamMessage[Any]) -> DomainEvent:
-        payload = await schema_registry.serializer.decode_message(msg.body)
-        return DomainEventAdapter.validate_python(payload)
+        try:
+            payload = await schema_registry.serializer.decode_message(msg.body)
+            return DomainEventAdapter.validate_python(payload)
+        except Exception as e:
+            logger.error(f"Failed to decode message: {e}", extra={"raw_body_len": len(msg.body)})
+            raise

Comment thread backend/app/events/core/producer.py

@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.

5 issues found across 27 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/events/handlers.py">

<violation number="1" location="backend/app/events/handlers.py:318">
P2: Similar to the status field, `int(...)` will raise a `ValueError` if the "retry_count" header is not a valid integer. This could crash the consumer on malformed headers.</violation>

<violation number="2" location="backend/app/events/handlers.py:319">
P1: This line will raise a `KeyError` if the "failed_at" header is missing. While `DLQMessage` has a default for this field, explicit passing of a missing key bypasses the default mechanism and crashes.

Robustness suggestion: Use `.get()` with a fallback to the current time.</violation>

<violation number="3" location="backend/app/events/handlers.py:320">
P2: The status update logic relies on the "status" header being a valid `DLQMessageStatus` enum value. If the header contains an invalid string, `DLQMessageStatus(...)` will raise a `ValueError`, causing the consumer to crash.

Consider using a try/except block or validation to default to `PENDING` on invalid input.</violation>
</file>

<file name="backend/app/events/schema/schema_registry.py">

<violation number="1" location="backend/app/events/schema/schema_registry.py:28">
P1: Missing timestamp conversion to microseconds. The original code converted datetime fields to microseconds (`int(timestamp.timestamp() * 1_000_000)`), which is required for Avro's `long` timestamp type. Using `model_dump()` alone returns native datetime objects that may not serialize correctly to Avro format.</violation>
</file>

<file name="backend/app/dlq/manager.py">

<violation number="1" location="backend/app/dlq/manager.py:156">
P3: The `event_type` is assigned an Enum member directly. While `StringEnum` may behave like a string at runtime, strict type checking expects a `str`. Explicitly casting to `str` ensures type safety and consistency with other usages in the file.</violation>
</file>

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

Comment thread backend/app/events/handlers.py
Comment thread backend/app/events/schema/schema_registry.py
Comment thread backend/app/events/handlers.py
Comment thread backend/app/events/handlers.py
Comment thread backend/app/dlq/manager.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