-
Notifications
You must be signed in to change notification settings - Fork 31
🐛 Fix orphan projects left behind when project creation fails #9155
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
base: master
Are you sure you want to change the base?
Changes from all commits
8ff2573
d2a08a2
1230d51
6487bf4
a2842f1
859cf3b
9de2931
67c5d2a
8a501dc
28665aa
c694ec7
44e4d71
4f5a85b
8a38a2a
9f562ae
f1ff05a
702e613
3346af9
241247f
8e023c2
e0ba7a1
89209a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @GitHK thx for this fix. This PR interests me. I would like to talk to you about what went wrong int he creation of the pipelines. Perhaps we acn talk offline? thx |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,6 +65,26 @@ | |
|
|
||
| _logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| async def _best_effort_cleanup( | ||
| app: web.Application, | ||
| project_uuid: ProjectID, | ||
| user_id: UserID, | ||
| simcore_user_agent: str, | ||
| product_name: ProductName, | ||
| ) -> None: | ||
| try: | ||
| await _projects_service.submit_delete_project_task( | ||
| app=app, | ||
| project_uuid=project_uuid, | ||
| user_id=user_id, | ||
| simcore_user_agent=simcore_user_agent, | ||
| product_name=product_name, | ||
| ) | ||
| except Exception: # pylint: disable=broad-except | ||
| _logger.exception("Best-effort cleanup failed for project %s", project_uuid) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. create_troubleshooting_log_kwargs` ? Pls update |
||
|
|
||
|
|
||
| type CopyFileCoro = Coroutine[Any, Any, None] | ||
| type CopyProjectNodesCoro = Coroutine[Any, Any, dict[NodeID, ProjectNodeCreate]] | ||
|
|
||
|
|
@@ -444,11 +464,12 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche | |
| } | ||
|
|
||
| _project_product_name = await _projects_repository_legacy.get_project_product(project_uuid=new_project["uuid"]) | ||
| assert ( | ||
| _project_product_name == product_name # nosec | ||
| ), "Project product name mismatch" | ||
| if _project_product_name != product_name: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. pls dobule check this warning |
||
| raise web.HTTPBadRequest(text=f"Project product name mismatch {product_name=} {_project_product_name=}") | ||
| if project_uuid := new_project.get("uuid"): | ||
| await _best_effort_cleanup(app, project_uuid, user_id, simcore_user_agent, _project_product_name) | ||
| raise web.HTTPBadRequest( # noqa: TRY301 | ||
| text=f"Project product name mismatch {product_name=} {_project_product_name=}" | ||
| ) | ||
|
|
||
| data = ProjectGet.from_domain_model(new_project).model_dump(**RESPONSE_MODEL_POLICY) | ||
|
|
||
|
|
@@ -468,28 +489,30 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche | |
|
|
||
| except (ParentProjectNotFoundError, ParentNodeNotFoundError) as exc: | ||
| if project_uuid := new_project.get("uuid"): | ||
| await _projects_service.submit_delete_project_task( | ||
| app=app, | ||
| project_uuid=project_uuid, | ||
| user_id=user_id, | ||
| simcore_user_agent=simcore_user_agent, | ||
| product_name=product_name, | ||
| ) | ||
| await _best_effort_cleanup(app, project_uuid, user_id, simcore_user_agent, product_name) | ||
| raise web.HTTPNotFound(text=f"{exc}") from exc | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know this is not your PR but please let's avoid passing |
||
|
|
||
| except asyncio.CancelledError: | ||
| _logger.warning( | ||
| "cancelled create_project for '%s'. Cleaning up", | ||
| f"{user_id=}", | ||
| "cancelled create_project for user_id='%s'. Cleaning up", | ||
| user_id, | ||
| ) | ||
| if project_uuid := new_project.get("uuid"): | ||
| await _projects_service.submit_delete_project_task( | ||
| app=app, | ||
| project_uuid=project_uuid, | ||
| user_id=user_id, | ||
| simcore_user_agent=simcore_user_agent, | ||
| product_name=product_name, | ||
| ) | ||
| await _best_effort_cleanup(app, project_uuid, user_id, simcore_user_agent, product_name) | ||
| raise | ||
|
|
||
| except web.HTTPException: | ||
| # Pre-insertion validation HTTP errors (e.g. invalid data, not found, forbidden) | ||
| # do not need cleanup. Post-insertion cases handle their own cleanup before raising. | ||
| raise | ||
|
|
||
| except Exception: | ||
| _logger.exception( | ||
| "Unexpected error during create_project for user_id='%s'. Cleaning up", | ||
| user_id, | ||
| ) | ||
|
GitHK marked this conversation as resolved.
|
||
| if project_uuid := new_project.get("uuid"): | ||
| await _best_effort_cleanup(app, project_uuid, user_id, simcore_user_agent, product_name) | ||
| raise | ||
|
GitHK marked this conversation as resolved.
|
||
|
|
||
|
|
||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inside this module I'd say that we want to suppress the error to have the same equivalent behaviour as before. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it feels like this "cleanup" patterns repeats again and again
...
The
_best_effort_cleanuppattern appears in three places within the except block, and once inline before a manual raise:Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider this proposal, after some iterations with claude
1. The inline product mismatch case
The issue: it manually does cleanup then raises
HTTPBadRequest, bypassing the consistent except-block pattern. Fix: raise a domain exception instead, let the existingexcept Exceptionblock handle cleanup, and translate to HTTP in a dedicated handler.Now cleanup always flows through the except block, never inline.
2. Consolidating the pattern
A context manager that arms itself once the project is inserted and runs cleanup on any unhandled exception:
Usage in
create_project:All three
exceptcleanup arms collapse into the context manager. Theexcept web.HTTPException: raiselogic is preserved — pre-insertion HTTP errors pass through cleanly.One caveat: the
_ProjectProductMismatchErrorfrom point 1 still needs its own handler (or to be a non-HTTPExceptionbase), otherwise theexcept web.HTTPException: raisearm would swallow it before the context manager sees it. With the fix from point 1, it's a plain exception and gets caught byBaseExceptioncorrectly.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
THOUGHT:
_best_effort_cleanupreminds me as a loose implementation of the SAGA pattern that you used recently in other PR: the compensating action for "insert project" is "delete project." Nonetheless, the "best effort" qualifier acknowledges it's not atomic: the compensation itself can fail, which is the realistic trade-off outside of a true transaction :-)