diff --git a/configs/dev.yaml b/configs/dev.yaml index 900a974..e843b98 100644 --- a/configs/dev.yaml +++ b/configs/dev.yaml @@ -1,11 +1,11 @@ HOST: 0.0.0.0 -PORT: 8000 +PORT: 7776 -COMMENT_SERVICE_HOST: 0.0.0.0 +COMMENT_SERVICE_HOST: localhost COMMENT_SERVICE_PORT: 7002 -RATING_SERVICE_HOST: 0.0.0.0 +RATING_SERVICE_HOST: localhost RATING_SERVICE_PORT: 7003 -MOD_SERVICE_HOST: 0.0.0.0 +MOD_SERVICE_HOST: localhost MOD_SERVICE_PORT: 7004 diff --git a/src/gateway/clients/base_client.py b/src/gateway/clients/base_client.py new file mode 100644 index 0000000..d818e79 --- /dev/null +++ b/src/gateway/clients/base_client.py @@ -0,0 +1,55 @@ +import logging +from typing import Self +from collections.abc import Awaitable, Callable + +import grpc +import grpc.aio +from google.protobuf import message as _message + +from ..helpers.retry import grpc_retry + +logger = logging.getLogger(__name__) + + +class GrpcError(Exception): + pass + + +class GrpcClient: + def __init__(self, channel: grpc.aio.Channel): + self._channel = channel + self._stub = None + self._initialize_stub() + + def _initialize_stub(self) -> None: + raise NotImplementedError() + + @grpc_retry() # type: ignore + async def call( + self, + rpc_method: Callable[["_message.Message"], Awaitable["_message.Message"]], + request: "_message.Message", + *, + timeout: int = 30, + ) -> "_message.Message": + try: + response: _message.Message = await rpc_method(request, timeout=timeout) # type: ignore[operator] + return response + except grpc.RpcError as e: + logger.error(f"gRPC ошибка {rpc_method.__name__ if hasattr(rpc_method, '__name__') else rpc_method}: {e}") + raise GrpcError(f"gRPC Ошибка вызова: {e}") from e + except Exception as e: + logger.error( + f"Неизвестная ошибка при вызове {rpc_method.__name__ if hasattr(rpc_method, '__name__') else rpc_method}: {e}" + ) + raise + + async def close(self) -> None: + if self._channel: + await self._channel.close() + + async def __aenter__(self) -> Self: + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: # type: ignore + await self.close() diff --git a/src/gateway/clients/client_factory.py b/src/gateway/clients/client_factory.py new file mode 100644 index 0000000..a585626 --- /dev/null +++ b/src/gateway/clients/client_factory.py @@ -0,0 +1,45 @@ +import grpc + +from gateway.clients.comment import CommentServiceClient +from gateway.clients.mod import ModServiceClient +from gateway.clients.rating import RatingServiceClient + + +class GrpcClientFactory: + def __init__(self, comment_service_url: str, mod_service_url: str, rating_service_url: str) -> None: + self._comment_service_url = comment_service_url + self._mod_service_url = mod_service_url + self._rating_service_url = rating_service_url + self._channels: dict[str, grpc.aio.Channel] = {} # На всякий случай + + def _add_to_channels(self, url: str) -> None: + if url not in self._channels: + self._channels[url] = grpc.aio.insecure_channel(url) + + def get_comment_client(self) -> CommentServiceClient: + url = self._comment_service_url + self._add_to_channels(url) + + return CommentServiceClient(self._channels[url]) + + def get_mod_client(self) -> ModServiceClient: + url = self._mod_service_url + self._add_to_channels(url) + + return ModServiceClient(self._channels[url]) + + def get_rating_client(self) -> RatingServiceClient: + url = self._rating_service_url + self._add_to_channels(url) + + return RatingServiceClient(self._channels[url]) + + async def close_all(self) -> None: + for channel in self._channels.values(): + await channel.close() + + async def __aenter__(self): # type: ignore + return self + + async def __aexit__(self, exc_type, exc, tb): # type: ignore + await self.close_all() diff --git a/src/gateway/clients/comment.py b/src/gateway/clients/comment.py index 8edb9f5..8447c97 100644 --- a/src/gateway/clients/comment.py +++ b/src/gateway/clients/comment.py @@ -1,33 +1,26 @@ -import grpc -import gateway.stubs.comment_pb2 -import gateway.stubs.comment_pb2_grpc -from gateway.settings import Settings -_settings = Settings() -_channel = grpc.insecure_channel(_settings.comment_service_url) -_stub = gateway.stubs.comment_pb2_grpc.CommentServiceStub(_channel) # type: ignore +from gateway.stubs import comment_pb2, comment_pb2_grpc +from .base_client import GrpcClient -def create_comment_rpc(mod_id: int, author_id: int, text: str) -> gateway.stubs.comment_pb2.CreateCommentResponse: - req = gateway.stubs.comment_pb2.CreateCommentRequest(mod_id=mod_id, author_id=author_id, text=text) - return _stub.CreateComment(req) # type: ignore +class CommentServiceClient(GrpcClient): + def _initialize_stub(self) -> None: + self._stub = comment_pb2_grpc.CommentServiceStub(self._channel) # type: ignore -def edit_comment_rpc(comment_id: int, text: str) -> gateway.stubs.comment_pb2.EditCommentResponse: - req = gateway.stubs.comment_pb2.EditCommentRequest(comment_id=comment_id, text=text) - return _stub.EditComment(req) # type: ignore + async def create_comment(self, mod_id: int, author_id: int, text: str) -> comment_pb2.CreateCommentResponse: + request = comment_pb2.CreateCommentRequest(mod_id=mod_id, author_id=author_id, text=text) + return await self.call(self._stub.CreateComment, request) + async def edit_comment(self, comment_id: int, text: str) -> comment_pb2.EditCommentResponse: + request = comment_pb2.EditCommentRequest(comment_id=comment_id, text=text) + return await self.call(self._stub.EditComment, request) -def delete_comment_rpc( - comment_id: int, -) -> gateway.stubs.comment_pb2.DeleteCommentResponse: - req = gateway.stubs.comment_pb2.DeleteCommentRequest(comment_id=comment_id) - return _stub.DeleteComment(req) # type: ignore + async def delete_comment(self, comment_id: int) -> comment_pb2.DeleteCommentResponse: + request = comment_pb2.DeleteCommentRequest(comment_id=comment_id) + return await self.call(self._stub.DeleteComment, request) - -def get_comments_rpc( - mod_id: int, -) -> gateway.stubs.comment_pb2.GetCommentsResponse: - req = gateway.stubs.comment_pb2.GetCommentsRequest(mod_id=mod_id) - return _stub.GetComments(req) # type: ignore + async def get_comments(self, mod_id: int) -> comment_pb2.GetCommentsResponse: + request = comment_pb2.GetCommentsRequest(mod_id=mod_id) + return await self.call(self._stub.GetComments, request) diff --git a/src/gateway/clients/mod.py b/src/gateway/clients/mod.py index 52143ed..749877f 100644 --- a/src/gateway/clients/mod.py +++ b/src/gateway/clients/mod.py @@ -1,39 +1,28 @@ -import grpc -import gateway.stubs.mod_pb2 -import gateway.stubs.mod_pb2_grpc -from gateway.converters.mod_status_converter import graphql_to_proto_mod_status -from gateway.settings import Settings -_settings = Settings() -_channel = grpc.insecure_channel(_settings.mod_service_url) -_stub = gateway.stubs.mod_pb2_grpc.ModServiceStub(_channel) # type: ignore +from gateway.stubs import mod_pb2, mod_pb2_grpc +from .base_client import GrpcClient -def create_mod_rpc( - title: str, author_id: int, filename: str, description: str -) -> gateway.stubs.mod_pb2.CreateModResponse: - req = gateway.stubs.mod_pb2.CreateModRequest( - title=title, - author_id=author_id, - filename=filename, - description=description, - ) - return _stub.CreateMod(req) # type: ignore +class ModServiceClient(GrpcClient): + def _initialize_stub(self) -> None: + self._stub = mod_pb2_grpc.ModServiceStub(self._channel) # type: ignore -def set_status_mod_rpc(mod_id: int, status: str) -> gateway.stubs.mod_pb2.SetStatusResponse: - req = gateway.stubs.mod_pb2.SetStatusRequest(mod_id=mod_id, status=graphql_to_proto_mod_status(status)) # type: ignore - return _stub.SetStatus(req) # type: ignore + async def create_mod( + self, title: str, author_id: int, filename: str, description: str + ) -> mod_pb2.CreateModResponse: + request = mod_pb2.CreateModRequest(title=title, author_id=author_id, filename=filename, description=description) + return await self.call(self._stub.CreateMod, request) + async def set_status_mod(self, mod_id: int, status: str) -> mod_pb2.SetStatusResponse: + request = mod_pb2.SetStatusRequest(mod_id=mod_id, status=status) + return await self.call(self._stub.SetStatus, request) -def get_mod_download_link_rpc( - mod_id: int, -) -> gateway.stubs.mod_pb2.GetModDownloadLinkResponse: - req = gateway.stubs.mod_pb2.GetModDownloadLinkRequest(mod_id=mod_id) - return _stub.GetModDownloadLink(req) # type: ignore + async def get_mod_download_link(self, mod_id: int) -> mod_pb2.GetModDownloadLinkResponse: + request = mod_pb2.GetModDownloadLinkRequest(mod_id=mod_id) + return await self.call(self._stub.GetModDownloadLink, request) - -def get_mods_rpc() -> gateway.stubs.mod_pb2.GetModsResponse: - req = gateway.stubs.mod_pb2.GetModsRequest() - return _stub.GetMods(req) # type: ignore + async def get_mods(self) -> mod_pb2.GetModsResponse: + request = mod_pb2.GetModsRequest() + return await self.call(self._stub.GetMods, request) diff --git a/src/gateway/clients/rating.py b/src/gateway/clients/rating.py index 822388b..4df61d5 100644 --- a/src/gateway/clients/rating.py +++ b/src/gateway/clients/rating.py @@ -1,19 +1,18 @@ -import grpc -import gateway.stubs.rating_pb2 -import gateway.stubs.rating_pb2_grpc -from gateway.settings import Settings -_settings = Settings() -_channel = grpc.insecure_channel(_settings.rating_service_url) -_stub = gateway.stubs.rating_pb2_grpc.RatingServiceStub(_channel) # type: ignore +from gateway.stubs import rating_pb2, rating_pb2_grpc +from .base_client import GrpcClient -def rate_mod_rpc(mod_id: int, author_id: int, rate: str) -> gateway.stubs.rating_pb2.RateModResponse: - req = gateway.stubs.rating_pb2.RateModRequest(mod_id=mod_id, author_id=author_id, rate=rate) - return _stub.RateMod(req) # type: ignore +class RatingServiceClient(GrpcClient): + def _initialize_stub(self) -> None: + self._stub = rating_pb2_grpc.RatingServiceStub(self._channel) # type: ignore -def get_rates_rpc(mod_id: int) -> gateway.stubs.rating_pb2.GetRatesResponse: - req = gateway.stubs.rating_pb2.GetRatesRequest(mod_id=mod_id) - return _stub.GetRates(req) # type: ignore + async def rate_mod(self, mod_id: int, author_id: int, rate: str) -> rating_pb2.RateModResponse: + request = rating_pb2.RateModRequest(mod_id=mod_id, author_id=author_id, rate=rate) + return await self.call(self._stub.RateMod, request) + + async def get_rates(self, mod_id: int) -> rating_pb2.GetRatesResponse: + request = rating_pb2.GetRatesRequest(mod_id=mod_id) + return await self.call(self._stub.GetRates, request) diff --git a/src/gateway/esclient_graphql.py b/src/gateway/esclient_graphql.py new file mode 100644 index 0000000..aa7bd70 --- /dev/null +++ b/src/gateway/esclient_graphql.py @@ -0,0 +1,11 @@ +from typing import Any + +from gateway.clients.base_client import GrpcClient + + +class GQLContextViewer: + def __init__(self) -> None: + self.clients: dict[str, GrpcClient] = {} + + def get_current(self, request: Any) -> dict[str, Any]: + return {"request": request, "clients": self.clients} diff --git a/src/gateway/helpers/retry.py b/src/gateway/helpers/retry.py new file mode 100644 index 0000000..4eb8140 --- /dev/null +++ b/src/gateway/helpers/retry.py @@ -0,0 +1,53 @@ +import logging + +import grpc +from tenacity import RetryCallState, retry, retry_if_exception, stop_after_attempt, wait_exponential + +logger = logging.getLogger(__name__) + +NON_RETRYABLE = {grpc.StatusCode.UNIMPLEMENTED, grpc.StatusCode.INVALID_ARGUMENT, grpc.StatusCode.NOT_FOUND} + +RETRY_ATTEMPTS = 3 +RETRY_DELAY_MULTIPLIER = 1 +RETRY_DELAY_MIN = 1 +RETRY_DELAY_MAX = 10 + + +def is_retryable_grpc_exception(exc: BaseException) -> bool: + if isinstance(exc, grpc.RpcError): + code = exc.code() if hasattr(exc, "code") else None + return code not in NON_RETRYABLE + return False + + +def log_retry_attempt(retry_state: RetryCallState) -> None: + if retry_state.attempt_number < 1: + return + + if retry_state.outcome is None or not retry_state.outcome.failed or not hasattr(retry_state.outcome, "exception"): + return + + try: + exception = retry_state.outcome.exception() + if exception is None: + return + + sleep_time = getattr(retry_state.next_action, "sleep", 0) if retry_state.next_action else 0 + + logger.warning( + f"Повторная попытка {retry_state.attempt_number}/{RETRY_ATTEMPTS} " + f"для gRPC вызова. Ошибка: {exception} " + f"(следующая попытка через {sleep_time:.1f} сек)" + ) + except Exception as e: + logger.debug(f"Ошибка при логировании повторной попытки: {e}") + + +def grpc_retry(): # type: ignore + return retry( + stop=stop_after_attempt(RETRY_ATTEMPTS), + wait=wait_exponential(multiplier=RETRY_DELAY_MULTIPLIER, min=RETRY_DELAY_MIN, max=RETRY_DELAY_MAX), + retry=retry_if_exception(is_retryable_grpc_exception), + reraise=True, + before_sleep=log_retry_attempt, + ) diff --git a/src/gateway/resolvers/grpc_error_wrapper.py b/src/gateway/resolvers/grpc_error_wrapper.py new file mode 100644 index 0000000..315ef59 --- /dev/null +++ b/src/gateway/resolvers/grpc_error_wrapper.py @@ -0,0 +1,36 @@ +import asyncio +from collections.abc import Callable +from functools import wraps +from typing import Any + +from graphql import GraphQLError + +from ..clients.base_client import GrpcError + + +def handle_grpc_errors(func: Callable) -> Callable: # type: ignore + if asyncio.iscoroutinefunction(func): + + @wraps(func) + async def async_wrapper(*args, **kwargs) -> Any: # type: ignore + try: + return await func(*args, **kwargs) + except GrpcError as e: + raise GraphQLError(f"gRPC ошибка: {e}") from None + except Exception as e: + raise GraphQLError(f"Неизвестная ошибка: {e}") from None + + return async_wrapper + + else: + + @wraps(func) + def sync_wrapper(*args, **kwargs) -> Any: # type: ignore + try: + return func(*args, **kwargs) + except GrpcError as e: + raise GraphQLError(f"gRPC ошибка: {e}") from None + except Exception as e: + raise GraphQLError(f"Неизвестная ошибка: {e}") from None + + return sync_wrapper diff --git a/src/gateway/resolvers/mutation/comment.py b/src/gateway/resolvers/mutation/comment.py index 1596843..97e1902 100644 --- a/src/gateway/resolvers/mutation/comment.py +++ b/src/gateway/resolvers/mutation/comment.py @@ -4,13 +4,10 @@ from graphql import GraphQLResolveInfo from pydantic import BaseModel, field_validator -from gateway.clients.comment import ( - create_comment_rpc, - delete_comment_rpc, - edit_comment_rpc, -) from gateway.helpers.id_helper import validate_and_convert_id +from ..grpc_error_wrapper import handle_grpc_errors + comment_mutation = ObjectType("CommentMutation") @@ -29,9 +26,11 @@ def _author_id(cls, v: Any) -> int: @comment_mutation.field("createComment") -def resolve_create_comment(parent: object, info: GraphQLResolveInfo, input: CreateCommentInput) -> str: +@handle_grpc_errors +async def resolve_create_comment(parent: object, info: GraphQLResolveInfo, input: CreateCommentInput) -> str: data = CreateCommentInput.model_validate(input) - resp = create_comment_rpc(data.mod_id, data.author_id, data.text) + client = info.context["clients"]["comment_service"] + resp = await client.create_comment(data.mod_id, data.author_id, data.text) return str(resp.comment_id) @@ -45,10 +44,12 @@ def _comment_id(cls, v: Any) -> int: @comment_mutation.field("editComment") -def resolve_edit_comment(parent: object, info: GraphQLResolveInfo, input: EditCommentInput) -> bool: +@handle_grpc_errors +async def resolve_edit_comment(parent: object, info: GraphQLResolveInfo, input: EditCommentInput) -> bool: data = EditCommentInput.model_validate(input) - resp = edit_comment_rpc(data.comment_id, data.text) - return resp.success + client = info.context["clients"]["comment_service"] + resp = await client.edit_comment(data.comment_id, data.text) + return resp.success # type: ignore class DeleteCommentInput(BaseModel): @@ -60,7 +61,9 @@ def _comment_id(cls, v: Any) -> int: @comment_mutation.field("deleteComment") -def resolve_delete_comment(parent: object, info: GraphQLResolveInfo, input: DeleteCommentInput) -> bool: +@handle_grpc_errors +async def resolve_delete_comment(parent: object, info: GraphQLResolveInfo, input: DeleteCommentInput) -> bool: data = DeleteCommentInput.model_validate(input) - resp = delete_comment_rpc(data.comment_id) - return resp.success + client = info.context["clients"]["comment_service"] + resp = await client.delete_comment(data.comment_id) + return resp.success # type: ignore diff --git a/src/gateway/resolvers/mutation/mod.py b/src/gateway/resolvers/mutation/mod.py index b6117ba..d343b64 100644 --- a/src/gateway/resolvers/mutation/mod.py +++ b/src/gateway/resolvers/mutation/mod.py @@ -5,9 +5,10 @@ from graphql import GraphQLResolveInfo from pydantic import BaseModel, field_validator -from gateway.clients.mod import create_mod_rpc, set_status_mod_rpc from gateway.helpers.id_helper import validate_and_convert_id +from ..grpc_error_wrapper import handle_grpc_errors + mod_mutation = ObjectType("ModMutation") @@ -36,9 +37,11 @@ class CreateModResult(BaseModel): @mod_mutation.field("createMod") -def resolve_create_mod(parent: object, info: GraphQLResolveInfo, input: CreateModInput) -> dict[str, Any]: +@handle_grpc_errors +async def resolve_create_mod(parent: object, info: GraphQLResolveInfo, input: CreateModInput) -> dict[str, Any]: data = CreateModInput.model_validate(input) - resp = create_mod_rpc(data.title, data.author_id, data.filename, data.description) + client = info.context["clients"]["mod_service"] + resp = await client.create_mod(data.title, data.author_id, data.filename, data.description) return CreateModResult(mod_id=resp.mod_id, s3_key=resp.s3_key, upload_url=resp.upload_url).model_dump() @@ -52,7 +55,9 @@ def validate_mod_id(cls, v: Any) -> int: @mod_mutation.field("setStatus") -def resolve_set_status_mod(parent: object, info: GraphQLResolveInfo, input: SetStatusInput) -> bool: +@handle_grpc_errors +async def resolve_set_status_mod(parent: object, info: GraphQLResolveInfo, input: SetStatusInput) -> bool: data = SetStatusInput.model_validate(input) - resp = set_status_mod_rpc(data.mod_id, data.status.value) - return resp.success + client = info.context["clients"]["mod_service"] + resp = await client.set_status_mod(data.mod_id, data.status.value) + return resp.success # type: ignore diff --git a/src/gateway/resolvers/mutation/rating.py b/src/gateway/resolvers/mutation/rating.py index a57bc48..8cc4c98 100644 --- a/src/gateway/resolvers/mutation/rating.py +++ b/src/gateway/resolvers/mutation/rating.py @@ -5,9 +5,10 @@ from graphql import GraphQLResolveInfo from pydantic import BaseModel, field_validator -from gateway.clients.rating import rate_mod_rpc from gateway.helpers.id_helper import validate_and_convert_id +from ..grpc_error_wrapper import handle_grpc_errors + rating_mutation = ObjectType("RatingMutation") @@ -35,7 +36,9 @@ def validate_author_id(cls, v: Any) -> int: @rating_mutation.field("addRate") -def resolve_add_rate(parent: object, info: GraphQLResolveInfo, input: AddRateInput) -> str: +@handle_grpc_errors +async def resolve_add_rate(parent: object, info: GraphQLResolveInfo, input: AddRateInput) -> str: data = AddRateInput.model_validate(input) - resp = rate_mod_rpc(data.mod_id, data.author_id, data.rate.value) + client = info.context["clients"]["rating_service"] + resp = await client.rate_mod(data.mod_id, data.author_id, data.rate.value) return str(resp.rate_id) diff --git a/src/gateway/resolvers/mutation/root.py b/src/gateway/resolvers/mutation/root.py index 89efc59..11c2486 100644 --- a/src/gateway/resolvers/mutation/root.py +++ b/src/gateway/resolvers/mutation/root.py @@ -3,19 +3,24 @@ from ariadne import MutationType from graphql import GraphQLResolveInfo +from ..grpc_error_wrapper import handle_grpc_errors + mutation = MutationType() @mutation.field("comment") +@handle_grpc_errors def resolve_comment_root(obj: Any, info: GraphQLResolveInfo, **kwargs: Any) -> dict[str, Any]: return {} @mutation.field("mod") +@handle_grpc_errors def resolve_mod_root(obj: Any, info: GraphQLResolveInfo, **kwargs: Any) -> dict[str, Any]: return {} @mutation.field("rating") +@handle_grpc_errors def resolve_rating_root(obj: Any, info: GraphQLResolveInfo, **kwargs: Any) -> dict[str, Any]: return {} diff --git a/src/gateway/resolvers/query/comment.py b/src/gateway/resolvers/query/comment.py index 8001d0c..00942ae 100644 --- a/src/gateway/resolvers/query/comment.py +++ b/src/gateway/resolvers/query/comment.py @@ -4,9 +4,10 @@ from graphql import GraphQLResolveInfo from pydantic import BaseModel, field_validator -from gateway.clients.comment import get_comments_rpc from gateway.helpers.id_helper import validate_and_convert_id +from ..grpc_error_wrapper import handle_grpc_errors + class GetCommentsInput(BaseModel): mod_id: int @@ -32,9 +33,13 @@ def _edited_at(cls, v: Any) -> Any | None: @comment_query.field("getComments") -def resolve_get_comments(parent: object, info: GraphQLResolveInfo, input: GetCommentsInput) -> list[dict[str, Any]]: +@handle_grpc_errors +async def resolve_get_comments( + parent: object, info: GraphQLResolveInfo, input: GetCommentsInput +) -> list[dict[str, Any]]: data = GetCommentsInput.model_validate(input) - resp = get_comments_rpc(data.mod_id) + client = info.context["clients"]["comment_service"] + resp = await client.get_comments(data.mod_id) return [ GetCommentsResult( id=item.id, diff --git a/src/gateway/resolvers/query/mod.py b/src/gateway/resolvers/query/mod.py index 138b44b..a91f488 100644 --- a/src/gateway/resolvers/query/mod.py +++ b/src/gateway/resolvers/query/mod.py @@ -4,10 +4,11 @@ from graphql import GraphQLResolveInfo from pydantic import BaseModel, field_validator -from gateway.clients.mod import get_mod_download_link_rpc, get_mods_rpc from gateway.converters.mod_status_converter import proto_to_graphql_mod_status from gateway.helpers.id_helper import validate_and_convert_id +from ..grpc_error_wrapper import handle_grpc_errors + class GetModDownloadLinkInput(BaseModel): mod_id: int @@ -21,15 +22,21 @@ def _mod_id(cls, v: Any) -> int: @mod_query.field("getModDownloadLink") -def resolve_get_mod_download_link(parent: object, info: GraphQLResolveInfo, input: GetModDownloadLinkInput) -> str: +@handle_grpc_errors +async def resolve_get_mod_download_link( + parent: object, info: GraphQLResolveInfo, input: GetModDownloadLinkInput +) -> str: data = GetModDownloadLinkInput.model_validate(input) - resp = get_mod_download_link_rpc(data.mod_id) - return resp.link_url + client = info.context["clients"]["mod_service"] + resp = await client.get_mod_download_link(data.mod_id) + return resp.link_url # type: ignore @mod_query.field("getMods") -def resolve_get_mods(parent: object, info: GraphQLResolveInfo) -> list[dict[str, Any]]: - resp = get_mods_rpc() +@handle_grpc_errors +async def resolve_get_mods(parent: object, info: GraphQLResolveInfo) -> list[dict[str, Any]]: + client = info.context["clients"]["mod_service"] + resp = await client.get_mods() return [ { "id": item.id, diff --git a/src/gateway/resolvers/query/root.py b/src/gateway/resolvers/query/root.py index abb9957..b1413c1 100644 --- a/src/gateway/resolvers/query/root.py +++ b/src/gateway/resolvers/query/root.py @@ -3,14 +3,18 @@ from ariadne import QueryType from graphql import GraphQLResolveInfo +from ..grpc_error_wrapper import handle_grpc_errors + query = QueryType() @query.field("comment") +@handle_grpc_errors def resolve_comment_root(obj: Any, info: GraphQLResolveInfo, **kwargs: Any) -> dict[str, Any]: return {} @query.field("mod") +@handle_grpc_errors def resolve_mod_root(obj: Any, info: GraphQLResolveInfo, **kwargs: Any) -> dict[str, Any]: return {} diff --git a/src/gateway/server.py b/src/gateway/server.py index 02b4063..9f2b914 100644 --- a/src/gateway/server.py +++ b/src/gateway/server.py @@ -1,8 +1,12 @@ +import asyncio +import logging + import uvicorn from ariadne import load_schema_from_path, make_executable_schema from ariadne.asgi import GraphQL from ariadne.explorer import ExplorerGraphiQL +from gateway.clients.client_factory import GrpcClientFactory from gateway.resolvers.mutation.comment import comment_mutation from gateway.resolvers.mutation.mod import mod_mutation from gateway.resolvers.mutation.root import mutation @@ -11,6 +15,11 @@ from gateway.resolvers.query.root import query from gateway.settings import Settings +from .esclient_graphql import GQLContextViewer + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + settings = Settings() type_defs = load_schema_from_path("src/gateway/schema") @@ -25,16 +34,46 @@ mod_mutation, ) -app = GraphQL( - schema, - debug=True, - explorer=ExplorerGraphiQL(), +context_viewer = GQLContextViewer() +clients_factory = GrpcClientFactory( + comment_service_url=settings.comment_service_url, + mod_service_url=settings.mod_service_url, + rating_service_url=settings.rating_service_url, ) -if __name__ == "__main__": - uvicorn.run( - "gateway.server:app", - host=settings.host, - port=settings.port, - reload=True, +app = GraphQL(schema, debug=True, explorer=ExplorerGraphiQL(), context_value=context_viewer.get_current) + + +async def main(): # type: ignore + logger.info("Инициализация gRPC клиентов...") + comment_client = clients_factory.get_comment_client() + mod_client = clients_factory.get_mod_client() + rating_client = clients_factory.get_rating_client() + logger.info("gRPC клиенты инициализированы.") + + context_viewer.clients = { + "comment_service": comment_client, + "mod_service": mod_client, + "rating_service": rating_client, + } + + app = GraphQL( + schema, + debug=True, + explorer=ExplorerGraphiQL(), + context_value=context_viewer.get_current, ) + + config = uvicorn.Config(app=app, host=settings.host, port=settings.port, reload=True) + server = uvicorn.Server(config) + + try: + await server.serve() + finally: + logger.info("Закрытие gRPC каналов...") + await clients_factory.close_all() + logger.info("Все соединения закрыты.") + + +if __name__ == "__main__": + asyncio.run(main()) # type: ignore