From 3db62931866652a7e01ec8671dd5a9080beb56bd Mon Sep 17 00:00:00 2001 From: Pedro Crespo <32402063+pcrespov@users.noreply.github.com> Date: Sat, 9 Apr 2022 13:09:17 +0200 Subject: [PATCH] fixes and annotations --- .../projects/_delete.py | 25 +++++++---------- .../projects/projects_api.py | 27 ++++++++++++------- .../projects/projects_db.py | 6 ++--- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/projects/_delete.py b/services/web/server/src/simcore_service_webserver/projects/_delete.py index 7308a2d1c81..026ce8fdb41 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_delete.py +++ b/services/web/server/src/simcore_service_webserver/projects/_delete.py @@ -3,10 +3,8 @@ """ import asyncio -import functools import logging from typing import Callable, List -from uuid import UUID from aiohttp import web from models_library.projects import ProjectID @@ -39,13 +37,13 @@ async def mark_project_as_deleted(app: web.Application, project_uuid: ProjectID) # TODO: note that if any of the steps below fail, it might results in a # services/projects/data that might be incosistent. The GC should # be able to detect that and resolve it. - await db.set_hidden_flag(f"{project_uuid}", enabled=True) + await db.set_hidden_flag(project_uuid, enabled=True) async def delete_project( app: web.Application, - project_uuid: str, - user_id: int, + project_uuid: ProjectID, + user_id: UserID, # TODO: this function was tmp added here to avoid refactoring all projects_api in a single PR remove_project_dynamic_services: Callable, ) -> None: @@ -64,7 +62,7 @@ async def delete_project( db: ProjectDBAPI = app[APP_PROJECT_DBAPI] try: - mark_project_as_deleted(app, project_uuid) + await mark_project_as_deleted(app, project_uuid) # stops dynamic services # - raises ProjectNotFoundError, UserNotFoundError, ProjectLockError @@ -74,13 +72,13 @@ async def delete_project( # stops computational services # - raises DirectorServiceError - await director_v2_api.delete_pipeline(app, user_id, UUID(project_uuid)) + await director_v2_api.delete_pipeline(app, user_id, project_uuid) # rm data from storage await delete_data_folders_of_project(app, project_uuid, user_id) # rm project from database - await db.delete_user_project(user_id, project_uuid) + await db.delete_user_project(user_id, f"{project_uuid}") except ProjectLockError as err: raise ProjectDeleteError( @@ -95,8 +93,8 @@ async def delete_project( def create_delete_project_background_task( app: web.Application, - project_uuid: str, - user_id: int, + project_uuid: ProjectID, + user_id: UserID, remove_project_dynamic_services: Callable, logger: logging.Logger, ) -> asyncio.Task: @@ -122,7 +120,7 @@ def _log_errors(fut: asyncio.Future): project_uuid, user_id ) - task.add_done_callback(functools.partial(_log_errors, log)) + task.add_done_callback(_log_errors) return task @@ -143,8 +141,3 @@ def is_delete_project_background_task_running( tasks = get_delete_project_background_tasks(project_uuid, user_id) assert len(tasks) <= 1 # nosec return len(tasks) > 0 - - -# alias -create_background_task = create_delete_project_background_task -is_background_task_running = is_delete_project_background_task_running diff --git a/services/web/server/src/simcore_service_webserver/projects/projects_api.py b/services/web/server/src/simcore_service_webserver/projects/projects_api.py index c5dfed7601e..042f1b82e66 100644 --- a/services/web/server/src/simcore_service_webserver/projects/projects_api.py +++ b/services/web/server/src/simcore_service_webserver/projects/projects_api.py @@ -18,6 +18,7 @@ from uuid import UUID, uuid4 from aiohttp import web +from models_library.projects import ProjectID from models_library.projects_state import ( Owner, ProjectLocked, @@ -26,6 +27,7 @@ ProjectStatus, RunningState, ) +from models_library.users import UserID from pydantic.types import PositiveInt from servicelib.aiohttp.application_keys import APP_JSONSCHEMA_SPECS_KEY from servicelib.aiohttp.jsonschema_validation import validate_instance @@ -186,8 +188,13 @@ async def start_project_interactive_services( log.error("Error while starting dynamic service %s", f"{entry=}") -async def delete_project(app: web.Application, project_uuid: str, user_id: int) -> None: +async def delete_project( + app: web.Application, project_uuid: ProjectID, user_id: UserID +) -> asyncio.Task: """ + Returns the background task to delete project using user permissions. If the + task is already running, it returns existing task otherwise it creates a new one. + The returned task can be ignored to implement a fire&forget or followed up with add_done_callback raises ProjectDeleteError """ @@ -197,15 +204,17 @@ async def delete_project(app: web.Application, project_uuid: str, user_id: int) except ProjectNotFoundError as err: raise ProjectDeleteError(project_uuid, reason=f"Invalid project {err}") from err - # If you have multiple tasks deleting the same project then one will atmost succeed and - # the rest always fail (because the project is deleted). - in_progress: bool = _delete.is_background_task_running(project_uuid, user_id) + # Ensures + tasks: List[asyncio.Task] = _delete.get_delete_project_background_tasks( + project_uuid, user_id + ) + assert len(tasks) <= 1, f"{tasks=}" # nosec + if tasks: + return tasks[0] - # This avoids having a burst of background tasks that were fire&forget failing - if not in_progress: - await _delete.create_background_task( - app, project_uuid, user_id, remove_project_dynamic_services, log - ) + return await _delete.create_delete_project_background_task( + app, project_uuid, user_id, remove_project_dynamic_services, log + ) @observe(event="SIGNAL_USER_DISCONNECTED") diff --git a/services/web/server/src/simcore_service_webserver/projects/projects_db.py b/services/web/server/src/simcore_service_webserver/projects/projects_db.py index 3ff4a80e72b..acb55c43186 100644 --- a/services/web/server/src/simcore_service_webserver/projects/projects_db.py +++ b/services/web/server/src/simcore_service_webserver/projects/projects_db.py @@ -21,7 +21,7 @@ from aiopg.sa.connection import SAConnection from aiopg.sa.result import RowProxy from change_case import ChangeCase -from models_library.projects import ProjectAtDB, ProjectIDStr +from models_library.projects import ProjectAtDB, ProjectID, ProjectIDStr from pydantic import ValidationError from pydantic.types import PositiveInt from servicelib.aiohttp.application_keys import APP_DB_ENGINE_KEY @@ -864,12 +864,12 @@ async def update_project_without_checking_permissions( ) return result.rowcount == 1 - async def set_hidden_flag(self, project_uuid: str, enabled: bool): + async def set_hidden_flag(self, project_uuid: ProjectID, enabled: bool): async with self.engine.acquire() as conn: stmt = ( projects.update() .values(hidden=enabled) - .where(projects.c.uuid == project_uuid) + .where(projects.c.uuid == f"{project_uuid}") ) await conn.execute(stmt)