Skip to content

Commit 50c2568

Browse files
✨ Check for associated jobs when deleting a function (#8342)
1 parent 96d5805 commit 50c2568

File tree

11 files changed

+171
-6
lines changed

11 files changed

+171
-6
lines changed

api/specs/web-server/_functions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
FunctionGroupPathParams,
2424
)
2525
from simcore_service_webserver.functions._controller._functions_rest_schemas import (
26+
FunctionDeleteQueryParams,
2627
FunctionGetQueryParams,
2728
FunctionPathParams,
2829
FunctionsListQueryParams,
@@ -80,6 +81,7 @@ async def update_function(
8081
)
8182
async def delete_function(
8283
_path: Annotated[FunctionPathParams, Depends()],
84+
_query: Annotated[as_query(FunctionDeleteQueryParams), Depends()],
8385
): ...
8486

8587

packages/models-library/src/models_library/functions_errors.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ class FunctionIDNotFoundError(FunctionBaseError):
1717
status_code: int = 404 # Not Found
1818

1919

20+
class FunctionHasJobsCannotDeleteError(FunctionBaseError):
21+
msg_template: str = (
22+
"Cannot delete function {function_id} because it has {jobs_count} associated job(s)."
23+
)
24+
status_code: int = 409 # Conflict
25+
26+
2027
class FunctionJobIDNotFoundError(FunctionBaseError):
2128
msg_template: str = "Function job {function_job_id} not found"
2229
status_code: int = 404 # Not Found

services/web/server/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.77.2
1+
0.78.0

services/web/server/setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 0.77.2
2+
current_version = 0.78.0
33
commit = True
44
message = services/webserver api version: {current_version} → {new_version}
55
tag = False

services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ openapi: 3.1.0
22
info:
33
title: simcore-service-webserver
44
description: Main service with an interface (http-API & websockets) to the web front-end
5-
version: 0.77.2
5+
version: 0.78.0
66
servers:
77
- url: ''
88
description: webserver
@@ -3788,6 +3788,13 @@ paths:
37883788
type: string
37893789
format: uuid
37903790
title: Function Id
3791+
- name: force
3792+
in: query
3793+
required: false
3794+
schema:
3795+
type: boolean
3796+
default: false
3797+
title: Force
37913798
responses:
37923799
'204':
37933800
description: Successful Response

services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
from .._services_metadata.proxy import ServiceMetadata
4545
from ._functions_rest_exceptions import handle_rest_requests_exceptions
4646
from ._functions_rest_schemas import (
47+
FunctionDeleteQueryParams,
4748
FunctionFilters,
4849
FunctionGetQueryParams,
4950
FunctionGroupPathParams,
@@ -370,12 +371,18 @@ async def update_function(request: web.Request) -> web.Response:
370371
async def delete_function(request: web.Request) -> web.Response:
371372
path_params = parse_request_path_parameters_as(FunctionPathParams, request)
372373
function_id = path_params.function_id
374+
375+
query_params: FunctionDeleteQueryParams = parse_request_query_parameters_as(
376+
FunctionDeleteQueryParams, request
377+
)
378+
373379
req_ctx = AuthenticatedRequestContext.model_validate(request)
374380
await _functions_service.delete_function(
375381
app=request.app,
376-
function_id=function_id,
377382
user_id=req_ctx.user_id,
378383
product_name=req_ctx.product_name,
384+
function_id=function_id,
385+
force=query_params.force,
379386
)
380387

381388
return web.json_response(status=status.HTTP_204_NO_CONTENT)

services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest_schemas.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,13 @@ class FunctionsListQueryParams(
7979
): ...
8080

8181

82+
class FunctionDeleteQueryParams(BaseModel):
83+
force: Annotated[
84+
bool,
85+
Field(
86+
description="If true, deletes the function even if it has associated jobs; otherwise, returns HTTP_409_CONFLICT if jobs exist.",
87+
),
88+
] = False
89+
90+
8291
__all__: tuple[str, ...] = ("AuthenticatedRequestContext",)

services/web/server/src/simcore_service_webserver/functions/_functions_repository.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from models_library.functions_errors import (
3535
FunctionBaseError,
3636
FunctionExecuteAccessDeniedError,
37+
FunctionHasJobsCannotDeleteError,
3738
FunctionIDNotFoundError,
3839
FunctionJobCollectionExecuteAccessDeniedError,
3940
FunctionJobCollectionIDNotFoundError,
@@ -819,6 +820,7 @@ async def delete_function(
819820
user_id: UserID,
820821
product_name: ProductName,
821822
function_id: FunctionID,
823+
force: bool = False,
822824
) -> None:
823825
async with transaction_context(get_asyncpg_engine(app), connection) as transaction:
824826
await check_user_permissions(
@@ -840,6 +842,20 @@ async def delete_function(
840842
if row is None:
841843
raise FunctionIDNotFoundError(function_id=function_id)
842844

845+
# Check for existing function jobs if force is not True
846+
if not force:
847+
jobs_result = await transaction.execute(
848+
function_jobs_table.select()
849+
.with_only_columns(func.count())
850+
.where(function_jobs_table.c.function_uuid == function_id)
851+
)
852+
jobs_count = jobs_result.scalar() or 0
853+
854+
if jobs_count > 0:
855+
raise FunctionHasJobsCannotDeleteError(
856+
function_id=function_id, jobs_count=jobs_count
857+
)
858+
843859
# Proceed with deletion
844860
await transaction.execute(
845861
functions_table.delete().where(functions_table.c.uuid == function_id)

services/web/server/src/simcore_service_webserver/functions/_functions_service.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,12 +353,14 @@ async def delete_function(
353353
user_id: UserID,
354354
product_name: ProductName,
355355
function_id: FunctionID,
356+
force: bool = False,
356357
) -> None:
357358
await _functions_repository.delete_function(
358359
app=app,
359360
user_id=user_id,
360361
product_name=product_name,
361362
function_id=function_id,
363+
force=force,
362364
)
363365

364366

services/web/server/tests/unit/with_dbs/04/functions/conftest.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from collections.abc import AsyncIterator, Awaitable, Callable
77
from contextlib import AsyncExitStack
88
from typing import Any
9-
from uuid import uuid4
9+
from uuid import UUID, uuid4
1010

1111
import pytest
1212
import sqlalchemy
@@ -31,6 +31,13 @@
3131
from simcore_postgres_database.models.funcapi_api_access_rights_table import (
3232
funcapi_api_access_rights_table,
3333
)
34+
from simcore_postgres_database.models.funcapi_function_jobs_table import (
35+
function_jobs_table,
36+
)
37+
from simcore_postgres_database.models.funcapi_functions_access_rights_table import (
38+
functions_access_rights_table,
39+
)
40+
from simcore_postgres_database.models.funcapi_functions_table import functions_table
3441
from simcore_service_webserver.application_settings import ApplicationSettings
3542
from simcore_service_webserver.statics._constants import FRONTEND_APP_DEFAULT
3643
from sqlalchemy.ext.asyncio import AsyncEngine
@@ -253,3 +260,71 @@ async def logged_user_function_api_access_rights(
253260
async with AsyncExitStack() as stack:
254261
row = await stack.enter_async_context(cm)
255262
yield row
263+
264+
265+
@pytest.fixture
266+
async def fake_function_with_associated_job(
267+
asyncpg_engine: AsyncEngine,
268+
logged_user: UserInfoDict,
269+
) -> AsyncIterator[UUID]:
270+
async with AsyncExitStack() as stack:
271+
function_row = await stack.enter_async_context(
272+
insert_and_get_row_lifespan(
273+
asyncpg_engine,
274+
table=functions_table,
275+
values={
276+
"title": "Test Function",
277+
"function_class": FunctionClass.PROJECT.value,
278+
"description": "A test function",
279+
"input_schema": {
280+
"schema_class": "application/schema+json",
281+
"schema_content": {
282+
"type": "object",
283+
"properties": {"input1": {"type": "string"}},
284+
},
285+
},
286+
"output_schema": {
287+
"schema_class": "application/schema+json",
288+
"schema_content": {
289+
"type": "object",
290+
"properties": {"output1": {"type": "string"}},
291+
},
292+
},
293+
"class_specific_data": {"project_id": f"{uuid4()}"},
294+
},
295+
pk_col=functions_table.c.uuid,
296+
)
297+
)
298+
299+
await stack.enter_async_context(
300+
insert_and_get_row_lifespan(
301+
asyncpg_engine,
302+
table=functions_access_rights_table,
303+
values={
304+
"function_uuid": function_row["uuid"],
305+
"group_id": logged_user["primary_gid"],
306+
"product_name": "osparc", # Default product name
307+
"read": True,
308+
"write": True,
309+
"execute": True,
310+
},
311+
pk_cols=[
312+
functions_access_rights_table.c.function_uuid,
313+
functions_access_rights_table.c.group_id,
314+
functions_access_rights_table.c.product_name,
315+
],
316+
)
317+
)
318+
319+
await stack.enter_async_context(
320+
insert_and_get_row_lifespan(
321+
asyncpg_engine,
322+
table=function_jobs_table,
323+
values={
324+
"function_uuid": function_row["uuid"],
325+
"status": "pending",
326+
},
327+
pk_col=function_jobs_table.c.uuid,
328+
)
329+
)
330+
yield function_row["uuid"]

0 commit comments

Comments
 (0)