mirror of
https://github.com/hexolan/panels.git
synced 2026-03-26 20:41:15 +00:00
init comment-service
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import logging
|
||||
from typing import Type
|
||||
|
||||
from google.protobuf import message
|
||||
from aiokafka import AIOKafkaConsumer
|
||||
from aiokafka.structs import ConsumerRecord
|
||||
|
||||
from comment_service.models.config import Config
|
||||
from comment_service.models.service import CommentDBRepository
|
||||
|
||||
|
||||
class EventConsumer:
|
||||
"""An abstract consumer base class.
|
||||
|
||||
Attributes:
|
||||
CONSUMER_TOPIC: The topic to consume events from.
|
||||
CONSUMER_EVENT_TYPE (Type[message.Message]): The protobuf class type of the event msgs (used for deserialisation).
|
||||
_db_repo (Type[CommentDBRepository]): The repository interface for modifying data.
|
||||
_consumer (aiokafka.AIOKafkaConsumer): The underlying Kafka instance.
|
||||
|
||||
"""
|
||||
CONSUMER_TOPIC: str
|
||||
CONSUMER_EVENT_TYPE: Type[message.Message]
|
||||
|
||||
def __init__(self, config: Config, db_repo: Type[CommentDBRepository]) -> None:
|
||||
"""Initialise the event consumer.
|
||||
|
||||
Args:
|
||||
config (Config): The app configuration instance (to access brokers list).
|
||||
db_repo (Type[CommentDBRepository]): The repository interface for updating data.
|
||||
|
||||
"""
|
||||
self._db_repo = db_repo
|
||||
self._consumer = AIOKafkaConsumer(
|
||||
self.CONSUMER_TOPIC,
|
||||
bootstrap_servers=config.kafka_brokers,
|
||||
group_id="comment-service"
|
||||
)
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Begin consuming messages."""
|
||||
await self._consumer.start()
|
||||
try:
|
||||
async for msg in self._consumer:
|
||||
await self._process_msg(msg)
|
||||
except Exception as e:
|
||||
logging.error(f"error whilst consuming messages (on topic '{self.CONSUMER_TOPIC}'): {e}")
|
||||
finally:
|
||||
await self._consumer.stop()
|
||||
|
||||
async def _process_msg(self, msg: ConsumerRecord) -> None:
|
||||
"""Process a recieved message.
|
||||
|
||||
The messages are deserialise from bytes into their protobuf form,
|
||||
then passed to the `_process_event` method to handle the logic.
|
||||
|
||||
Args:
|
||||
msg (kafka.Message): The event to process.
|
||||
|
||||
"""
|
||||
try:
|
||||
event = self.CONSUMER_EVENT_TYPE()
|
||||
event.ParseFromString(msg.value)
|
||||
assert event.type != ""
|
||||
await self._process_event(event)
|
||||
except AssertionError:
|
||||
logging.error("invalid event recieved")
|
||||
return
|
||||
except Exception as e:
|
||||
logging.error("error whilst processing recieved event:", e)
|
||||
return
|
||||
|
||||
async def _process_event(self, event: Type[message.Message]) -> None:
|
||||
"""Process a recieved event.
|
||||
|
||||
Args:
|
||||
event (Type[message.Message]): The event serialised to protobuf form.
|
||||
|
||||
"""
|
||||
raise NotImplementedError("required consumer method (_process_event) not implemented")
|
||||
@@ -0,0 +1,33 @@
|
||||
import logging
|
||||
|
||||
from comment_service.models.proto import post_pb2
|
||||
from comment_service.events.base_consumer import EventConsumer
|
||||
|
||||
|
||||
class PostEventConsumer(EventConsumer):
|
||||
"""Consumer class responsible for 'post' events.
|
||||
|
||||
Attributes:
|
||||
CONSUMER_TOPIC: The topic to consume events from.
|
||||
CONSUMER_EVENT_TYPE (post_pb2.PostEvent): Kafka messages are serialised to this type.
|
||||
_db_repo (CommentDBRepository): The repository interface for modifying data.
|
||||
_consumer (aiokafka.AIOKafkaConsumer): The underlying Kafka instance.
|
||||
|
||||
"""
|
||||
CONSUMER_TOPIC = "post"
|
||||
CONSUMER_EVENT_TYPE = post_pb2.PostEvent
|
||||
|
||||
async def _process_event(self, event: post_pb2.PostEvent) -> None:
|
||||
"""Process a recieved event.
|
||||
|
||||
In response to a Post deleted event, delete any comments
|
||||
that fall under that post.
|
||||
|
||||
Args:
|
||||
event (post_pb2.PostEvent): The decoded protobuf message.
|
||||
|
||||
"""
|
||||
if event.type == "deleted":
|
||||
assert event.data.id != ""
|
||||
await self._db_repo.delete_post_comments(event.data.id)
|
||||
logging.info("succesfully processed PostEvent (type: 'deleted')")
|
||||
89
services/comment-service/comment_service/events/producer.py
Normal file
89
services/comment-service/comment_service/events/producer.py
Normal file
@@ -0,0 +1,89 @@
|
||||
import asyncio
|
||||
|
||||
from aiokafka import AIOKafkaProducer
|
||||
|
||||
from comment_service.models.config import Config
|
||||
from comment_service.models.service import Comment
|
||||
from comment_service.models.proto import comment_pb2
|
||||
|
||||
|
||||
class CommentEventProducer:
|
||||
"""Service event producer.
|
||||
|
||||
Attributes:
|
||||
_producer (aiokafka.AIOKafkaProducer): The underlying Kafka instance.
|
||||
_pending_sends (set): Background events that are still awaiting a response.
|
||||
|
||||
"""
|
||||
def __init__(self, config: Config) -> None:
|
||||
"""Initialise the event producer.
|
||||
|
||||
Args:
|
||||
config (Config): The app configuration instance (to access Kafka brokers list).
|
||||
|
||||
"""
|
||||
self._producer = AIOKafkaProducer(
|
||||
bootstrap_servers=config.kafka_brokers,
|
||||
client_id="auth-service"
|
||||
)
|
||||
self._pending_sends = set()
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start the underlying Kafka instance (open a connection)."""
|
||||
await self._producer.start()
|
||||
|
||||
async def _send_event(self, event: comment_pb2.CommentEvent) -> None:
|
||||
"""Send an event.
|
||||
|
||||
Execute the sending action as a background task to avoid
|
||||
delaying a response to the request.
|
||||
|
||||
Args:
|
||||
event (comment_pb2.CommentEvent): The protobuf class for the event.
|
||||
|
||||
"""
|
||||
bg_task = asyncio.create_task(self._producer.send(
|
||||
topic="comment",
|
||||
value=event.SerializeToString(),
|
||||
))
|
||||
self._pending_sends.add(bg_task)
|
||||
bg_task.add_done_callback(self._pending_sends.discard)
|
||||
|
||||
async def send_created_event(self, comment: Comment) -> None:
|
||||
"""Send a comment created event.
|
||||
|
||||
Args:
|
||||
comment (Comment): The comment to reference.
|
||||
|
||||
"""
|
||||
event = comment_pb2.CommentEvent(
|
||||
type="created",
|
||||
data=Comment.to_protobuf(comment)
|
||||
)
|
||||
await self._send_event(event)
|
||||
|
||||
async def send_updated_event(self, comment: Comment) -> None:
|
||||
"""Send a comment updated event.
|
||||
|
||||
Args:
|
||||
comment (Comment): The comment to reference.
|
||||
|
||||
"""
|
||||
event = comment_pb2.CommentEvent(
|
||||
type="updated",
|
||||
data=Comment.to_protobuf(comment)
|
||||
)
|
||||
await self._send_event(event)
|
||||
|
||||
async def send_deleted_event(self, comment: Comment) -> None:
|
||||
"""Send a comment deleted event.
|
||||
|
||||
Args:
|
||||
comment (Comment): The comment to reference.
|
||||
|
||||
"""
|
||||
event = comment_pb2.CommentEvent(
|
||||
type="deleted",
|
||||
data=Comment.to_protobuf(comment)
|
||||
)
|
||||
await self._send_event(event)
|
||||
67
services/comment-service/comment_service/events/service.py
Normal file
67
services/comment-service/comment_service/events/service.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import asyncio
|
||||
from typing import Type
|
||||
|
||||
from comment_service.models.config import Config
|
||||
from comment_service.models.service import CommentDBRepository
|
||||
from comment_service.events.producer import CommentEventProducer
|
||||
from comment_service.events.post_consumer import PostEventConsumer
|
||||
from comment_service.events.user_consumer import UserEventConsumer
|
||||
|
||||
|
||||
class EventConsumersWrapper:
|
||||
"""A wrapper class for starting the event consumers.
|
||||
|
||||
Attributes:
|
||||
_post_consumer (PostEventConsumer): A wrapped consumer.
|
||||
_user_consumer (UserEventConsumer): A wrapped consumer.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, post_consumer: PostEventConsumer, user_consumer: UserEventConsumer) -> None:
|
||||
"""Add the consumers to the wrapper
|
||||
|
||||
Args:
|
||||
post_consumer (PostEventConsumer): Initialised post consumer.
|
||||
user_consumer (UserEventConsumer): Initialised user consumer.
|
||||
|
||||
"""
|
||||
self._post_consumer = post_consumer
|
||||
self._user_consumer = user_consumer
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Begin consuming events on all the event consumers."""
|
||||
await asyncio.gather(
|
||||
self._post_consumer.start(),
|
||||
self._user_consumer.start()
|
||||
)
|
||||
|
||||
|
||||
def create_consumers(config: Config, db_repo: Type[CommentDBRepository]) -> EventConsumersWrapper:
|
||||
"""Initialse the event consumers and return them in a wrapper.
|
||||
|
||||
Args:
|
||||
config (Config): The app configuration instance.
|
||||
db_repo (Type[CommentDBRepository]): The database repo to pass to the consumers.
|
||||
|
||||
Returns:
|
||||
EventConsumerWrapper
|
||||
|
||||
"""
|
||||
post_consumer = PostEventConsumer(config, db_repo)
|
||||
user_consumer = UserEventConsumer(config, db_repo)
|
||||
return EventConsumersWrapper(post_consumer=post_consumer, user_consumer=user_consumer)
|
||||
|
||||
|
||||
async def create_producer(config: Config) -> CommentEventProducer:
|
||||
"""Create an event producer for the service.
|
||||
|
||||
Args:
|
||||
config (Config): The app configuration instance.
|
||||
|
||||
Returns:
|
||||
CommentEventProducer
|
||||
|
||||
"""
|
||||
producer = CommentEventProducer(config)
|
||||
await producer.start()
|
||||
return producer
|
||||
@@ -0,0 +1,33 @@
|
||||
import logging
|
||||
|
||||
from comment_service.models.proto import user_pb2
|
||||
from comment_service.events.base_consumer import EventConsumer
|
||||
|
||||
|
||||
class UserEventConsumer(EventConsumer):
|
||||
"""Consumer class responsible for 'user' events.
|
||||
|
||||
Attributes:
|
||||
CONSUMER_TOPIC: The topic to consume events from.
|
||||
CONSUMER_EVENT_TYPE (user_pb2.UserEvent): Kafka messages are serialised to this type.
|
||||
_db_repo (CommentDBRepository): The repository interface for modifying data.
|
||||
_consumer (aiokafka.AIOKafkaConsumer): The underlying Kafka instance.
|
||||
|
||||
"""
|
||||
CONSUMER_TOPIC = "user"
|
||||
CONSUMER_EVENT_TYPE = user_pb2.UserEvent
|
||||
|
||||
async def _process_event(self, event: user_pb2.UserEvent) -> None:
|
||||
"""Process a recieved event.
|
||||
|
||||
In response to a User deleted event, delete any comments
|
||||
created by that user.
|
||||
|
||||
Args:
|
||||
event (user_pb2.UserEvent): The decoded protobuf message.
|
||||
|
||||
"""
|
||||
if event.type == "deleted":
|
||||
assert event.data.id != ""
|
||||
await self._db_repo.delete_user_comments(event.data.id)
|
||||
logging.info("succesfully processed UserEvent (type: 'deleted')")
|
||||
Reference in New Issue
Block a user