Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

♻️🎨 Improves error handling in the api-server #5866

Merged
merged 31 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from 30 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
Empty file.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from starlette.responses import RedirectResponse

from ..._meta import API_VTAG
from ...exceptions.service_errors_utils import DEFAULT_BACKEND_SERVICE_STATUS_CODES
from ...models.pagination import Page, PaginationParams
from ...models.schemas.errors import ErrorGet
from ...models.schemas.files import (
Expand All @@ -39,7 +40,6 @@
FileUploadData,
UploadLinks,
)
from ...services.service_exception_handling import DEFAULT_BACKEND_SERVICE_STATUS_CODES
from ...services.storage import StorageApi, StorageFileMetaData, to_file_api_model
from ..dependencies.authentication import get_current_user_id
from ..dependencies.services import get_api_client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
from collections.abc import Callable
from typing import Annotated

from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import PlainTextResponse
from models_library.app_diagnostics import AppStatusCheck
from servicelib.aiohttp import status

from ..._meta import API_VERSION, PROJECT_NAME
from ...core.health_checker import ApiServerHealthChecker, get_health_checker
Expand All @@ -19,16 +20,15 @@
router = APIRouter()


class HealtchCheckException(RuntimeError):
"""Failed a health check"""


@router.get("/", include_in_schema=False, response_class=PlainTextResponse)
async def check_service_health(
health_checker: Annotated[ApiServerHealthChecker, Depends(get_health_checker)]
):
if not health_checker.healthy:
raise HealtchCheckException()
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="unhealthy"
)

return f"{__name__}@{datetime.datetime.now(tz=datetime.timezone.utc).isoformat()}"


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
from pydantic import ValidationError
from pydantic.errors import PydanticValueError

from ...exceptions.service_errors_utils import DEFAULT_BACKEND_SERVICE_STATUS_CODES
from ...models.basic_types import VersionStr
from ...models.pagination import OnePage, Page, PaginationParams
from ...models.schemas.errors import ErrorGet
from ...models.schemas.solvers import Solver, SolverKeyId, SolverPort
from ...services.catalog import CatalogApi
from ...services.service_exception_handling import DEFAULT_BACKEND_SERVICE_STATUS_CODES
from ..dependencies.application import get_reverse_url_mapper
from ..dependencies.authentication import get_current_user_id, get_product_name
from ..dependencies.services import get_api_client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
from typing import Annotated, Any

from fastapi import APIRouter, Depends, Request, status
from fastapi.exceptions import HTTPException
from models_library.api_schemas_webserver.projects import ProjectCreateNew, ProjectGet
from models_library.clusters import ClusterID
from pydantic.types import PositiveInt

from ...exceptions.service_errors_utils import DEFAULT_BACKEND_SERVICE_STATUS_CODES
from ...models.basic_types import VersionStr
from ...models.schemas.errors import ErrorGet
from ...models.schemas.jobs import (
Expand All @@ -23,7 +23,6 @@
from ...models.schemas.solvers import Solver, SolverKeyId
from ...services.catalog import CatalogApi
from ...services.director_v2 import DirectorV2Api
from ...services.service_exception_handling import DEFAULT_BACKEND_SERVICE_STATUS_CODES
from ...services.solver_job_models_converters import (
create_job_from_project,
create_jobstatus_from_task,
Expand All @@ -33,7 +32,6 @@
from ..dependencies.authentication import get_current_user_id, get_product_name
from ..dependencies.services import get_api_client
from ..dependencies.webserver import AuthSession, get_webserver_session
from ..errors.http_error import create_error_json_response
from ._common import API_SERVER_DEV_FEATURES_ENABLED
from ._jobs import start_project, stop_project

Expand Down Expand Up @@ -247,24 +245,16 @@ async def replace_job_custom_metadata(
job_name = _compose_job_resource_name(solver_key, version, job_id)
_logger.debug("Custom metadata for '%s'", job_name)

try:
project_metadata = await webserver_api.update_project_metadata(
project_id=job_id, metadata=update.metadata
)
return JobMetadata(
project_metadata = await webserver_api.update_project_metadata(
project_id=job_id, metadata=update.metadata
)
return JobMetadata(
job_id=job_id,
metadata=project_metadata.custom,
url=url_for(
"replace_job_custom_metadata",
solver_key=solver_key,
version=version,
job_id=job_id,
metadata=project_metadata.custom,
url=url_for(
"replace_job_custom_metadata",
solver_key=solver_key,
version=version,
job_id=job_id,
),
)

except HTTPException as err:
if err.status_code == status.HTTP_404_NOT_FOUND:
return create_error_json_response(
f"Cannot find job={job_name} ",
status_code=status.HTTP_404_NOT_FOUND,
)
),
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# pylint: disable=too-many-arguments
# pylint: disable=W0613

import logging
from collections import deque
Expand All @@ -16,12 +15,15 @@
from models_library.api_schemas_webserver.wallets import WalletGetWithAvailableCredits
from models_library.projects_nodes_io import BaseFileLink
from models_library.users import UserID
from models_library.wallets import ZERO_CREDITS
from pydantic import NonNegativeInt
from pydantic.types import PositiveInt
from servicelib.fastapi.requests_decorators import cancel_on_disconnect
from servicelib.logging_utils import log_context
from starlette.background import BackgroundTask

from ...exceptions.custom_errors import InsufficientCreditsError, MissingWalletError
from ...exceptions.service_errors_utils import DEFAULT_BACKEND_SERVICE_STATUS_CODES
from ...models.basic_types import LogStreamingResponse, VersionStr
from ...models.pagination import Page, PaginationParams
from ...models.schemas.errors import ErrorGet
Expand All @@ -31,7 +33,6 @@
from ...services.catalog import CatalogApi
from ...services.director_v2 import DirectorV2Api, DownloadLink, NodeName
from ...services.log_streaming import LogDistributor, LogStreamer
from ...services.service_exception_handling import DEFAULT_BACKEND_SERVICE_STATUS_CODES
from ...services.solver_job_models_converters import create_job_from_project
from ...services.solver_job_outputs import ResultsTypes, get_solver_output_results
from ...services.storage import StorageApi, to_file_api_model
Expand All @@ -41,8 +42,6 @@
from ..dependencies.rabbitmq import get_log_check_timeout, get_log_distributor
from ..dependencies.services import get_api_client
from ..dependencies.webserver import AuthSession, get_webserver_session
from ..errors.custom_errors import InsufficientCredits, MissingWallet
from ..errors.http_error import create_error_json_response
from ._common import API_SERVER_DEV_FEATURES_ENABLED
from ._jobs import raise_if_job_not_associated_with_solver
from .solvers_jobs import (
Expand Down Expand Up @@ -232,12 +231,13 @@ async def get_job_outputs(
if product_price is not None:
wallet = await webserver_api.get_project_wallet(project_id=project.uuid)
if wallet is None:
msg = f"Job {project.uuid} does not have an associated wallet."
raise MissingWallet(msg)
raise MissingWalletError(job_id=project.uuid)
wallet_with_credits = await webserver_api.get_wallet(wallet_id=wallet.wallet_id)
if wallet_with_credits.available_credits < 0.0:
msg = f"Wallet '{wallet_with_credits.name}' does not have any credits. Please add some before requesting solver ouputs"
raise InsufficientCredits(msg)
if wallet_with_credits.available_credits <= ZERO_CREDITS:
raise InsufficientCreditsError(
wallet_name=wallet_with_credits.name,
wallet_credit_amount=wallet_with_credits.available_credits,
)

outputs: dict[str, ResultsTypes] = await get_solver_output_results(
user_id=user_id,
Expand Down Expand Up @@ -345,25 +345,17 @@ async def get_job_custom_metadata(
job_name = _compose_job_resource_name(solver_key, version, job_id)
_logger.debug("Custom metadata for '%s'", job_name)

try:
project_metadata = await webserver_api.get_project_metadata(project_id=job_id)
return JobMetadata(
project_metadata = await webserver_api.get_project_metadata(project_id=job_id)
return JobMetadata(
job_id=job_id,
metadata=project_metadata.custom,
url=url_for(
"get_job_custom_metadata",
solver_key=solver_key,
version=version,
job_id=job_id,
metadata=project_metadata.custom,
url=url_for(
"get_job_custom_metadata",
solver_key=solver_key,
version=version,
job_id=job_id,
),
)

except HTTPException as err:
if err.status_code == status.HTTP_404_NOT_FOUND:
return create_error_json_response(
f"Cannot find job={job_name} ",
status_code=status.HTTP_404_NOT_FOUND,
)
),
)


@router.get(
Expand Down Expand Up @@ -428,6 +420,8 @@ async def get_log_stream(
user_id: Annotated[UserID, Depends(get_current_user_id)],
log_check_timeout: Annotated[NonNegativeInt, Depends(get_log_check_timeout)],
):
assert request # nosec

job_name = _compose_job_resource_name(solver_key, version, job_id)
with log_context(
_logger, logging.DEBUG, f"Streaming logs for {job_name=} and {user_id=}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

from fastapi import APIRouter, Depends, Security, status

from ...exceptions.service_errors_utils import DEFAULT_BACKEND_SERVICE_STATUS_CODES
from ...models.schemas.errors import ErrorGet
from ...models.schemas.profiles import Profile, ProfileUpdate
from ...services.service_exception_handling import DEFAULT_BACKEND_SERVICE_STATUS_CODES
from ...services.webserver import AuthSession
from ..dependencies.webserver import get_webserver_session

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from fastapi import APIRouter, Depends, status
from models_library.api_schemas_webserver.wallets import WalletGetWithAvailableCredits

from ...exceptions.service_errors_utils import DEFAULT_BACKEND_SERVICE_STATUS_CODES
from ...models.schemas.errors import ErrorGet
from ...services.service_exception_handling import DEFAULT_BACKEND_SERVICE_STATUS_CODES
from ..dependencies.webserver import AuthSession, get_webserver_session
from ._common import API_SERVER_DEV_FEATURES_ENABLED

Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,16 @@
import logging

from fastapi import FastAPI
from fastapi.exceptions import RequestValidationError
from fastapi_pagination import add_pagination
from httpx import HTTPError as HttpxException
from models_library.basic_types import BootModeEnum
from servicelib.fastapi.profiler_middleware import ProfilerMiddleware
from servicelib.logging_utils import config_all_loggers
from starlette import status
from starlette.exceptions import HTTPException

from .. import exceptions
from .._meta import API_VERSION, API_VTAG
from ..api.errors.custom_errors import CustomBaseError, custom_error_handler
from ..api.errors.http_error import (
http_error_handler,
make_http_error_handler_for_exception,
)
from ..api.errors.httpx_client_error import handle_httpx_client_exceptions
from ..api.errors.log_handling_error import log_handling_error_handler
from ..api.errors.validation_error import http422_error_handler
from ..api.root import create_router
from ..api.routes.health import router as health_router
from ..services import catalog, director_v2, storage, webserver
from ..services.log_streaming import LogDistributionBaseException
from ..services.rabbitmq import setup_rabbitmq
from ._prometheus_instrumentation import setup_prometheus_instrumentation
from .events import create_start_app_handler, create_stop_app_handler
Expand Down Expand Up @@ -96,31 +84,10 @@ def init_app(settings: ApplicationSettings | None = None) -> FastAPI:
app.add_event_handler("startup", create_start_app_handler(app))
app.add_event_handler("shutdown", create_stop_app_handler(app))

app.add_exception_handler(HTTPException, http_error_handler)
app.add_exception_handler(HttpxException, handle_httpx_client_exceptions)
app.add_exception_handler(RequestValidationError, http422_error_handler)
app.add_exception_handler(LogDistributionBaseException, log_handling_error_handler)
app.add_exception_handler(CustomBaseError, custom_error_handler)

# SEE https://docs.python.org/3/library/exceptions.html#exception-hierarchy
app.add_exception_handler(
NotImplementedError,
make_http_error_handler_for_exception(
NotImplementedError,
status.HTTP_501_NOT_IMPLEMENTED,
detail_message="Endpoint not implemented",
),
)
app.add_exception_handler(
Exception,
make_http_error_handler_for_exception(
Exception,
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail_message="Unexpected error",
add_exception_to_message=(settings.SC_BOOT_MODE == BootModeEnum.DEBUG),
add_oec_to_message=True,
),
exceptions.setup_exception_handlers(
app, is_debug=settings.SC_BOOT_MODE == BootModeEnum.DEBUG
)

if settings.API_SERVER_PROFILING:
app.add_middleware(ProfilerMiddleware)

Expand Down
Empty file.
Loading