Skip to content

Commit 84d6975

Browse files
committed
error handling
1 parent 206cd7f commit 84d6975

File tree

3 files changed

+116
-24
lines changed

3 files changed

+116
-24
lines changed

services/web/server/src/simcore_service_webserver/projects/_trash_api.py

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from ..director_v2 import api as director_v2_api
1414
from . import projects_api
15-
from .exceptions import ProjectLockError, StopProjectBeforeTrashError
15+
from .exceptions import ProjectLockError, ProjectRunningConflictError, ProjectStopError
1616
from .models import ProjectPatchExtended
1717
from .settings import get_plugin_settings
1818

@@ -49,28 +49,43 @@ async def trash_project(
4949
user_id: UserID,
5050
project_id: ProjectID,
5151
trashed: bool,
52+
forced: bool = True,
5253
):
5354
if trashed:
5455
# stop first
55-
try:
56-
await projects_api.remove_project_dynamic_services(
57-
user_id=user_id,
58-
project_uuid=f"{project_id}",
59-
app=app,
60-
simcore_user_agent=UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE,
61-
notify_users=False,
62-
)
6356

64-
await director_v2_api.delete_pipeline(
65-
app, user_id=user_id, project_id=project_id
57+
if forced:
58+
try:
59+
await projects_api.remove_project_dynamic_services(
60+
user_id=user_id,
61+
project_uuid=f"{project_id}",
62+
app=app,
63+
simcore_user_agent=UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE,
64+
notify_users=False,
65+
)
66+
67+
await director_v2_api.delete_pipeline(
68+
app, user_id=user_id, project_id=project_id
69+
)
70+
except (DirectorServiceError, ProjectLockError) as exc:
71+
raise ProjectStopError(
72+
project_uuid=project_id,
73+
user_id=user_id,
74+
product_name=product_name,
75+
from_err=exc,
76+
) from exc
77+
else:
78+
79+
running = await director_v2_api.is_pipeline_running(
80+
app=app, user_id=user_id, project_id=project_id
6681
)
67-
except (DirectorServiceError, ProjectLockError) as exc:
68-
raise StopProjectBeforeTrashError(
69-
project_uuid=project_id,
70-
user_id=user_id,
71-
product_name=product_name,
72-
from_err=exc,
73-
) from exc
82+
# NOTE: must do here as well for dynamic services but needs refactoring!
83+
if running:
84+
raise ProjectRunningConflictError(
85+
project_uuid=project_id,
86+
user_id=user_id,
87+
product_name=product_name,
88+
)
7489

7590
# mark as trash
7691
await projects_api.patch_project(

services/web/server/src/simcore_service_webserver/projects/_trash_handlers.py

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,94 @@
1+
import functools
2+
import logging
3+
from typing import NamedTuple
4+
15
from aiohttp import web
26
from servicelib.aiohttp import status
37
from servicelib.aiohttp.requests_validation import parse_request_path_parameters_as
8+
from servicelib.aiohttp.typing_extension import Handler
9+
from servicelib.aiohttp.web_exceptions_extension import get_http_error_class_or_none
10+
from servicelib.logging_errors import create_troubleshotting_log_kwargs
11+
from servicelib.status_codes_utils import is_5xx_server_error
412

513
from .._meta import API_VTAG as VTAG
614
from ..login.decorators import get_user_id, login_required
715
from ..products.api import get_product_name
816
from ..projects._common_models import ProjectPathParams
917
from ..security.decorators import permission_required
1018
from . import _trash_api
19+
from .exceptions import ProjectRunningConflictError, ProjectStopError, ProjectTrashError
20+
21+
_logger = logging.getLogger(__name__)
22+
23+
24+
class HttpErrorInfo(NamedTuple):
25+
status_code: int
26+
msg_template: str
27+
28+
29+
_TO_HTTP_ERROR_MAP: dict[type[Exception], HttpErrorInfo] = {
30+
ProjectRunningConflictError: HttpErrorInfo(
31+
status.HTTP_409_CONFLICT,
32+
"Current study is in use and cannot be trashed [{project_uuid}]",
33+
),
34+
ProjectStopError: HttpErrorInfo(
35+
status.HTTP_503_SERVICE_UNAVAILABLE,
36+
"Something went wrong while stopping services before trashing. Aborting trash.",
37+
),
38+
}
39+
40+
41+
class _DefaultDict(dict):
42+
def __missing__(self, key):
43+
return f"'{key}=?'"
44+
45+
46+
def _handle_request_exceptions(handler: Handler):
47+
@functools.wraps(handler)
48+
async def _wrapper(request: web.Request) -> web.StreamResponse:
49+
try:
50+
return await handler(request)
51+
52+
except ProjectTrashError as exc:
53+
for exc_cls, http_error_info in _TO_HTTP_ERROR_MAP.items():
54+
if isinstance(exc, exc_cls):
55+
56+
# safe formatting
57+
user_msg = http_error_info.msg_template.format_map(
58+
_DefaultDict(getattr(exc, "__dict__", {}))
59+
)
60+
61+
http_error_cls = get_http_error_class_or_none(
62+
http_error_info.status_code
63+
)
64+
assert http_error_cls # nosec
65+
66+
if is_5xx_server_error(http_error_info.status_code):
67+
_logger.exception(
68+
**create_troubleshotting_log_kwargs(
69+
user_msg,
70+
error=exc,
71+
error_context={
72+
"request": request,
73+
"request.remote": f"{request.remote}",
74+
"request.method": f"{request.method}",
75+
"request.path": f"{request.path}",
76+
},
77+
)
78+
)
79+
raise http_error_cls(reason=user_msg) from exc
80+
raise
81+
82+
return _wrapper
83+
1184

1285
routes = web.RouteTableDef()
1386

1487

1588
@routes.delete(f"/{VTAG}/trash", name="empty_trash")
1689
@login_required
1790
@permission_required("project.delete")
91+
@_handle_request_exceptions
1892
async def empty_trash(request: web.Request):
1993
user_id = get_user_id(request)
2094
product_name = get_product_name(request)
@@ -26,14 +100,10 @@ async def empty_trash(request: web.Request):
26100
return web.json_response(status=status.HTTP_204_NO_CONTENT)
27101

28102

29-
#
30-
# Projects
31-
#
32-
33-
34103
@routes.post(f"/{VTAG}/projects/{{project_id}}:trash", name="trash_project")
35104
@login_required
36105
@permission_required("project.delete")
106+
@_handle_request_exceptions
37107
async def trash_project(request: web.Request):
38108
user_id = get_user_id(request)
39109
product_name = get_product_name(request)
@@ -53,6 +123,7 @@ async def trash_project(request: web.Request):
53123
@routes.post(f"/{VTAG}/projects/{{project_id}}:untrash", name="untrash_project")
54124
@login_required
55125
@permission_required("project.delete")
126+
@_handle_request_exceptions
56127
async def untrash_project(request: web.Request):
57128
user_id = get_user_id(request)
58129
product_name = get_product_name(request)

services/web/server/src/simcore_service_webserver/projects/exceptions.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,16 @@ class ProjectTrashError(BaseProjectError):
8282
...
8383

8484

85-
class StopProjectBeforeTrashError(ProjectTrashError):
85+
class ProjectStopError(ProjectTrashError):
8686
msg_template = "Failed to services in '{project_uuid}' before trashing"
8787

8888

89+
class ProjectRunningConflictError(ProjectTrashError):
90+
msg_template = (
91+
"Cannot trash running project '{project_uuid}' except if forced option is on"
92+
)
93+
94+
8995
class NodeNotFoundError(BaseProjectError):
9096
msg_template = "Node '{node_uuid}' not found in project '{project_uuid}'"
9197

0 commit comments

Comments
 (0)