completed local setup with compose
Some checks failed
CI/CD Pipeline / Generate SBOM (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Code Quality & Linting (push) Has been cancelled
CI/CD Pipeline / Policy Validation (push) Has been cancelled
CI/CD Pipeline / Test Suite (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-coverage) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-extract) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-firm-connectors) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-forms) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-hmrc) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-ingestion) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-kg) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-normalize-map) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-ocr) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rag-indexer) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rag-retriever) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-reason) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rpa) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (ui-review) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-coverage) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-extract) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-kg) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-rag-retriever) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (ui-review) (push) Has been cancelled
CI/CD Pipeline / Notifications (push) Has been cancelled
Some checks failed
CI/CD Pipeline / Generate SBOM (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Code Quality & Linting (push) Has been cancelled
CI/CD Pipeline / Policy Validation (push) Has been cancelled
CI/CD Pipeline / Test Suite (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-coverage) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-extract) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-firm-connectors) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-forms) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-hmrc) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-ingestion) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-kg) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-normalize-map) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-ocr) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rag-indexer) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rag-retriever) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-reason) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rpa) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (ui-review) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-coverage) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-extract) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-kg) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-rag-retriever) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (ui-review) (push) Has been cancelled
CI/CD Pipeline / Notifications (push) Has been cancelled
This commit is contained in:
140
libs/events/contrib/kafka_bus.py
Normal file
140
libs/events/contrib/kafka_bus.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""Kafka implementation of EventBus."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
import structlog
|
||||
from aiokafka import AIOKafkaConsumer, AIOKafkaProducer # type: ignore
|
||||
|
||||
from ..base import EventBus, EventPayload
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class KafkaEventBus(EventBus):
|
||||
"""Kafka implementation of EventBus"""
|
||||
|
||||
def __init__(self, bootstrap_servers: str):
|
||||
self.bootstrap_servers = bootstrap_servers.split(",")
|
||||
self.producer: AIOKafkaProducer | None = None
|
||||
self.consumers: dict[str, AIOKafkaConsumer] = {}
|
||||
self.handlers: dict[
|
||||
str, list[Callable[[str, EventPayload], Awaitable[None]]]
|
||||
] = {}
|
||||
self.running = False
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start Kafka producer"""
|
||||
if self.running:
|
||||
return
|
||||
|
||||
self.producer = AIOKafkaProducer(
|
||||
bootstrap_servers=",".join(self.bootstrap_servers),
|
||||
value_serializer=lambda v: v.encode("utf-8"),
|
||||
)
|
||||
await self.producer.start()
|
||||
self.running = True
|
||||
logger.info("Kafka event bus started", bootstrap_servers=self.bootstrap_servers)
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop Kafka producer and consumers"""
|
||||
if not self.running:
|
||||
return
|
||||
|
||||
if self.producer:
|
||||
await self.producer.stop()
|
||||
|
||||
for consumer in self.consumers.values():
|
||||
await consumer.stop()
|
||||
|
||||
self.running = False
|
||||
logger.info("Kafka event bus stopped")
|
||||
|
||||
async def publish(self, topic: str, payload: EventPayload) -> bool:
|
||||
"""Publish event to Kafka topic"""
|
||||
if not self.producer:
|
||||
raise RuntimeError("Event bus not started")
|
||||
|
||||
try:
|
||||
await self.producer.send_and_wait(topic, payload.to_json())
|
||||
logger.info(
|
||||
"Event published",
|
||||
topic=topic,
|
||||
event_id=payload.event_id,
|
||||
actor=payload.actor,
|
||||
tenant_id=payload.tenant_id,
|
||||
)
|
||||
return True
|
||||
except Exception as e: # pylint: disable=broad-exception-caught
|
||||
logger.error(
|
||||
"Failed to publish event",
|
||||
topic=topic,
|
||||
event_id=payload.event_id,
|
||||
error=str(e),
|
||||
)
|
||||
return False
|
||||
|
||||
async def subscribe(
|
||||
self, topic: str, handler: Callable[[str, EventPayload], Awaitable[None]]
|
||||
) -> None:
|
||||
"""Subscribe to Kafka topic"""
|
||||
if topic not in self.handlers:
|
||||
self.handlers[topic] = []
|
||||
self.handlers[topic].append(handler)
|
||||
|
||||
if topic not in self.consumers:
|
||||
consumer = AIOKafkaConsumer(
|
||||
topic,
|
||||
bootstrap_servers=",".join(self.bootstrap_servers),
|
||||
value_deserializer=lambda m: m.decode("utf-8"),
|
||||
group_id=f"tax-agent-{topic}",
|
||||
auto_offset_reset="latest",
|
||||
)
|
||||
self.consumers[topic] = consumer
|
||||
await consumer.start()
|
||||
|
||||
# Start consumer task
|
||||
asyncio.create_task(self._consume_messages(topic, consumer))
|
||||
|
||||
logger.info("Subscribed to topic", topic=topic)
|
||||
|
||||
async def _consume_messages(self, topic: str, consumer: AIOKafkaConsumer) -> None:
|
||||
"""Consume messages from Kafka topic"""
|
||||
try:
|
||||
async for message in consumer:
|
||||
try:
|
||||
if message.value is not None:
|
||||
payload_dict = json.loads(message.value)
|
||||
else:
|
||||
continue
|
||||
payload = EventPayload(
|
||||
data=payload_dict["data"],
|
||||
actor=payload_dict["actor"],
|
||||
tenant_id=payload_dict["tenant_id"],
|
||||
trace_id=payload_dict.get("trace_id"),
|
||||
schema_version=payload_dict.get("schema_version", "1.0"),
|
||||
)
|
||||
payload.event_id = payload_dict["event_id"]
|
||||
payload.occurred_at = payload_dict["occurred_at"]
|
||||
|
||||
# Call all handlers for this topic
|
||||
for handler in self.handlers.get(topic, []):
|
||||
try:
|
||||
await handler(topic, payload)
|
||||
except Exception as e: # pylint: disable=broad-exception-caught
|
||||
logger.error(
|
||||
"Handler failed",
|
||||
topic=topic,
|
||||
event_id=payload.event_id,
|
||||
handler=handler.__name__,
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error("Failed to decode message", topic=topic, error=str(e))
|
||||
except Exception as e: # pylint: disable=broad-exception-caught
|
||||
logger.error("Failed to process message", topic=topic, error=str(e))
|
||||
|
||||
except Exception as e: # pylint: disable=broad-exception-caught
|
||||
logger.error("Consumer error", topic=topic, error=str(e))
|
||||
Reference in New Issue
Block a user