generated from ghga-de/microservice-repository-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for clearing Kafka topics
- Loading branch information
1 parent
c0837b2
commit b1d8784
Showing
7 changed files
with
239 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
# Copyright 2021 - 2024 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln | ||
# for the German Human Genome-Phenome Archive (GHGA) | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
"""FastAPI routes for Kafka state management.""" | ||
|
||
import re | ||
from typing import Annotated | ||
|
||
from fastapi import APIRouter, HTTPException, Query, status | ||
|
||
from sms.adapters.inbound.fastapi_ import dummies | ||
from sms.adapters.inbound.fastapi_.http_authorization import ( | ||
TokenAuthContext, | ||
require_token, | ||
) | ||
|
||
KAFKA_TOPIC_PATTERN = r"^[a-zA-Z0-9._-]+$" | ||
|
||
kafka_router = APIRouter() | ||
|
||
|
||
def validate_kafka_topic_name(topic: str) -> None: | ||
"""Validate a Kafka topic name, raising a ValueError if invalid.""" | ||
if not re.match(KAFKA_TOPIC_PATTERN, topic): | ||
raise ValueError(f"Invalid topic name: {topic}") | ||
|
||
|
||
@kafka_router.delete( | ||
"/", | ||
operation_id="clear_topics", | ||
summary="Clear events from the given topic(s).", | ||
description=( | ||
"If no topics are specified, all topics will be cleared, except internal" | ||
+ " topics unless otherwise specified." | ||
), | ||
status_code=status.HTTP_204_NO_CONTENT, | ||
) | ||
async def clear_topics( | ||
events_handler: dummies.EventsHandlerPortDummy, | ||
_token: Annotated[TokenAuthContext, require_token], | ||
topics: list[str] = Query( | ||
default=[], | ||
description="The topic(s) to clear.", | ||
), | ||
exclude_internal: bool = Query( | ||
True, description="Whether to exclude internal topics." | ||
), | ||
) -> None: | ||
"""Clear messages from given topic(s). | ||
If no topics are specified, all topics will be cleared, except internal topics | ||
unless otherwise specified. | ||
Args: | ||
- `topics`: The topic(s) to clear. | ||
- `exclude_internal`: Whether to exclude internal topics. | ||
- `events_handler`: The events handler to use. | ||
Raises: | ||
- `HTTPException`: If an error occurs. | ||
""" | ||
try: | ||
for topic in topics: | ||
validate_kafka_topic_name(topic) | ||
except ValueError as err: | ||
raise HTTPException( | ||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(err) | ||
) from err | ||
|
||
try: | ||
await events_handler.clear_topics( | ||
topics=topics, exclude_internal=exclude_internal | ||
) | ||
except Exception as err: | ||
raise HTTPException( | ||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(err) | ||
) from err |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
# Copyright 2021 - 2024 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln | ||
# for the German Human Genome-Phenome Archive (GHGA) | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
"""Contains implementation of the EventsHandler class.""" | ||
|
||
from aiokafka import TopicPartition | ||
from aiokafka.admin import AIOKafkaAdminClient, RecordsToDelete | ||
|
||
from sms.config import Config | ||
from sms.ports.inbound.events_handler import EventsHandlerPort | ||
|
||
|
||
class EventsHandler(EventsHandlerPort): | ||
"""A class to manage the state of kafka events.""" | ||
|
||
def __init__(self, *, config: Config): | ||
self._config = config | ||
|
||
def get_admin_client(self) -> AIOKafkaAdminClient: | ||
"""Construct and return an instance of AIOKafkaAdminClient.""" | ||
return AIOKafkaAdminClient(bootstrap_servers=self._config.kafka_servers) | ||
|
||
async def clear_topics( | ||
self, *, topics: str | list[str], exclude_internal: bool = True | ||
): | ||
"""Clear messages from given topic(s). | ||
If no topics are specified, all topics will be cleared, except internal topics | ||
unless otherwise specified. | ||
""" | ||
admin_client = self.get_admin_client() | ||
await admin_client.start() | ||
try: | ||
if not topics: | ||
topics = await admin_client.list_topics() | ||
elif isinstance(topics, str): | ||
topics = [topics] | ||
if exclude_internal: | ||
topics = [topic for topic in topics if not topic.startswith("__")] | ||
topics_info = await admin_client.describe_topics(topics) | ||
records_to_delete = { | ||
TopicPartition( | ||
topic=topic_info["topic"], partition=partition_info["partition"] | ||
): RecordsToDelete(before_offset=-1) | ||
for topic_info in topics_info | ||
for partition_info in topic_info["partitions"] | ||
} | ||
await admin_client.delete_records(records_to_delete, timeout_ms=10000) | ||
finally: | ||
await admin_client.close() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# Copyright 2021 - 2024 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln | ||
# for the German Human Genome-Phenome Archive (GHGA) | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
"""Defines the API of a class that interfaces between inbound requests and kafka.""" | ||
|
||
from abc import ABC, abstractmethod | ||
|
||
|
||
class EventsHandlerPort(ABC): | ||
"""A class to manage the state of kafka events.""" | ||
|
||
@abstractmethod | ||
async def clear_topics( | ||
self, *, topics: str | list[str], exclude_internal: bool = True | ||
): | ||
"""Clear messages from given topic(s). | ||
If no topics are specified, all topics will be cleared, except internal topics | ||
unless otherwise specified. | ||
""" | ||
... |