Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 6 additions & 25 deletions backend/app/core/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
from app.domain.rate_limit import RateLimitConfig
from app.domain.saga.models import SagaConfig
from app.events.core import UnifiedProducer
from app.events.event_store import EventStore, create_event_store
from app.events.schema.schema_registry import SchemaRegistryManager
from app.services.admin import AdminEventsService, AdminSettingsService, AdminUserService
from app.services.auth_service import AuthService
Expand Down Expand Up @@ -182,11 +181,12 @@ def get_unified_producer(
self,
broker: KafkaBroker,
schema_registry: SchemaRegistryManager,
event_repository: EventRepository,
logger: logging.Logger,
settings: Settings,
event_metrics: EventMetrics,
) -> UnifiedProducer:
return UnifiedProducer(broker, schema_registry, logger, settings, event_metrics)
return UnifiedProducer(broker, schema_registry, event_repository, logger, settings, event_metrics)

@provide
def get_idempotency_repository(self, redis_client: redis.Redis) -> RedisIdempotencyRepository:
Expand Down Expand Up @@ -276,21 +276,8 @@ class EventProvider(Provider):
scope = Scope.APP

@provide
async def get_schema_registry(self, settings: Settings, logger: logging.Logger) -> SchemaRegistryManager:
registry = SchemaRegistryManager(settings, logger)
await registry.initialize_schemas()
return registry

@provide
def get_event_store(
self,
schema_registry: SchemaRegistryManager,
logger: logging.Logger,
event_metrics: EventMetrics,
) -> EventStore:
return create_event_store(
schema_registry=schema_registry, logger=logger, event_metrics=event_metrics, ttl_days=90
)
def get_schema_registry(self, settings: Settings, logger: logging.Logger) -> SchemaRegistryManager:
return SchemaRegistryManager(settings, logger)


class KubernetesProvider(Provider):
Expand Down Expand Up @@ -481,14 +468,12 @@ def get_event_service(self, event_repository: EventRepository) -> EventService:
@provide
def get_kafka_event_service(
self,
event_repository: EventRepository,
kafka_producer: UnifiedProducer,
settings: Settings,
logger: logging.Logger,
event_metrics: EventMetrics,
) -> KafkaEventService:
return KafkaEventService(
event_repository=event_repository,
kafka_producer=kafka_producer,
settings=settings,
logger=logger,
Expand Down Expand Up @@ -632,15 +617,15 @@ def get_execution_service(
self,
execution_repository: ExecutionRepository,
kafka_producer: UnifiedProducer,
event_store: EventStore,
event_repository: EventRepository,
settings: Settings,
logger: logging.Logger,
execution_metrics: ExecutionMetrics,
) -> ExecutionService:
return ExecutionService(
execution_repo=execution_repository,
producer=kafka_producer,
event_store=event_store,
event_repository=event_repository,
settings=settings,
logger=logger,
execution_metrics=execution_metrics,
Expand Down Expand Up @@ -874,14 +859,12 @@ def get_event_replay_service(
self,
replay_repository: ReplayRepository,
kafka_producer: UnifiedProducer,
event_store: EventStore,
replay_metrics: ReplayMetrics,
logger: logging.Logger,
) -> EventReplayService:
return EventReplayService(
repository=replay_repository,
producer=kafka_producer,
event_store=event_store,
replay_metrics=replay_metrics,
logger=logger,
)
Expand All @@ -901,7 +884,6 @@ async def get_event_replay_service(
self,
replay_repository: ReplayRepository,
kafka_producer: UnifiedProducer,
event_store: EventStore,
replay_metrics: ReplayMetrics,
logger: logging.Logger,
database: Database,
Expand All @@ -910,7 +892,6 @@ async def get_event_replay_service(
service = EventReplayService(
repository=replay_repository,
producer=kafka_producer,
event_store=event_store,
replay_metrics=replay_metrics,
logger=logger,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from app.domain.enums.replay import ReplayStatus
from app.domain.events import (
DomainEvent,
DomainEventAdapter,
EventBrowseResult,
EventDetail,
EventExportRow,
Expand All @@ -26,7 +27,6 @@
EventTypeCount,
HourlyEventCount,
UserEventCount,
domain_event_adapter,
)
from app.domain.replay.models import ReplayFilter, ReplaySessionState

Expand Down Expand Up @@ -59,7 +59,7 @@ async def browse_events(
total = await query.count()

docs = await query.sort([(sort_by, sort_order)]).skip(skip).limit(limit).to_list()
events = [domain_event_adapter.validate_python(d, from_attributes=True) for d in docs]
events = [DomainEventAdapter.validate_python(d, from_attributes=True) for d in docs]

return EventBrowseResult(events=events, total=total, skip=skip, limit=limit)

Expand All @@ -68,7 +68,7 @@ async def get_event_detail(self, event_id: str) -> EventDetail | None:
if not doc:
return None

event = domain_event_adapter.validate_python(doc, from_attributes=True)
event = DomainEventAdapter.validate_python(doc, from_attributes=True)

related_query = {"metadata.correlation_id": doc.metadata.correlation_id, "event_id": {"$ne": event_id}}
related_docs = await (
Expand Down
36 changes: 24 additions & 12 deletions backend/app/db/repositories/event_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from beanie.odm.operators.find import BaseFindOperator
from beanie.operators import GTE, LT, LTE, Eq, In, Not, Or, RegEx
from monggregate import Pipeline, S
from pymongo.errors import DuplicateKeyError

from app.core.tracing import EventAttributes
from app.core.tracing.utils import add_span_attributes
Expand All @@ -14,13 +15,13 @@
from app.domain.events import (
ArchivedEvent,
DomainEvent,
DomainEventAdapter,
EventAggregationResult,
EventListResult,
EventReplayInfo,
EventStatistics,
EventTypeCount,
ServiceEventCount,
domain_event_adapter,
)


Expand All @@ -41,6 +42,7 @@ def _build_time_filter(self, start_time: datetime | None, end_time: datetime | N
return {key: value for key, value in {"$gte": start_time, "$lte": end_time}.items() if value is not None}

async def store_event(self, event: DomainEvent) -> str:
"""Idempotent event store — silently ignores duplicates by event_id."""
data = event.model_dump(exclude_none=True)
data.setdefault("stored_at", datetime.now(timezone.utc))
doc = EventDocument(**data)
Expand All @@ -51,7 +53,11 @@ async def store_event(self, event: DomainEvent) -> str:
str(EventAttributes.EXECUTION_ID): event.aggregate_id or "",
}
)
await doc.insert()
try:
await doc.insert()
except DuplicateKeyError:
self.logger.debug(f"Event {event.event_id} already stored, skipping")
return event.event_id
self.logger.debug(f"Stored event {event.event_id} of type {event.event_type}")
return event.event_id

Expand All @@ -73,7 +79,7 @@ async def get_event(self, event_id: str) -> DomainEvent | None:
doc = await EventDocument.find_one(EventDocument.event_id == event_id)
if not doc:
return None
return domain_event_adapter.validate_python(doc, from_attributes=True)
return DomainEventAdapter.validate_python(doc, from_attributes=True)

async def get_events_by_type(
self,
Expand All @@ -94,7 +100,7 @@ async def get_events_by_type(
.limit(limit)
.to_list()
)
return [domain_event_adapter.validate_python(d, from_attributes=True) for d in docs]
return [DomainEventAdapter.validate_python(d, from_attributes=True) for d in docs]

async def get_events_by_aggregate(
self, aggregate_id: str, event_types: list[EventType] | None = None, limit: int = 100
Expand All @@ -105,7 +111,7 @@ async def get_events_by_aggregate(
docs = (
await EventDocument.find(*conditions).sort([("timestamp", SortDirection.ASCENDING)]).limit(limit).to_list()
)
return [domain_event_adapter.validate_python(d, from_attributes=True) for d in docs]
return [DomainEventAdapter.validate_python(d, from_attributes=True) for d in docs]

async def get_events_by_correlation(
self, correlation_id: str, limit: int = 100, skip: int = 0, user_id: str | None = None,
Expand All @@ -119,7 +125,7 @@ async def get_events_by_correlation(
.sort([("timestamp", SortDirection.ASCENDING)])
.skip(skip).limit(limit).to_list()
)
events = [domain_event_adapter.validate_python(d, from_attributes=True) for d in docs]
events = [DomainEventAdapter.validate_python(d, from_attributes=True) for d in docs]
total_count = await EventDocument.find(condition).count()
total_count = max(total_count, skip + len(events))
return EventListResult(
Expand Down Expand Up @@ -152,16 +158,22 @@ async def get_events_by_user(
.limit(limit)
.to_list()
)
return [domain_event_adapter.validate_python(d, from_attributes=True) for d in docs]
return [DomainEventAdapter.validate_python(d, from_attributes=True) for d in docs]

async def get_execution_events(
self, execution_id: str, limit: int = 100, skip: int = 0, exclude_system_events: bool = False
self,
execution_id: str,
limit: int = 100,
skip: int = 0,
exclude_system_events: bool = False,
event_types: list[EventType] | None = None,
) -> EventListResult:
conditions: list[Any] = [
Or(
EventDocument.execution_id == execution_id,
EventDocument.aggregate_id == execution_id,
),
In(EventDocument.event_type, event_types) if event_types else None,
Not(RegEx(EventDocument.metadata.service_name, "^system-")) if exclude_system_events else None,
]
conditions = [c for c in conditions if c is not None]
Expand All @@ -172,7 +184,7 @@ async def get_execution_events(
.sort([("timestamp", SortDirection.ASCENDING)])
.skip(skip).limit(limit).to_list()
)
events = [domain_event_adapter.validate_python(d, from_attributes=True) for d in docs]
events = [DomainEventAdapter.validate_python(d, from_attributes=True) for d in docs]
total_count = await EventDocument.find(*conditions).count()
total_count = max(total_count, skip + len(events))
return EventListResult(
Expand Down Expand Up @@ -307,7 +319,7 @@ async def get_user_events_paginated(
.sort([("timestamp", sort_direction)])
.skip(skip).limit(limit).to_list()
)
events = [domain_event_adapter.validate_python(d, from_attributes=True) for d in docs]
events = [DomainEventAdapter.validate_python(d, from_attributes=True) for d in docs]
total_count = await EventDocument.find(*conditions).count()
total_count = max(total_count, skip + len(events))
return EventListResult(
Expand All @@ -334,7 +346,7 @@ async def query_events(
.sort([(sort_field, SortDirection.DESCENDING)])
.skip(skip).limit(limit).to_list()
)
events = [domain_event_adapter.validate_python(d, from_attributes=True) for d in docs]
events = [DomainEventAdapter.validate_python(d, from_attributes=True) for d in docs]
total_count = await EventDocument.find(query).count()
total_count = max(total_count, skip + len(events))
return EventListResult(
Expand Down Expand Up @@ -398,7 +410,7 @@ async def get_aggregate_replay_info(self, aggregate_id: str) -> EventReplayInfo
)

async for doc in EventDocument.aggregate(pipeline.export()):
events = [domain_event_adapter.validate_python(e) for e in doc["events"]]
events = [DomainEventAdapter.validate_python(e) for e in doc["events"]]
return EventReplayInfo(
events=events,
event_count=doc["event_count"],
Expand Down
19 changes: 3 additions & 16 deletions backend/app/dlq/manager.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import json
import logging
from datetime import datetime, timezone
from typing import Any, Callable
from typing import Callable

from faststream.kafka import KafkaBroker

Expand Down Expand Up @@ -122,18 +121,6 @@ async def process_monitoring_cycle(self) -> None:
await self.process_due_retries()
await self.update_queue_metrics()

def parse_kafka_message(self, msg: Any) -> DLQMessage:
"""Parse a raw Kafka ConsumerRecord into a DLQMessage."""
data = json.loads(msg.value)
headers = {k: v.decode() for k, v in (msg.headers or [])}
return DLQMessage(**data, dlq_offset=msg.offset, dlq_partition=msg.partition, headers=headers)

def parse_dlq_body(
self, data: dict[str, Any], offset: int, partition: int, headers: dict[str, str]
) -> DLQMessage:
"""Parse a deserialized DLQ message body into a DLQMessage."""
return DLQMessage(**data, dlq_offset=offset, dlq_partition=partition, headers=headers)

async def handle_message(self, message: DLQMessage) -> None:
"""Process a single DLQ message: filter → store → decide retry/discard."""
for filter_func in self._filters:
Expand Down Expand Up @@ -166,14 +153,14 @@ async def retry_message(self, message: DLQMessage) -> None:
retry_topic = f"{message.original_topic}{self.retry_topic_suffix}"

hdrs: dict[str, str] = {
"event_type": message.event.event_type,
"dlq_retry_count": str(message.retry_count + 1),
"dlq_original_error": message.error,
"dlq_retry_timestamp": datetime.now(timezone.utc).isoformat(),
}
hdrs = inject_trace_context(hdrs)

event = message.event
serialized = json.dumps(event.model_dump(mode="json")).encode()
serialized = await self.schema_registry.serialize_event(message.event)
Comment thread
HardMax71 marked this conversation as resolved.
Outdated

await self._broker.publish(
message=serialized,
Expand Down
4 changes: 2 additions & 2 deletions backend/app/domain/events/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
CreatePodCommandEvent,
DeletePodCommandEvent,
DomainEvent,
DomainEventAdapter,
EventMetadata,
# Execution Events
ExecutionAcceptedEvent,
Expand Down Expand Up @@ -90,7 +91,6 @@
UserRegisteredEvent,
UserSettingsUpdatedEvent,
UserUpdatedEvent,
domain_event_adapter,
)

__all__ = [
Expand Down Expand Up @@ -119,7 +119,7 @@
"DomainEvent",
"EventMetadata",
"ResourceUsageDomain",
"domain_event_adapter",
"DomainEventAdapter",
# Execution Events
"ExecutionRequestedEvent",
"ExecutionAcceptedEvent",
Expand Down
2 changes: 1 addition & 1 deletion backend/app/domain/events/typed.py
Original file line number Diff line number Diff line change
Expand Up @@ -703,4 +703,4 @@ class ArchivedEvent(AvroBase):
]

# TypeAdapter for polymorphic loading - validates raw data to correct typed event
domain_event_adapter: TypeAdapter[DomainEvent] = TypeAdapter(DomainEvent)
DomainEventAdapter: TypeAdapter[DomainEvent] = TypeAdapter(DomainEvent)
Loading
Loading