Skip to content

Commit 8e84f97

Browse files
authored
🐛Fix blocking call in the garbage collector (ITISFoundation#2884)
1 parent 2755a10 commit 8e84f97

File tree

3 files changed

+119
-38
lines changed

3 files changed

+119
-38
lines changed

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -512,17 +512,17 @@ async def get_workbench_node_ids_from_project_uuid(
512512
project_uuid: str,
513513
) -> Set[str]:
514514
"""Returns a set with all the node_ids from a project's workbench"""
515-
db = app[APP_PROJECT_DBAPI]
516-
return await db.get_all_node_ids_from_workbenches(project_uuid)
515+
db: ProjectDBAPI = app[APP_PROJECT_DBAPI]
516+
return await db.get_node_ids_from_project(project_uuid)
517517

518518

519519
async def is_node_id_present_in_any_project_workbench(
520520
app: web.Application,
521521
node_id: str,
522522
) -> bool:
523523
"""If the node_id is presnet in one of the projects' workbenche returns True"""
524-
db = app[APP_PROJECT_DBAPI]
525-
return node_id in await db.get_all_node_ids_from_workbenches()
524+
db: ProjectDBAPI = app[APP_PROJECT_DBAPI]
525+
return await db.node_id_exists(node_id)
526526

527527

528528
async def notify_project_state_update(

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

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ async def add_projects(self, projects_list: List[Dict], user_id: int) -> List[st
225225
async def add_project(
226226
self,
227227
prj: Dict[str, Any],
228-
user_id: int,
228+
user_id: Optional[int],
229229
*,
230230
force_project_uuid: bool = False,
231231
force_as_template: bool = False,
@@ -794,7 +794,7 @@ async def _get_user_email(conn: SAConnection, user_id: Optional[int]) -> str:
794794

795795
@staticmethod
796796
async def _get_user_primary_group_gid(conn: SAConnection, user_id: int) -> int:
797-
primary_gid: int = await conn.scalar(
797+
primary_gid: Optional[int] = await conn.scalar(
798798
sa.select([users.c.primary_gid]).where(users.c.id == str(user_id))
799799
)
800800
if not primary_gid:
@@ -808,26 +808,28 @@ async def _get_tags_by_project(conn: SAConnection, project_id: str) -> List:
808808
)
809809
return [row.tag_id async for row in conn.execute(query)]
810810

811-
async def get_all_node_ids_from_workbenches(
812-
self, project_uuid: str = None
813-
) -> Set[str]:
814-
"""Returns a set containing all the workbench node_ids from all projects
815-
816-
If a project_uuid is passed, only that project's workbench nodes will be included
817-
"""
818-
819-
if project_uuid is None:
820-
query = "SELECT json_object_keys(projects.workbench) FROM projects"
821-
else:
822-
query = f"SELECT json_object_keys(projects.workbench) FROM projects WHERE projects.uuid = '{project_uuid}'"
823-
811+
async def node_id_exists(self, node_id: str) -> bool:
812+
"""Returns True if the node id exists in any of the available projects"""
824813
async with self.engine.acquire() as conn:
825-
result = set()
826-
query_result = await conn.execute(query)
827-
async for row in query_result:
828-
result.update(set(row.values()))
814+
num_entries = await conn.scalar(
815+
sa.select([func.count()])
816+
.select_from(projects)
817+
.where(projects.c.workbench.op("->>")(f"{node_id}") != None)
818+
)
819+
assert num_entries is not None # nosec
820+
return bool(num_entries > 0)
829821

830-
return result
822+
async def get_node_ids_from_project(self, project_uuid: str) -> Set[str]:
823+
"""Returns a set containing all the node_ids from project with project_uuid"""
824+
result = set()
825+
async with self.engine.acquire() as conn:
826+
async for row in conn.execute(
827+
sa.select([sa.func.json_object_keys(projects.c.workbench)])
828+
.select_from(projects)
829+
.where(projects.c.uuid == f"{project_uuid}")
830+
):
831+
result.update(row.as_tuple()) # type: ignore
832+
return result
831833

832834
async def list_all_projects_by_uuid_for_user(self, user_id: int) -> List[str]:
833835
result = deque()

services/web/server/tests/unit/with_dbs/05/test_project_db.py

Lines changed: 93 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@
1111
from copy import deepcopy
1212
from itertools import combinations
1313
from random import randint
14-
from typing import Any, Dict, List, Optional, Tuple
14+
from secrets import choice
15+
from typing import Any, AsyncIterator, Dict, Iterator, List, Optional, Set, Tuple
1516
from uuid import UUID, uuid5
1617

1718
import pytest
1819
import sqlalchemy as sa
1920
from aiohttp.test_utils import TestClient
21+
from models_library.projects import ProjectID
22+
from models_library.projects_nodes_io import NodeID
2023
from psycopg2.errors import UniqueViolation
2124
from simcore_postgres_database.models.groups import GroupType
2225
from simcore_service_webserver.db_models import UserRole
@@ -32,7 +35,6 @@
3235
_convert_to_schema_names,
3336
_create_project_access_rights,
3437
_find_changed_dict_keys,
35-
setup_projects_db,
3638
)
3739
from simcore_service_webserver.users_exceptions import UserNotFoundError
3840
from simcore_service_webserver.utils import to_datetime
@@ -233,26 +235,20 @@ def _project_access_rights_from_permissions(
233235
_check_project_permissions(project, user_id, user_groups, wanted_permissions)
234236

235237

236-
def _create_project_db(client: TestClient) -> ProjectDBAPI:
237-
setup_projects_db(client.app)
238-
239-
assert APP_PROJECT_DBAPI in client.app
238+
async def test_setup_projects_db(client: TestClient):
239+
assert client.app
240240
db_api = client.app[APP_PROJECT_DBAPI]
241241
assert db_api
242242
assert isinstance(db_api, ProjectDBAPI)
243243

244244
assert db_api._app == client.app
245-
assert db_api._engine
246-
return db_api
247-
248-
249-
async def test_setup_projects_db(client: TestClient):
250-
_create_project_db(client)
245+
assert db_api.engine
251246

252247

253248
@pytest.fixture()
254-
def db_api(client: TestClient, postgres_db: sa.engine.Engine) -> ProjectDBAPI:
255-
db_api = _create_project_db(client)
249+
def db_api(client: TestClient, postgres_db: sa.engine.Engine) -> Iterator[ProjectDBAPI]:
250+
assert client.app
251+
db_api = client.app[APP_PROJECT_DBAPI]
256252

257253
yield db_api
258254

@@ -670,3 +666,86 @@ async def test_patch_user_project_workbench_concurrently(
670666
creation_date=to_datetime(new_project["creationDate"]),
671667
last_change_date=latest_change_date,
672668
)
669+
670+
671+
@pytest.fixture()
672+
async def lots_of_projects_and_nodes(
673+
logged_user: Dict[str, Any],
674+
fake_project: Dict[str, Any],
675+
db_api: ProjectDBAPI,
676+
) -> AsyncIterator[Dict[ProjectID, List[NodeID]]]:
677+
"""Will create >1000 projects with each between 200-1434 nodes"""
678+
NUMBER_OF_PROJECTS = 1245
679+
680+
BASE_UUID = UUID("ccc0839f-93b8-4387-ab16-197281060927")
681+
all_created_projects = {}
682+
project_creation_tasks = []
683+
for p in range(NUMBER_OF_PROJECTS):
684+
project_uuid = uuid5(BASE_UUID, f"project_{p}")
685+
all_created_projects[project_uuid] = []
686+
workbench = {}
687+
for n in range(randint(200, 1434)):
688+
node_uuid = uuid5(project_uuid, f"node_{n}")
689+
all_created_projects[project_uuid].append(node_uuid)
690+
workbench[f"{node_uuid}"] = {
691+
"key": "simcore/services/comp/sleepers",
692+
"version": "1.43.5",
693+
"label": f"I am node {n}",
694+
}
695+
new_project = deepcopy(fake_project)
696+
new_project.update(uuid=project_uuid, name=f"project {p}", workbench=workbench)
697+
# add the project
698+
project_creation_tasks.append(
699+
db_api.add_project(prj=new_project, user_id=logged_user["id"])
700+
)
701+
await asyncio.gather(*project_creation_tasks)
702+
print(f"---> created {len(all_created_projects)} projects in the database")
703+
yield all_created_projects
704+
print(f"<--- removed {len(all_created_projects)} projects in the database")
705+
706+
# cleanup
707+
await asyncio.gather(
708+
*[
709+
db_api.delete_user_project(logged_user["id"], f"{p_uuid}")
710+
for p_uuid in all_created_projects
711+
]
712+
)
713+
714+
715+
@pytest.mark.parametrize(
716+
"user_role",
717+
[UserRole.USER],
718+
)
719+
async def test_node_id_exists(
720+
db_api: ProjectDBAPI, lots_of_projects_and_nodes: Dict[ProjectID, List[NodeID]]
721+
):
722+
723+
# create a node uuid that does not exist from an existing project
724+
existing_project_id = choice(list(lots_of_projects_and_nodes.keys()))
725+
not_existing_node_id_in_existing_project = uuid5(
726+
existing_project_id, "node_invalid_node"
727+
)
728+
729+
node_id_exists = await db_api.node_id_exists(
730+
f"{not_existing_node_id_in_existing_project}"
731+
)
732+
assert node_id_exists == False
733+
existing_node_id = choice(lots_of_projects_and_nodes[existing_project_id])
734+
node_id_exists = await db_api.node_id_exists(f"{existing_node_id}")
735+
assert node_id_exists == True
736+
737+
738+
@pytest.mark.parametrize(
739+
"user_role",
740+
[UserRole.USER],
741+
)
742+
async def test_get_node_ids_from_project(
743+
db_api: ProjectDBAPI, lots_of_projects_and_nodes: Dict[ProjectID, List[NodeID]]
744+
):
745+
for project_id in lots_of_projects_and_nodes:
746+
node_ids_inside_project: Set[str] = await db_api.get_node_ids_from_project(
747+
f"{project_id}"
748+
)
749+
assert node_ids_inside_project == {
750+
f"{n}" for n in lots_of_projects_and_nodes[project_id]
751+
}

0 commit comments

Comments
 (0)