From eb665e14aab5b1304f1e71cbb09d8fb6b0dd01ab Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Fri, 18 Oct 2024 16:29:10 +0200 Subject: [PATCH 01/17] daily work --- .../src/models_library/projects_state.py | 1 + .../utils_projects.py | 32 +++ packages/postgres-database/tests/conftest.py | 2 +- .../src/pytest_simcore/faker_projects_data.py | 13 ++ .../unit/test_utils_distributed_identifier.py | 1 + services/docker-compose.yml | 5 + services/efs-guardian/requirements/_base.in | 1 + services/efs-guardian/requirements/_base.txt | 202 +++++++++++------ services/efs-guardian/requirements/_test.txt | 85 +++---- services/efs-guardian/requirements/_tools.txt | 30 +-- services/efs-guardian/requirements/ci.txt | 1 + services/efs-guardian/requirements/dev.txt | 1 + services/efs-guardian/requirements/prod.txt | 1 + .../core/application.py | 20 +- .../core/settings.py | 7 + .../services/background_tasks.py | 107 ++++++++- .../services/background_tasks_setup.py | 2 +- .../services/efs_manager.py | 47 +++- .../services/modules/db.py | 17 ++ services/efs-guardian/tests/conftest.py | 78 +++++++ services/efs-guardian/tests/unit/conftest.py | 209 +++++++++--------- .../tests/unit/test_api_health.py | 13 +- .../tests/unit/test_efs_guardian_rpc.py | 5 +- .../tests/unit/test_efs_manager.py | 7 +- .../unit/test_efs_removal_policy_task.py | 172 ++++++++++++++ 25 files changed, 801 insertions(+), 258 deletions(-) create mode 100644 packages/postgres-database/src/simcore_postgres_database/utils_projects.py create mode 100644 services/efs-guardian/src/simcore_service_efs_guardian/services/modules/db.py create mode 100644 services/efs-guardian/tests/conftest.py create mode 100644 services/efs-guardian/tests/unit/test_efs_removal_policy_task.py diff --git a/packages/models-library/src/models_library/projects_state.py b/packages/models-library/src/models_library/projects_state.py index 38c68d5d4a4d..757704e14d33 100644 --- a/packages/models-library/src/models_library/projects_state.py +++ b/packages/models-library/src/models_library/projects_state.py @@ -52,6 +52,7 @@ class ProjectStatus(str, Enum): EXPORTING = "EXPORTING" OPENING = "OPENING" OPENED = "OPENED" + MAINTAINING = "MAINTAINING" class ProjectLocked(BaseModel): diff --git a/packages/postgres-database/src/simcore_postgres_database/utils_projects.py b/packages/postgres-database/src/simcore_postgres_database/utils_projects.py new file mode 100644 index 000000000000..97af0cae507e --- /dev/null +++ b/packages/postgres-database/src/simcore_postgres_database/utils_projects.py @@ -0,0 +1,32 @@ +import uuid +from datetime import datetime, timezone + +import sqlalchemy as sa +from pydantic import parse_obj_as +from sqlalchemy.ext.asyncio import AsyncConnection + +from .models.projects import projects +from .utils_repos import transaction_context + + +class ProjectsRepo: + def __init__(self, engine): + self.engine = engine + + async def get_project_last_change_date( + self, + project_uuid: uuid.UUID, + *, + connection: AsyncConnection | None = None, + ) -> datetime | None: + async with transaction_context(self.engine, connection) as conn: + get_stmt = sa.select(projects.c.last_change_date).where( + projects.c.uuid == f"{project_uuid}" + ) + + result = await conn.execute(get_stmt) + row = result.first() + if row is None: + return None + date = parse_obj_as(datetime, row[0]) + return date.replace(tzinfo=timezone.utc) diff --git a/packages/postgres-database/tests/conftest.py b/packages/postgres-database/tests/conftest.py index 5526b668e398..feb8bfaae97a 100644 --- a/packages/postgres-database/tests/conftest.py +++ b/packages/postgres-database/tests/conftest.py @@ -206,7 +206,7 @@ async def connection(aiopg_engine: Engine) -> AsyncIterator[SAConnection]: @pytest.fixture -async def asyncpg_engine( +async def asyncpg_engine( # <-- WE SHOULD USE THIS ONE is_pdb_enabled: bool, pg_sa_engine: sa.engine.Engine, make_asyncpg_engine: Callable[[bool], AsyncEngine], diff --git a/packages/pytest-simcore/src/pytest_simcore/faker_projects_data.py b/packages/pytest-simcore/src/pytest_simcore/faker_projects_data.py index 643ffee88593..5e993471ec85 100644 --- a/packages/pytest-simcore/src/pytest_simcore/faker_projects_data.py +++ b/packages/pytest-simcore/src/pytest_simcore/faker_projects_data.py @@ -9,11 +9,15 @@ """ +from typing import Any + import pytest from faker import Faker from models_library.projects import ProjectID from models_library.projects_nodes_io import NodeID +from models_library.users import UserID from pydantic import parse_obj_as +from pytest_simcore.helpers.faker_factories import random_project _MESSAGE = ( "If set, it overrides the fake value of `{}` fixture." @@ -43,3 +47,12 @@ def project_id(faker: Faker, request: pytest.FixtureRequest) -> ProjectID: @pytest.fixture def node_id(faker: Faker) -> NodeID: return parse_obj_as(NodeID, faker.uuid4()) + + +@pytest.fixture +def project( + faker: Faker, + project_id: ProjectID, + user_id: UserID, +) -> dict[str, Any]: + return random_project(fake=faker, uuid=f"{project_id}", prj_owner=user_id) diff --git a/services/director-v2/tests/unit/test_utils_distributed_identifier.py b/services/director-v2/tests/unit/test_utils_distributed_identifier.py index ce200feef977..6d4723507c31 100644 --- a/services/director-v2/tests/unit/test_utils_distributed_identifier.py +++ b/services/director-v2/tests/unit/test_utils_distributed_identifier.py @@ -163,6 +163,7 @@ async def _destroy( self.api.delete(identifier) +# MD: here redis @pytest.fixture async def redis_client_sdk( redis_service: RedisSettings, diff --git a/services/docker-compose.yml b/services/docker-compose.yml index c5f8e762ee77..181b1bd42368 100644 --- a/services/docker-compose.yml +++ b/services/docker-compose.yml @@ -413,6 +413,11 @@ services: REDIS_PORT: ${REDIS_PORT} REDIS_SECURE: ${REDIS_SECURE} REDIS_USER: ${REDIS_USER} + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_HOST: ${POSTGRES_HOST} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_PORT: ${POSTGRES_PORT} + POSTGRES_USER: ${POSTGRES_USER} SC_USER_ID: ${SC_USER_ID} SC_USER_NAME: ${SC_USER_NAME} EFS_USER_ID: ${EFS_USER_ID} diff --git a/services/efs-guardian/requirements/_base.in b/services/efs-guardian/requirements/_base.in index 84e8460fa05c..90fc6e24ac64 100644 --- a/services/efs-guardian/requirements/_base.in +++ b/services/efs-guardian/requirements/_base.in @@ -9,6 +9,7 @@ --requirement ../../../packages/models-library/requirements/_base.in --requirement ../../../packages/settings-library/requirements/_base.in --requirement ../../../packages/aws-library/requirements/_base.in +--requirement ../../../packages/postgres-database/requirements/_base.in # service-library[fastapi] --requirement ../../../packages/service-library/requirements/_base.in --requirement ../../../packages/service-library/requirements/_fastapi.in diff --git a/services/efs-guardian/requirements/_base.txt b/services/efs-guardian/requirements/_base.txt index 9f98cc7a99db..0e7970349859 100644 --- a/services/efs-guardian/requirements/_base.txt +++ b/services/efs-guardian/requirements/_base.txt @@ -1,12 +1,12 @@ -aio-pika==9.4.1 +aio-pika==9.4.3 # via # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in # -r requirements/../../../packages/service-library/requirements/_base.in -aioboto3==13.1.0 +aioboto3==13.2.0 # via -r requirements/../../../packages/aws-library/requirements/_base.in -aiobotocore==2.13.1 +aiobotocore==2.15.2 # via aioboto3 -aiocache==0.12.2 +aiocache==0.12.3 # via # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in # -r requirements/../../../packages/aws-library/requirements/_base.in @@ -15,16 +15,18 @@ aiodebug==2.3.0 # via # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in # -r requirements/../../../packages/service-library/requirements/_base.in -aiodocker==0.21.0 +aiodocker==0.23.0 # via # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in # -r requirements/../../../packages/service-library/requirements/_base.in -aiofiles==23.2.1 +aiofiles==24.1.0 # via # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in # -r requirements/../../../packages/service-library/requirements/_base.in # aioboto3 -aiohttp==3.9.5 +aiohappyeyeballs==2.4.3 + # via aiohttp +aiohttp==3.10.10 # via # -c requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt @@ -33,6 +35,7 @@ aiohttp==3.9.5 # -c requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt @@ -40,13 +43,15 @@ aiohttp==3.9.5 # -c requirements/../../../requirements/constraints.txt # aiobotocore # aiodocker -aioitertools==0.11.0 +aioitertools==0.12.0 # via aiobotocore -aiormq==6.8.0 +aiormq==6.8.1 # via aio-pika aiosignal==1.3.1 # via aiohttp -anyio==4.4.0 +alembic==1.13.3 + # via -r requirements/../../../packages/postgres-database/requirements/_base.in +anyio==4.6.2.post1 # via # fast-depends # faststream @@ -63,21 +68,25 @@ arrow==1.3.0 # -r requirements/../../../packages/service-library/requirements/_base.in asgiref==3.8.1 # via opentelemetry-instrumentation-asgi -attrs==23.2.0 +async-timeout==4.0.3 + # via asyncpg +asyncpg==0.29.0 + # via sqlalchemy +attrs==24.2.0 # via # aiohttp # jsonschema # referencing -boto3==1.34.106 +boto3==1.35.36 # via aiobotocore -botocore==1.34.106 +botocore==1.35.36 # via # aiobotocore # boto3 # s3transfer -botocore-stubs==1.34.94 +botocore-stubs==1.35.43 # via types-aiobotocore -certifi==2024.2.2 +certifi==2024.8.30 # via # -c requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt @@ -86,6 +95,7 @@ certifi==2024.2.2 # -c requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt @@ -94,7 +104,7 @@ certifi==2024.2.2 # httpcore # httpx # requests -charset-normalizer==3.3.2 +charset-normalizer==3.4.0 # via requests click==8.1.7 # via @@ -106,11 +116,11 @@ deprecated==1.2.14 # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-exporter-otlp-proto-http # opentelemetry-semantic-conventions -dnspython==2.6.1 +dnspython==2.7.0 # via email-validator -email-validator==2.1.1 +email-validator==2.2.0 # via pydantic -fast-depends==2.4.3 +fast-depends==2.4.12 # via faststream fastapi==0.99.1 # via @@ -121,6 +131,7 @@ fastapi==0.99.1 # -c requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt @@ -129,7 +140,7 @@ fastapi==0.99.1 # -r requirements/../../../packages/service-library/requirements/_fastapi.in # -r requirements/_base.in # prometheus-fastapi-instrumentator -faststream==0.5.9 +faststream==0.5.27 # via # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in # -r requirements/../../../packages/service-library/requirements/_base.in @@ -141,15 +152,17 @@ googleapis-common-protos==1.65.0 # via # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-exporter-otlp-proto-http -grpcio==1.66.0 +greenlet==3.1.1 + # via sqlalchemy +grpcio==1.67.0 # via opentelemetry-exporter-otlp-proto-grpc h11==0.14.0 # via # httpcore # uvicorn -httpcore==1.0.5 +httpcore==1.0.6 # via httpx -httpx==0.27.0 +httpx==0.27.2 # via # -c requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt @@ -158,26 +171,27 @@ httpx==0.27.0 # -c requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../requirements/constraints.txt # -r requirements/../../../packages/service-library/requirements/_fastapi.in -idna==3.7 +idna==3.10 # via # anyio # email-validator # httpx # requests # yarl -importlib-metadata==8.0.0 +importlib-metadata==8.4.0 # via opentelemetry-api jmespath==1.0.1 # via # boto3 # botocore -jsonschema==4.22.0 +jsonschema==4.23.0 # via # -r requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/_base.in # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in @@ -185,15 +199,33 @@ jsonschema==4.22.0 # -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in jsonschema-specifications==2023.7.1 # via jsonschema +mako==1.3.5 + # via + # -c requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../requirements/constraints.txt + # alembic markdown-it-py==3.0.0 # via rich +markupsafe==3.0.1 + # via mako mdurl==0.1.2 # via markdown-it-py -multidict==6.0.5 +multidict==6.1.0 # via # aiohttp # yarl -opentelemetry-api==1.26.0 +opentelemetry-api==1.27.0 # via # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in # -r requirements/../../../packages/service-library/requirements/_base.in @@ -205,54 +237,54 @@ opentelemetry-api==1.26.0 # opentelemetry-instrumentation-requests # opentelemetry-sdk # opentelemetry-semantic-conventions -opentelemetry-exporter-otlp==1.26.0 +opentelemetry-exporter-otlp==1.27.0 # via # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in # -r requirements/../../../packages/service-library/requirements/_base.in -opentelemetry-exporter-otlp-proto-common==1.26.0 +opentelemetry-exporter-otlp-proto-common==1.27.0 # via # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-exporter-otlp-proto-http -opentelemetry-exporter-otlp-proto-grpc==1.26.0 +opentelemetry-exporter-otlp-proto-grpc==1.27.0 # via opentelemetry-exporter-otlp -opentelemetry-exporter-otlp-proto-http==1.26.0 +opentelemetry-exporter-otlp-proto-http==1.27.0 # via opentelemetry-exporter-otlp -opentelemetry-instrumentation==0.47b0 +opentelemetry-instrumentation==0.48b0 # via # opentelemetry-instrumentation-asgi # opentelemetry-instrumentation-fastapi # opentelemetry-instrumentation-requests -opentelemetry-instrumentation-asgi==0.47b0 +opentelemetry-instrumentation-asgi==0.48b0 # via opentelemetry-instrumentation-fastapi -opentelemetry-instrumentation-fastapi==0.47b0 +opentelemetry-instrumentation-fastapi==0.48b0 # via -r requirements/../../../packages/service-library/requirements/_fastapi.in -opentelemetry-instrumentation-requests==0.47b0 +opentelemetry-instrumentation-requests==0.48b0 # via # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in # -r requirements/../../../packages/service-library/requirements/_base.in -opentelemetry-proto==1.26.0 +opentelemetry-proto==1.27.0 # via # opentelemetry-exporter-otlp-proto-common # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-exporter-otlp-proto-http -opentelemetry-sdk==1.26.0 +opentelemetry-sdk==1.27.0 # via # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in # -r requirements/../../../packages/service-library/requirements/_base.in # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-exporter-otlp-proto-http -opentelemetry-semantic-conventions==0.47b0 +opentelemetry-semantic-conventions==0.48b0 # via # opentelemetry-instrumentation-asgi # opentelemetry-instrumentation-fastapi # opentelemetry-instrumentation-requests # opentelemetry-sdk -opentelemetry-util-http==0.47b0 +opentelemetry-util-http==0.48b0 # via # opentelemetry-instrumentation-asgi # opentelemetry-instrumentation-fastapi # opentelemetry-instrumentation-requests -orjson==3.10.3 +orjson==3.10.7 # via # -c requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt @@ -261,6 +293,7 @@ orjson==3.10.3 # -c requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt @@ -270,25 +303,29 @@ orjson==3.10.3 # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in # -r requirements/../../../packages/models-library/requirements/_base.in # -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in -packaging==24.0 +packaging==24.1 # via -r requirements/_base.in pamqp==3.3.0 # via aiormq -prometheus-client==0.20.0 +prometheus-client==0.21.0 # via # -r requirements/../../../packages/service-library/requirements/_fastapi.in # prometheus-fastapi-instrumentator prometheus-fastapi-instrumentator==6.1.0 # via -r requirements/../../../packages/service-library/requirements/_fastapi.in -protobuf==4.25.4 +propcache==0.2.0 + # via yarl +protobuf==4.25.5 # via # googleapis-common-protos # opentelemetry-proto -psutil==6.0.0 +psutil==6.1.0 # via # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in # -r requirements/../../../packages/service-library/requirements/_base.in -pydantic==1.10.15 +psycopg2-binary==2.9.10 + # via sqlalchemy +pydantic==1.10.18 # via # -c requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt @@ -297,6 +334,7 @@ pydantic==1.10.15 # -c requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in @@ -310,6 +348,7 @@ pydantic==1.10.15 # -r requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/_base.in # -r requirements/../../../packages/aws-library/requirements/_base.in # -r requirements/../../../packages/models-library/requirements/_base.in + # -r requirements/../../../packages/postgres-database/requirements/_base.in # -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in # -r requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in # -r requirements/../../../packages/service-library/requirements/_base.in @@ -318,7 +357,7 @@ pydantic==1.10.15 # fastapi pygments==2.18.0 # via rich -pyinstrument==4.6.2 +pyinstrument==5.0.0 # via # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in # -r requirements/../../../packages/service-library/requirements/_base.in @@ -326,7 +365,7 @@ python-dateutil==2.9.0.post0 # via # arrow # botocore -pyyaml==6.0.1 +pyyaml==6.0.2 # via # -c requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt @@ -335,6 +374,7 @@ pyyaml==6.0.1 # -c requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt @@ -342,7 +382,7 @@ pyyaml==6.0.1 # -c requirements/../../../requirements/constraints.txt # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in # -r requirements/../../../packages/service-library/requirements/_base.in -redis==5.0.4 +redis==5.1.1 # via # -c requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt @@ -351,6 +391,7 @@ redis==5.0.4 # -c requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt @@ -370,22 +411,22 @@ repro-zipfile==0.3.1 # -r requirements/../../../packages/service-library/requirements/_base.in requests==2.32.3 # via opentelemetry-exporter-otlp-proto-http -rich==13.7.1 +rich==13.9.2 # via # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in # -r requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/_base.in # -r requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in # -r requirements/../../../packages/settings-library/requirements/_base.in # typer -rpds-py==0.18.1 +rpds-py==0.20.0 # via # jsonschema # referencing -s3transfer==0.10.1 +s3transfer==0.10.3 # via boto3 -setuptools==74.0.0 +setuptools==75.2.0 # via opentelemetry-instrumentation -sh==2.0.6 +sh==2.1.0 # via -r requirements/../../../packages/aws-library/requirements/_base.in shellingham==1.5.4 # via typer @@ -395,6 +436,23 @@ sniffio==1.3.1 # via # anyio # httpx +sqlalchemy==1.4.54 + # via + # -c requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../requirements/constraints.txt + # -r requirements/../../../packages/postgres-database/requirements/_base.in + # alembic starlette==0.27.0 # via # -c requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt @@ -404,47 +462,47 @@ starlette==0.27.0 # -c requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../requirements/constraints.txt # fastapi -tenacity==8.5.0 +tenacity==9.0.0 # via # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in # -r requirements/../../../packages/service-library/requirements/_base.in -toolz==0.12.1 +toolz==1.0.0 # via # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in # -r requirements/../../../packages/service-library/requirements/_base.in -tqdm==4.66.4 +tqdm==4.66.5 # via # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in # -r requirements/../../../packages/service-library/requirements/_base.in -typer==0.12.3 +typer==0.12.5 # via # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in # -r requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/_base.in # -r requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in # -r requirements/../../../packages/settings-library/requirements/_base.in - # faststream -types-aiobotocore==2.13.0 +types-aiobotocore==2.15.2 # via -r requirements/../../../packages/aws-library/requirements/_base.in -types-aiobotocore-ec2==2.13.0 +types-aiobotocore-ec2==2.15.2 # via types-aiobotocore -types-aiobotocore-s3==2.13.0 +types-aiobotocore-s3==2.15.2 # via types-aiobotocore -types-aiobotocore-ssm==2.13.1 +types-aiobotocore-ssm==2.15.2 # via types-aiobotocore -types-awscrt==0.20.9 +types-awscrt==0.22.0 # via botocore-stubs -types-python-dateutil==2.9.0.20240316 +types-python-dateutil==2.9.0.20241003 # via arrow -typing-extensions==4.11.0 +typing-extensions==4.12.2 # via # aiodebug - # aiodocker + # alembic # fastapi # faststream # opentelemetry-sdk @@ -454,7 +512,7 @@ typing-extensions==4.11.0 # types-aiobotocore-ec2 # types-aiobotocore-s3 # types-aiobotocore-ssm -urllib3==2.2.1 +urllib3==2.2.3 # via # -c requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt @@ -463,6 +521,7 @@ urllib3==2.2.1 # -c requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt @@ -470,17 +529,18 @@ urllib3==2.2.1 # -c requirements/../../../requirements/constraints.txt # botocore # requests -uvicorn==0.30.0 +uvicorn==0.32.0 # via -r requirements/../../../packages/service-library/requirements/_fastapi.in wrapt==1.16.0 # via # aiobotocore # deprecated # opentelemetry-instrumentation -yarl==1.9.4 +yarl==1.15.4 # via + # -r requirements/../../../packages/postgres-database/requirements/_base.in # aio-pika # aiohttp # aiormq -zipp==3.20.1 +zipp==3.20.2 # via importlib-metadata diff --git a/services/efs-guardian/requirements/_test.txt b/services/efs-guardian/requirements/_test.txt index efd05c557f60..f188e8071de3 100644 --- a/services/efs-guardian/requirements/_test.txt +++ b/services/efs-guardian/requirements/_test.txt @@ -1,8 +1,12 @@ -aiodocker==0.21.0 +aiodocker==0.23.0 # via # -c requirements/_base.txt # -r requirements/_test.in -aiohttp==3.9.5 +aiohappyeyeballs==2.4.3 + # via + # -c requirements/_base.txt + # aiohttp +aiohttp==3.10.10 # via # -c requirements/../../../requirements/constraints.txt # -c requirements/_base.txt @@ -13,37 +17,37 @@ aiosignal==1.3.1 # aiohttp antlr4-python3-runtime==4.13.2 # via moto -anyio==4.4.0 +anyio==4.6.2.post1 # via # -c requirements/_base.txt # httpx asgi-lifespan==2.1.0 # via -r requirements/_test.in -attrs==23.2.0 +attrs==24.2.0 # via # -c requirements/_base.txt # aiohttp # jsonschema # referencing -aws-sam-translator==1.89.0 +aws-sam-translator==1.91.0 # via cfn-lint aws-xray-sdk==2.14.0 # via moto blinker==1.8.2 # via flask -boto3==1.34.106 +boto3==1.35.36 # via # -c requirements/_base.txt # aws-sam-translator # moto -botocore==1.34.106 +botocore==1.35.36 # via # -c requirements/_base.txt # aws-xray-sdk # boto3 # moto # s3transfer -certifi==2024.2.2 +certifi==2024.8.30 # via # -c requirements/../../../requirements/constraints.txt # -c requirements/_base.txt @@ -52,9 +56,9 @@ certifi==2024.2.2 # requests cffi==1.17.1 # via cryptography -cfn-lint==1.10.3 +cfn-lint==1.17.1 # via moto -charset-normalizer==3.3.2 +charset-normalizer==3.4.0 # via # -c requirements/_base.txt # requests @@ -62,7 +66,7 @@ click==8.1.7 # via # -c requirements/_base.txt # flask -coverage==7.6.1 +coverage==7.6.3 # via # -r requirements/_test.in # pytest-cov @@ -71,7 +75,7 @@ cryptography==43.0.1 # -c requirements/../../../requirements/constraints.txt # joserfc # moto -debugpy==1.8.5 +debugpy==1.8.7 # via -r requirements/_test.in deepdiff==8.0.1 # via -r requirements/_test.in @@ -79,9 +83,9 @@ docker==7.1.0 # via # -r requirements/_test.in # moto -faker==29.0.0 +faker==30.6.0 # via -r requirements/_test.in -fakeredis==2.24.1 +fakeredis==2.25.1 # via -r requirements/_test.in flask==3.0.3 # via @@ -94,23 +98,23 @@ frozenlist==1.4.1 # -c requirements/_base.txt # aiohttp # aiosignal -graphql-core==3.2.4 +graphql-core==3.2.5 # via moto h11==0.14.0 # via # -c requirements/_base.txt # httpcore -httpcore==1.0.5 +httpcore==1.0.6 # via # -c requirements/_base.txt # httpx -httpx==0.27.0 +httpx==0.27.2 # via # -c requirements/../../../requirements/constraints.txt # -c requirements/_base.txt # -r requirements/_test.in # respx -idna==3.7 +idna==3.10 # via # -c requirements/_base.txt # anyio @@ -137,11 +141,11 @@ jsondiff==2.2.1 # via moto jsonpatch==1.33 # via cfn-lint -jsonpath-ng==1.6.1 +jsonpath-ng==1.7.0 # via moto jsonpointer==3.0.0 # via jsonpatch -jsonschema==4.22.0 +jsonschema==4.23.0 # via # -c requirements/_base.txt # aws-sam-translator @@ -158,20 +162,21 @@ lazy-object-proxy==1.10.0 # via openapi-spec-validator lupa==2.2 # via fakeredis -markupsafe==2.1.5 +markupsafe==3.0.1 # via + # -c requirements/_base.txt # jinja2 # werkzeug -moto==5.0.15 +moto==5.0.17 # via -r requirements/_test.in mpmath==1.3.0 # via sympy -multidict==6.0.5 +multidict==6.1.0 # via # -c requirements/_base.txt # aiohttp # yarl -networkx==3.3 +networkx==3.4.1 # via cfn-lint openapi-schema-validator==0.6.2 # via openapi-spec-validator @@ -179,7 +184,7 @@ openapi-spec-validator==0.7.1 # via moto orderly-set==5.2.2 # via deepdiff -packaging==24.0 +packaging==24.1 # via # -c requirements/_base.txt # pytest @@ -191,7 +196,11 @@ pluggy==1.5.0 # via pytest ply==3.11 # via jsonpath-ng -psutil==6.0.0 +propcache==0.2.0 + # via + # -c requirements/_base.txt + # yarl +psutil==6.1.0 # via # -c requirements/_base.txt # -r requirements/_test.in @@ -199,12 +208,12 @@ py-partiql-parser==0.5.6 # via moto pycparser==2.22 # via cffi -pydantic==1.10.15 +pydantic==1.10.18 # via # -c requirements/../../../requirements/constraints.txt # -c requirements/_base.txt # aws-sam-translator -pyparsing==3.1.4 +pyparsing==3.2.0 # via moto pytest==8.3.3 # via @@ -230,7 +239,7 @@ python-dateutil==2.9.0.post0 # moto python-dotenv==1.0.1 # via -r requirements/_test.in -pyyaml==6.0.1 +pyyaml==6.0.2 # via # -c requirements/../../../requirements/constraints.txt # -c requirements/_base.txt @@ -239,7 +248,7 @@ pyyaml==6.0.1 # jsonschema-path # moto # responses -redis==5.0.4 +redis==5.1.1 # via # -c requirements/../../../requirements/constraints.txt # -c requirements/_base.txt @@ -265,16 +274,16 @@ respx==0.21.1 # via -r requirements/_test.in rfc3339-validator==0.1.4 # via openapi-schema-validator -rpds-py==0.18.1 +rpds-py==0.20.0 # via # -c requirements/_base.txt # jsonschema # referencing -s3transfer==0.10.1 +s3transfer==0.10.3 # via # -c requirements/_base.txt # boto3 -setuptools==74.0.0 +setuptools==75.2.0 # via # -c requirements/_base.txt # moto @@ -293,14 +302,14 @@ sortedcontainers==2.4.0 # via fakeredis sympy==1.13.3 # via cfn-lint -typing-extensions==4.11.0 +typing-extensions==4.12.2 # via # -c requirements/_base.txt - # aiodocker # aws-sam-translator # cfn-lint + # faker # pydantic -urllib3==2.2.1 +urllib3==2.2.3 # via # -c requirements/../../../requirements/constraints.txt # -c requirements/_base.txt @@ -316,9 +325,9 @@ wrapt==1.16.0 # via # -c requirements/_base.txt # aws-xray-sdk -xmltodict==0.13.0 +xmltodict==0.14.2 # via moto -yarl==1.9.4 +yarl==1.15.4 # via # -c requirements/_base.txt # aiohttp diff --git a/services/efs-guardian/requirements/_tools.txt b/services/efs-guardian/requirements/_tools.txt index dec3b9c204df..6bf2e784ed39 100644 --- a/services/efs-guardian/requirements/_tools.txt +++ b/services/efs-guardian/requirements/_tools.txt @@ -1,8 +1,8 @@ -astroid==3.3.4 +astroid==3.3.5 # via pylint -black==24.8.0 +black==24.10.0 # via -r requirements/../../../requirements/devenv.txt -build==1.2.2 +build==1.2.2.post1 # via pip-tools bump2version==1.0.1 # via -r requirements/../../../requirements/devenv.txt @@ -14,9 +14,9 @@ click==8.1.7 # -c requirements/_test.txt # black # pip-tools -dill==0.3.8 +dill==0.3.9 # via pylint -distlib==0.3.8 +distlib==0.3.9 # via virtualenv filelock==3.16.1 # via virtualenv @@ -36,7 +36,7 @@ mypy-extensions==1.0.0 # mypy nodeenv==1.9.1 # via pre-commit -packaging==24.0 +packaging==24.1 # via # -c requirements/_base.txt # -c requirements/_test.txt @@ -53,38 +53,38 @@ platformdirs==4.3.6 # black # pylint # virtualenv -pre-commit==3.8.0 +pre-commit==4.0.1 # via -r requirements/../../../requirements/devenv.txt -pylint==3.3.0 +pylint==3.3.1 # via -r requirements/../../../requirements/devenv.txt -pyproject-hooks==1.1.0 +pyproject-hooks==1.2.0 # via # build # pip-tools -pyyaml==6.0.1 +pyyaml==6.0.2 # via # -c requirements/../../../requirements/constraints.txt # -c requirements/_base.txt # -c requirements/_test.txt # pre-commit # watchdog -ruff==0.6.7 +ruff==0.7.0 # via -r requirements/../../../requirements/devenv.txt -setuptools==74.0.0 +setuptools==75.2.0 # via # -c requirements/_base.txt # -c requirements/_test.txt # pip-tools tomlkit==0.13.2 # via pylint -typing-extensions==4.11.0 +typing-extensions==4.12.2 # via # -c requirements/_base.txt # -c requirements/_test.txt # mypy -virtualenv==20.26.5 +virtualenv==20.27.0 # via pre-commit -watchdog==5.0.2 +watchdog==5.0.3 # via -r requirements/_tools.in wheel==0.44.0 # via pip-tools diff --git a/services/efs-guardian/requirements/ci.txt b/services/efs-guardian/requirements/ci.txt index 0d62eb41eb96..193365cdddc3 100644 --- a/services/efs-guardian/requirements/ci.txt +++ b/services/efs-guardian/requirements/ci.txt @@ -17,6 +17,7 @@ simcore-models-library @ ../../packages/models-library pytest-simcore @ ../../packages/pytest-simcore simcore-service-library[fastapi] @ ../../packages/service-library simcore-settings-library @ ../../packages/settings-library +simcore-postgres-database @ ../../packages/postgres-database # installs current package simcore-service-efs-guardian @ . diff --git a/services/efs-guardian/requirements/dev.txt b/services/efs-guardian/requirements/dev.txt index 76ea75d980d9..0c832fb84112 100644 --- a/services/efs-guardian/requirements/dev.txt +++ b/services/efs-guardian/requirements/dev.txt @@ -17,6 +17,7 @@ --editable ../../packages/pytest-simcore --editable ../../packages/service-library[fastapi] --editable ../../packages/settings-library +--editable ../../packages/postgres-database # installs current package --editable . diff --git a/services/efs-guardian/requirements/prod.txt b/services/efs-guardian/requirements/prod.txt index 0a75d60f13ff..486d5e9f7a4c 100644 --- a/services/efs-guardian/requirements/prod.txt +++ b/services/efs-guardian/requirements/prod.txt @@ -14,5 +14,6 @@ simcore-aws-library @ ../../packages/aws-library simcore-models-library @ ../../packages/models-library simcore-service-library[fastapi] @ ../../packages/service-library simcore-settings-library @ ../../packages/settings-library +simcore-postgres-database @ ../../packages/postgres-database # installs current package simcore-service-efs-guardian @ . diff --git a/services/efs-guardian/src/simcore_service_efs_guardian/core/application.py b/services/efs-guardian/src/simcore_service_efs_guardian/core/application.py index 6bf2833ed02c..f52120966f21 100644 --- a/services/efs-guardian/src/simcore_service_efs_guardian/core/application.py +++ b/services/efs-guardian/src/simcore_service_efs_guardian/core/application.py @@ -15,6 +15,7 @@ from ..api.rpc.routes import setup_rpc_routes from ..services.background_tasks_setup import setup as setup_background_tasks from ..services.efs_manager_setup import setup as setup_efs_manager +from ..services.modules.db import setup as setup_db from ..services.modules.rabbitmq import setup as setup_rabbitmq from ..services.modules.redis import setup as setup_redis from ..services.process_messages_setup import setup as setup_process_messages @@ -23,11 +24,13 @@ logger = logging.getLogger(__name__) -def create_app(settings: ApplicationSettings) -> FastAPI: - logger.info("app settings: %s", settings.json(indent=1)) +def create_app(settings: ApplicationSettings | None = None) -> FastAPI: + app_settings = settings or ApplicationSettings.create_from_envs() + + logger.info("app settings: %s", app_settings.json(indent=1)) app = FastAPI( - debug=settings.EFS_GUARDIAN_DEBUG, + debug=app_settings.EFS_GUARDIAN_DEBUG, title=APP_NAME, description="Service to monitor and manage elastic file system", version=API_VERSION, @@ -36,7 +39,7 @@ def create_app(settings: ApplicationSettings) -> FastAPI: redoc_url=None, # default disabled ) # STATE - app.state.settings = settings + app.state.settings = app_settings assert app.state.settings.API_VERSION == API_VERSION # nosec if app.state.settings.EFS_GUARDIAN_TRACING: setup_tracing(app, app.state.settings.EFS_GUARDIAN_TRACING, APP_NAME) @@ -44,13 +47,14 @@ def create_app(settings: ApplicationSettings) -> FastAPI: # PLUGINS SETUP setup_rabbitmq(app) setup_redis(app) + setup_db(app) setup_api_routes(app) - setup_rpc_routes(app) + setup_rpc_routes(app) # requires Rabbit setup_efs_manager(app) - setup_background_tasks(app) - setup_process_messages(app) + setup_background_tasks(app) # requires Redis, DB + setup_process_messages(app) # requires Rabbit # EVENTS async def _on_startup() -> None: @@ -58,7 +62,7 @@ async def _on_startup() -> None: if any( s is None for s in [ - settings.EFS_GUARDIAN_AWS_EFS_SETTINGS, + app_settings.EFS_GUARDIAN_AWS_EFS_SETTINGS, ] ): print(APP_STARTED_DISABLED_BANNER_MSG, flush=True) # noqa: T201 diff --git a/services/efs-guardian/src/simcore_service_efs_guardian/core/settings.py b/services/efs-guardian/src/simcore_service_efs_guardian/core/settings.py index 7b630993830e..2e91ed2e9580 100644 --- a/services/efs-guardian/src/simcore_service_efs_guardian/core/settings.py +++ b/services/efs-guardian/src/simcore_service_efs_guardian/core/settings.py @@ -1,3 +1,4 @@ +import datetime from functools import cached_property from typing import Final, cast @@ -11,6 +12,7 @@ from pydantic import ByteSize, Field, PositiveInt, parse_obj_as, validator from settings_library.base import BaseCustomSettings from settings_library.efs import AwsEfsSettings +from settings_library.postgres import PostgresSettings from settings_library.rabbit import RabbitSettings from settings_library.redis import RedisSettings from settings_library.tracing import TracingSettings @@ -61,6 +63,10 @@ class ApplicationSettings(BaseCustomSettings, MixinLoggingSettings): EFS_DEFAULT_USER_SERVICE_SIZE_BYTES: ByteSize = Field( default=parse_obj_as(ByteSize, "500GiB") ) + EFS_REMOVAL_POLICY_TASK_AGE_LIMIT_TIMEDELTA: datetime.timedelta = Field( + default=datetime.timedelta(days=10), + description="For how long must a project remain unused before we remove its data from the EFS. (default to seconds, or see https://pydantic-docs.helpmanual.io/usage/types/#datetime-types for string formating)", + ) # RUNTIME ----------------------------------------------------------- EFS_GUARDIAN_DEBUG: bool = Field( @@ -79,6 +85,7 @@ class ApplicationSettings(BaseCustomSettings, MixinLoggingSettings): ) EFS_GUARDIAN_AWS_EFS_SETTINGS: AwsEfsSettings = Field(auto_default_from_env=True) + EFS_GUARDIAN_POSTGRES: PostgresSettings = Field(auto_default_from_env=True) EFS_GUARDIAN_RABBITMQ: RabbitSettings = Field(auto_default_from_env=True) EFS_GUARDIAN_REDIS: RedisSettings = Field(auto_default_from_env=True) EFS_GUARDIAN_TRACING: TracingSettings | None = Field( diff --git a/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks.py b/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks.py index 8edce477cc78..d5a26991be28 100644 --- a/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks.py +++ b/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks.py @@ -1,18 +1,115 @@ import logging +from contextlib import asynccontextmanager +from datetime import datetime, timedelta, timezone +from typing import AsyncIterator, Final +import redis from fastapi import FastAPI +from models_library.projects import ProjectID +from models_library.projects_state import ProjectLocked, ProjectStatus +from redis.asyncio.lock import Lock +from servicelib.background_task import periodic_task +from servicelib.logging_utils import log_context +from simcore_postgres_database.utils_projects import ProjectsRepo from ..core.settings import ApplicationSettings +from .efs_manager import EfsManager +from .modules.redis import get_redis_lock_client _logger = logging.getLogger(__name__) -async def removal_policy_task(app: FastAPI) -> None: - _logger.info("FAKE Removal policy task started (not yet implemented)") +PROJECT_REDIS_LOCK_KEY: str = "project_lock:{}" +PROJECT_LOCK_TIMEOUT: Final[timedelta] = timedelta(seconds=10) + + +async def _auto_extend_project_lock(project_lock: Lock) -> None: + # NOTE: the background task already catches anything that might raise here + await project_lock.reacquire() + + +@asynccontextmanager +async def lock_project( + app: FastAPI, + project_uuid: ProjectID, + status: ProjectStatus = ProjectStatus.MAINTAINING, +) -> AsyncIterator[None]: + """Context manager to lock and unlock a project by user_id + + Raises: + ProjectLockError: if project is already locked + """ + + redis_lock = get_redis_lock_client(app).redis.lock( + PROJECT_REDIS_LOCK_KEY.format(project_uuid), + timeout=PROJECT_LOCK_TIMEOUT.total_seconds(), + ) + try: + if not await redis_lock.acquire( + blocking=False, + token=ProjectLocked( + value=True, + owner=None, + status=status, + ).json(), + ): + msg = f"Lock for project {project_uuid!r} could not be acquired" + raise ValueError(msg) - # After X days of inactivity remove data from EFS - # Probably use `last_modified_data` in the project DB table - # Maybe lock project during this time lock_project() + with log_context( + _logger, + logging.DEBUG, + msg=f"with lock for {project_uuid=}:{status=}", + ): + async with periodic_task( + _auto_extend_project_lock, + interval=0.6 * PROJECT_LOCK_TIMEOUT, + task_name=f"{PROJECT_REDIS_LOCK_KEY.format(project_uuid)}_lock_auto_extend", + project_lock=redis_lock, + ): + yield + + finally: + try: + if await redis_lock.owned(): + await redis_lock.release() + except (redis.exceptions.LockError, redis.exceptions.LockNotOwnedError) as exc: + _logger.warning( + "releasing %s unexpectedly raised an exception: %s", + f"{redis_lock=!r}", + f"{exc}", + ) + + +async def removal_policy_task(app: FastAPI) -> None: + _logger.info("Removal policy task started") app_settings: ApplicationSettings = app.state.settings assert app_settings # nosec + efs_manager: EfsManager = app.state.efs_manager + + base_start_timestamp = datetime.now(tz=timezone.utc) + + efs_project_ids: list[ + ProjectID + ] = await efs_manager.list_projects_across_whole_efs() + + projects_repo = ProjectsRepo(app.state.engine) + for project_id in efs_project_ids: + _project_last_change_date = await projects_repo.get_project_last_change_date( + project_id + ) + if ( + _project_last_change_date is None + or _project_last_change_date + < base_start_timestamp + - app_settings.EFS_REMOVAL_POLICY_TASK_AGE_LIMIT_TIMEDELTA + ): + _logger.info( + "Removing data for project %s started, project last change date %s, efs removal policy task age limit timedelta %s", + project_id, + _project_last_change_date, + app_settings.EFS_REMOVAL_POLICY_TASK_AGE_LIMIT_TIMEDELTA, + ) + async with lock_project(app, project_uuid=project_id): + await efs_manager.remove_project_efs_data(project_id) diff --git a/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks_setup.py b/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks_setup.py index 0946b82177b9..922f0d8747ca 100644 --- a/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks_setup.py +++ b/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks_setup.py @@ -39,7 +39,7 @@ async def _startup() -> None: exclusive_task = start_exclusive_periodic_task( get_redis_lock_client(app), task["task_func"], - task_period=timedelta(seconds=60), # 1 minute + task_period=timedelta(seconds=3600), # 1 hour retry_after=timedelta(seconds=300), # 5 minutes task_name=task["name"], app=app, diff --git a/services/efs-guardian/src/simcore_service_efs_guardian/services/efs_manager.py b/services/efs-guardian/src/simcore_service_efs_guardian/services/efs_manager.py index be0460b7e644..9767a21461a4 100644 --- a/services/efs-guardian/src/simcore_service_efs_guardian/services/efs_manager.py +++ b/services/efs-guardian/src/simcore_service_efs_guardian/services/efs_manager.py @@ -1,15 +1,19 @@ +import logging import os +import shutil from dataclasses import dataclass from pathlib import Path from fastapi import FastAPI from models_library.projects import ProjectID from models_library.projects_nodes_io import NodeID -from pydantic import ByteSize +from pydantic import ByteSize, parse_obj_as from ..core.settings import ApplicationSettings, get_application_settings from . import efs_manager_utils +_logger = logging.getLogger(__name__) + @dataclass(frozen=True) class EfsManager: @@ -90,3 +94,44 @@ async def remove_project_node_data_write_permissions( ) await efs_manager_utils.remove_write_permissions_bash_async(_dir_path) + + async def list_projects_across_whole_efs(self) -> list[ProjectID]: + _dir_path = self._efs_mounted_path / self._project_specific_data_base_directory + + # List all items in the directory + items = os.listdir(_dir_path) + + # Filter and list only directories (which should be Project UUIDs) + directories = [] + for item in items: + _item_path = _dir_path / item + if Path.is_dir(_item_path): + try: + _project_id = parse_obj_as(ProjectID, item) + directories.append(_project_id) + except ValueError: + _logger.warning( + "This is not a project ID. This should not happen! %s", + _item_path, + ) + else: + _logger.warning( + "This is not a directory. This should not happen! %s", + _item_path, + ) + + return directories + + async def remove_project_efs_data(self, project_id: ProjectID) -> None: + _dir_path = ( + self._efs_mounted_path + / self._project_specific_data_base_directory + / f"{project_id}" + ) + + if Path.exists(_dir_path): + # Remove the directory and all its contents + shutil.rmtree(_dir_path) + _logger.info("%s has been deleted.", _dir_path) + else: + _logger.warning("%s does not exist.", _dir_path) diff --git a/services/efs-guardian/src/simcore_service_efs_guardian/services/modules/db.py b/services/efs-guardian/src/simcore_service_efs_guardian/services/modules/db.py new file mode 100644 index 000000000000..f5d5970216e0 --- /dev/null +++ b/services/efs-guardian/src/simcore_service_efs_guardian/services/modules/db.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI +from servicelib.fastapi.db_asyncpg_engine import close_db_connection, connect_to_db + + +def setup(app: FastAPI): + async def on_startup() -> None: + await connect_to_db(app, app.state.settings.EFS_GUARDIAN_POSTGRES) + + async def on_shutdown() -> None: + await close_db_connection(app) + + app.add_event_handler("startup", on_startup) + app.add_event_handler("shutdown", on_shutdown) + + +def get_database_engine(app: FastAPI): + return app.state.engine diff --git a/services/efs-guardian/tests/conftest.py b/services/efs-guardian/tests/conftest.py new file mode 100644 index 000000000000..e0cf5167a42a --- /dev/null +++ b/services/efs-guardian/tests/conftest.py @@ -0,0 +1,78 @@ +# pylint: disable=protected-access +# pylint: disable=redefined-outer-name +# pylint: disable=too-many-arguments +# pylint: disable=unused-argument +# pylint: disable=unused-variable + + +from pathlib import Path + +import pytest +import simcore_service_efs_guardian +from faker import Faker +from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict +from pytest_simcore.helpers.typing_env import EnvVarsDict + +pytest_plugins = [ + "pytest_simcore.cli_runner", + "pytest_simcore.docker_compose", + "pytest_simcore.docker_registry", + "pytest_simcore.docker_swarm", + "pytest_simcore.environment_configs", + "pytest_simcore.faker_projects_data", + "pytest_simcore.faker_users_data", + "pytest_simcore.faker_projects_data", + "pytest_simcore.pydantic_models", + "pytest_simcore.pytest_global_environs", + "pytest_simcore.rabbit_service", + "pytest_simcore.redis_service", + "pytest_simcore.postgres_service", + "pytest_simcore.repository_paths", + "pytest_simcore.aws_s3_service", + "pytest_simcore.aws_server", + # "pytest_simcore.db_entries_mocks", +] + + +@pytest.fixture(scope="session") +def project_slug_dir(osparc_simcore_root_dir: Path) -> Path: + # fixtures in pytest_simcore.environs + service_folder = osparc_simcore_root_dir / "services" / "payments" + assert service_folder.exists() + assert any(service_folder.glob("src/simcore_service_payments")) + return service_folder + + +@pytest.fixture(scope="session") +def installed_package_dir() -> Path: + dirpath = Path(simcore_service_efs_guardian.__file__).resolve().parent + assert dirpath.exists() + return dirpath + + +@pytest.fixture(scope="session") +def env_devel_dict( + env_devel_dict: EnvVarsDict, external_envfile_dict: EnvVarsDict +) -> EnvVarsDict: + if external_envfile_dict: + return external_envfile_dict + return env_devel_dict + + +@pytest.fixture +def app_environment( + monkeypatch: pytest.MonkeyPatch, + docker_compose_service_environment_dict: EnvVarsDict, + faker: Faker, +) -> EnvVarsDict: + return setenvs_from_dict( + monkeypatch, + { + **docker_compose_service_environment_dict, + "EFS_DNS_NAME": "fs-xxx.efs.us-east-1.amazonaws.com", + "EFS_MOUNTED_PATH": "/tmp/efs", + "EFS_PROJECT_SPECIFIC_DATA_DIRECTORY": "project-specific-data", + "EFS_ONLY_ENABLED_FOR_USERIDS": "[]", + "EFS_GUARDIAN_TRACING": "null", + }, + ) diff --git a/services/efs-guardian/tests/unit/conftest.py b/services/efs-guardian/tests/unit/conftest.py index da4196ea859e..a51b45e5e22e 100644 --- a/services/efs-guardian/tests/unit/conftest.py +++ b/services/efs-guardian/tests/unit/conftest.py @@ -1,126 +1,109 @@ -# pylint:disable=unused-variable -# pylint:disable=unused-argument -# pylint:disable=redefined-outer-name - import os -import re import shutil import stat -from collections.abc import AsyncIterator, Callable +from collections.abc import AsyncIterator, Awaitable, Callable from pathlib import Path -from typing import Awaitable +from unittest.mock import Mock import httpx import pytest -import simcore_service_efs_guardian -import yaml +import sqlalchemy as sa from asgi_lifespan import LifespanManager -from fakeredis.aioredis import FakeRedis +from faker import Faker from fastapi import FastAPI -from httpx import ASGITransport +from httpx._transports.asgi import ASGITransport from pytest_mock import MockerFixture -from pytest_simcore.helpers.monkeypatch_envs import EnvVarsDict, setenvs_from_dict +from pytest_simcore.helpers.typing_env import EnvVarsDict from servicelib.rabbitmq import RabbitMQRPCClient from settings_library.efs import AwsEfsSettings -from settings_library.rabbit import RabbitSettings from simcore_service_efs_guardian.core.application import create_app -from simcore_service_efs_guardian.core.settings import ApplicationSettings - -pytest_plugins = [ - "pytest_simcore.cli_runner", - "pytest_simcore.docker_compose", - "pytest_simcore.docker_registry", - "pytest_simcore.docker_swarm", - "pytest_simcore.environment_configs", - "pytest_simcore.faker_projects_data", - "pytest_simcore.pydantic_models", - "pytest_simcore.pytest_global_environs", - "pytest_simcore.rabbit_service", - "pytest_simcore.redis_service", - "pytest_simcore.repository_paths", - "pytest_simcore.aws_s3_service", - "pytest_simcore.aws_server", -] - - -@pytest.fixture(scope="session") -def project_slug_dir(osparc_simcore_root_dir: Path) -> Path: - # fixtures in pytest_simcore.environs - service_folder = osparc_simcore_root_dir / "services" / "efs_guardian" - assert service_folder.exists() - assert any(service_folder.glob("src/simcore_service_efs_guardian")) - return service_folder - - -@pytest.fixture(scope="session") -def installed_package_dir() -> Path: - dirpath = Path(simcore_service_efs_guardian.__file__).resolve().parent - assert dirpath.exists() - return dirpath + +# +# rabbit-MQ +# + + +@pytest.fixture +def disable_rabbitmq_and_rpc_setup(mocker: MockerFixture) -> Callable: + def _(): + # The following services are affected if rabbitmq is not in place + mocker.patch("simcore_service_efs_guardian.core.application.setup_rabbitmq") + mocker.patch("simcore_service_efs_guardian.core.application.setup_rpc_routes") + mocker.patch( + "simcore_service_efs_guardian.core.application.setup_process_messages" + ) + + return _ @pytest.fixture -def docker_compose_service_efs_guardian_env_vars( - services_docker_compose_file: Path, - env_devel_dict: EnvVarsDict, -) -> EnvVarsDict: - """env vars injected at the docker-compose""" - - payments = yaml.safe_load(services_docker_compose_file.read_text())["services"][ - "efs-guardian" - ] - - def _substitute(key, value): - if m := re.match(r"\${([^{}:-]\w+)", value): - expected_env_var = m.group(1) - try: - # NOTE: if this raises, then the RHS env-vars in the docker-compose are - # not defined in the env-devel - if value := env_devel_dict[expected_env_var]: - return key, value - except KeyError: - pytest.fail( - f"{expected_env_var} is not defined in .env-devel but used in docker-compose services[{payments}].environment[{key}]" - ) - return None - - envs: EnvVarsDict = {} - for key, value in payments.get("environment", {}).items(): - if found := _substitute(key, value): - _, new_value = found - envs[key] = new_value - - return envs +def with_disabled_rabbitmq_and_rpc(disable_rabbitmq_and_rpc_setup: Callable): + disable_rabbitmq_and_rpc_setup() @pytest.fixture -def app_environment( - monkeypatch: pytest.MonkeyPatch, - docker_compose_service_efs_guardian_env_vars: EnvVarsDict, -) -> EnvVarsDict: - return setenvs_from_dict( - monkeypatch, - { - **docker_compose_service_efs_guardian_env_vars, - "EFS_DNS_NAME": "fs-xxx.efs.us-east-1.amazonaws.com", - "EFS_MOUNTED_PATH": "/tmp/efs", - "EFS_PROJECT_SPECIFIC_DATA_DIRECTORY": "project-specific-data", - "EFS_ONLY_ENABLED_FOR_USERIDS": "[]", - }, - ) +async def rpc_client( + faker: Faker, rabbitmq_rpc_client: Callable[[str], Awaitable[RabbitMQRPCClient]] +) -> RabbitMQRPCClient: + return await rabbitmq_rpc_client(f"director-v2-client-{faker.word()}") + + +# +# postgres +# @pytest.fixture -def app_settings(app_environment: EnvVarsDict) -> ApplicationSettings: - settings = ApplicationSettings.create_from_envs() - return settings +def disable_postgres_setup(mocker: MockerFixture) -> Callable: + def _setup(app: FastAPI): + app.state.engine = ( + Mock() + ) # NOTE: avoids error in api._dependencies::get_db_engine + + def _(): + # The following services are affected if postgres is not in place + mocker.patch( + "simcore_service_efs_guardian.core.application.setup_db", + spec=True, + side_effect=_setup, + ) + + return _ @pytest.fixture -async def app(app_settings: ApplicationSettings) -> AsyncIterator[FastAPI]: - the_test_app = create_app(app_settings) +def with_disabled_postgres(disable_postgres_setup: Callable): + disable_postgres_setup() + + +@pytest.fixture +def wait_for_postgres_ready_and_db_migrated(postgres_db: sa.engine.Engine) -> None: + """ + Typical use-case is to include it in + + @pytest.fixture + def app_environment( + ... + postgres_env_vars_dict: EnvVarsDict, + wait_for_postgres_ready_and_db_migrated: None, + ) + """ + assert postgres_db + + +MAX_TIME_FOR_APP_TO_STARTUP = 10 +MAX_TIME_FOR_APP_TO_SHUTDOWN = 10 + + +@pytest.fixture +async def app( + app_environment: EnvVarsDict, is_pdb_enabled: bool +) -> AsyncIterator[FastAPI]: + the_test_app = create_app() async with LifespanManager( the_test_app, + startup_timeout=None if is_pdb_enabled else MAX_TIME_FOR_APP_TO_STARTUP, + shutdown_timeout=None if is_pdb_enabled else MAX_TIME_FOR_APP_TO_SHUTDOWN, ): yield the_test_app @@ -140,23 +123,37 @@ async def client(app: FastAPI) -> AsyncIterator[httpx.AsyncClient]: yield client +# +# Redis +# + + @pytest.fixture -async def rpc_client( - rabbit_service: RabbitSettings, - app: FastAPI, - rabbitmq_rpc_client: Callable[[str], Awaitable[RabbitMQRPCClient]], -) -> RabbitMQRPCClient: - return await rabbitmq_rpc_client("client") +def disable_redis_and_background_tasks_setup(mocker: MockerFixture) -> Callable: + def _(): + # The following services are affected if redis is not in place + mocker.patch("simcore_service_efs_guardian.core.application.setup_redis") + mocker.patch( + "simcore_service_efs_guardian.core.application.setup_background_tasks" + ) + + return _ @pytest.fixture -async def mocked_redis_server(mocker: MockerFixture) -> None: - mock_redis = FakeRedis() - mocker.patch("redis.asyncio.from_url", return_value=mock_redis) +def with_disabled_redis_and_background_tasks( + disable_redis_and_background_tasks_setup: Callable, +): + disable_redis_and_background_tasks_setup() + + +# +# Others +# @pytest.fixture -async def cleanup(app: FastAPI): +async def efs_cleanup(app: FastAPI): yield diff --git a/services/efs-guardian/tests/unit/test_api_health.py b/services/efs-guardian/tests/unit/test_api_health.py index 8b42d559e7f7..1818e03093e3 100644 --- a/services/efs-guardian/tests/unit/test_api_health.py +++ b/services/efs-guardian/tests/unit/test_api_health.py @@ -6,10 +6,9 @@ import pytest from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict from pytest_simcore.helpers.typing_env import EnvVarsDict -from settings_library.rabbit import RabbitSettings from starlette import status -pytest_simcore_core_services_selection = ["rabbit"] +pytest_simcore_core_services_selection = [] pytest_simcore_ops_services_selection = [] @@ -17,20 +16,20 @@ def app_environment( monkeypatch: pytest.MonkeyPatch, app_environment: EnvVarsDict, - rabbit_env_vars_dict: EnvVarsDict, # rabbitMQ settings from 'rabbit' service -) -> EnvVarsDict: + with_disabled_redis_and_background_tasks: None, + with_disabled_rabbitmq_and_rpc: None, + with_disabled_postgres: None, +): return setenvs_from_dict( monkeypatch, { **app_environment, - **rabbit_env_vars_dict, + # **rabbit_env_vars_dict, }, ) async def test_healthcheck( - rabbit_service: RabbitSettings, - mocked_redis_server: None, client: httpx.AsyncClient, ): response = await client.get("/") diff --git a/services/efs-guardian/tests/unit/test_efs_guardian_rpc.py b/services/efs-guardian/tests/unit/test_efs_guardian_rpc.py index 48474a69d454..e5d2dfab27a4 100644 --- a/services/efs-guardian/tests/unit/test_efs_guardian_rpc.py +++ b/services/efs-guardian/tests/unit/test_efs_guardian_rpc.py @@ -27,6 +27,8 @@ def app_environment( monkeypatch: pytest.MonkeyPatch, app_environment: EnvVarsDict, rabbit_env_vars_dict: EnvVarsDict, # rabbitMQ settings from 'rabbit' service + with_disabled_redis_and_background_tasks: None, + with_disabled_postgres: None, ) -> EnvVarsDict: return setenvs_from_dict( monkeypatch, @@ -38,13 +40,12 @@ def app_environment( async def test_rpc_create_project_specific_data_dir( - mocked_redis_server: None, rpc_client: RabbitMQRPCClient, faker: Faker, app: FastAPI, project_id: ProjectID, node_id: NodeID, - cleanup: None, + efs_cleanup: None, ): aws_efs_settings: AwsEfsSettings = app.state.settings.EFS_GUARDIAN_AWS_EFS_SETTINGS diff --git a/services/efs-guardian/tests/unit/test_efs_manager.py b/services/efs-guardian/tests/unit/test_efs_manager.py index 5c5c57cf3aba..b889b4cd1668 100644 --- a/services/efs-guardian/tests/unit/test_efs_manager.py +++ b/services/efs-guardian/tests/unit/test_efs_manager.py @@ -20,7 +20,7 @@ ProjectID, ) -pytest_simcore_core_services_selection = ["rabbit"] +pytest_simcore_core_services_selection = ["rabbit", "postgres"] pytest_simcore_ops_services_selection = [] @@ -29,6 +29,8 @@ def app_environment( monkeypatch: pytest.MonkeyPatch, app_environment: EnvVarsDict, rabbit_env_vars_dict: EnvVarsDict, + with_disabled_redis_and_background_tasks: None, + with_disabled_postgres: None, ) -> EnvVarsDict: return setenvs_from_dict( monkeypatch, @@ -64,9 +66,8 @@ def assert_permissions( async def test_remove_write_access_rights( faker: Faker, - mocked_redis_server: None, app: FastAPI, - cleanup: None, + efs_cleanup: None, project_id: ProjectID, node_id: NodeID, ): diff --git a/services/efs-guardian/tests/unit/test_efs_removal_policy_task.py b/services/efs-guardian/tests/unit/test_efs_removal_policy_task.py new file mode 100644 index 000000000000..58f197a95ccc --- /dev/null +++ b/services/efs-guardian/tests/unit/test_efs_removal_policy_task.py @@ -0,0 +1,172 @@ +# pylint: disable=protected-access +# pylint: disable=redefined-outer-name +# pylint: disable=too-many-arguments +# pylint: disable=unused-argument +# pylint: disable=unused-variable + + +from collections.abc import AsyncIterator +from datetime import datetime, timedelta +from pathlib import Path +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest +from faker import Faker +from fastapi import FastAPI +from models_library.users import UserID +from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict +from pytest_simcore.helpers.postgres_tools import insert_and_get_row_lifespan +from pytest_simcore.helpers.typing_env import EnvVarsDict +from simcore_postgres_database.models.projects import projects +from simcore_postgres_database.models.users import users +from simcore_postgres_database.utils_repos import transaction_context +from simcore_service_efs_guardian.core.settings import ( + ApplicationSettings, + AwsEfsSettings, +) +from simcore_service_efs_guardian.services.background_tasks import removal_policy_task +from simcore_service_efs_guardian.services.efs_manager import ( + EfsManager, + NodeID, + ProjectID, +) + +pytest_simcore_core_services_selection = ["postgres", "redis"] +pytest_simcore_ops_services_selection = [ + "adminer", +] + + +@pytest.fixture +def app_environment( + monkeypatch: pytest.MonkeyPatch, + app_environment: EnvVarsDict, + postgres_env_vars_dict: EnvVarsDict, + with_disabled_rabbitmq_and_rpc: None, + wait_for_postgres_ready_and_db_migrated: None, + with_disabled_redis_and_background_tasks: None, +): + # set environs + monkeypatch.delenv("EFS_GUARDIAN_POSTGRES", raising=False) + + return setenvs_from_dict( + monkeypatch, + { + **app_environment, + **postgres_env_vars_dict, + "POSTGRES_CLIENT_NAME": "efs-guardian-service-pg-client", + "EFS_REMOVAL_POLICY_TASK_AGE_LIMIT_TIMEDELTA": "3600", + }, + ) + + +@pytest.fixture +async def user_in_db( + app: FastAPI, + user: dict[str, Any], + user_id: UserID, +) -> AsyncIterator[dict[str, Any]]: + """ + injects a user in db + """ + assert user_id == user["id"] + async with insert_and_get_row_lifespan( # pylint:disable=contextmanager-generator-missing-cleanup + app.state.engine, + table=users, + values=user, + pk_col=users.c.id, + pk_value=user["id"], + ) as row: + yield row + + +@pytest.fixture +async def project_in_db( + app: FastAPI, + user_in_db: dict, + project: dict[str, Any], + project_id: ProjectID, +) -> AsyncIterator[dict[str, Any]]: + """ + injects a project in db + """ + assert f"{project_id}" == project["uuid"] + async with insert_and_get_row_lifespan( # pylint:disable=contextmanager-generator-missing-cleanup + app.state.engine, + table=projects, + values=project, + pk_col=projects.c.uuid, + pk_value=project["uuid"], + ) as row: + yield row + + +@patch("simcore_service_efs_guardian.services.background_tasks.lock_project") +async def test_efs_removal_policy_task( + mock_lock_project: MagicMock, + faker: Faker, + app: FastAPI, + efs_cleanup: None, + project_id: ProjectID, + node_id: NodeID, + project_in_db: dict, +): + # 1. Nothing should happen + await removal_policy_task(app) + assert not mock_lock_project.called + + # 2. Lets create some project with data + aws_efs_settings: AwsEfsSettings = app.state.settings.EFS_GUARDIAN_AWS_EFS_SETTINGS + _storage_directory_name = faker.word() + _dir_path = ( + aws_efs_settings.EFS_MOUNTED_PATH + / aws_efs_settings.EFS_PROJECT_SPECIFIC_DATA_DIRECTORY + / f"{project_id}" + / f"{node_id}" + / f"{_storage_directory_name}" + ) + efs_manager: EfsManager = app.state.efs_manager + + with patch( + "simcore_service_efs_guardian.services.efs_manager.os.chown" + ) as mocked_chown: + await efs_manager.create_project_specific_data_dir( + project_id=project_id, + node_id=node_id, + storage_directory_name=_storage_directory_name, + ) + assert mocked_chown.called + + file_paths = [] + for i in range(3): # Let's create 3 small files for testing + file_path = Path(_dir_path, f"test_file_{i}.txt") + file_path.write_text(f"This is file {i}") + file_paths.append(file_path) + + # 3. Nothing should happen + await removal_policy_task(app) + assert not mock_lock_project.called + + # 4. We will artifically change the project last change date + app_settings: ApplicationSettings = app.state.settings + _current_timestamp = datetime.now() + _old_timestamp = ( + _current_timestamp + - app_settings.EFS_REMOVAL_POLICY_TASK_AGE_LIMIT_TIMEDELTA + - timedelta(days=1) + ) + async with transaction_context(app.state.engine) as conn: + result = await conn.execute( + projects.update() + .values(last_change_date=_old_timestamp) + .where(projects.c.uuid == f"{project_id}") + ) + result_row_count: int = result.rowcount + assert result_row_count == 1 # nosec + + # 5. Now removal policy should remove those data + await removal_policy_task(app) + assert mock_lock_project.called + projects_list = await efs_manager.list_projects_across_whole_efs() + assert projects_list == [] From bd2e218c6f91d84da0362338c79f8b99b3b41c3f Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Fri, 18 Oct 2024 17:06:05 +0200 Subject: [PATCH 02/17] refactoring project lock --- .../src/servicelib/project_lock.py | 78 +++++++++++++++++ .../unit/test_utils_distributed_identifier.py | 1 - .../services/background_tasks.py | 85 +++---------------- .../tests/unit/test_api_health.py | 1 - 4 files changed, 92 insertions(+), 73 deletions(-) create mode 100644 packages/service-library/src/servicelib/project_lock.py diff --git a/packages/service-library/src/servicelib/project_lock.py b/packages/service-library/src/servicelib/project_lock.py new file mode 100644 index 000000000000..2a58f31385f4 --- /dev/null +++ b/packages/service-library/src/servicelib/project_lock.py @@ -0,0 +1,78 @@ +import datetime +import logging +from asyncio.log import logger +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from typing import Final + +import redis +from models_library.projects import ProjectID +from models_library.projects_access import Owner +from models_library.projects_state import ProjectLocked, ProjectStatus +from redis.asyncio.lock import Lock +from servicelib.background_task import periodic_task +from servicelib.logging_utils import log_context + +_logger = logging.getLogger(__name__) + +PROJECT_REDIS_LOCK_KEY: str = "project_lock:{}" +PROJECT_LOCK_TIMEOUT: Final[datetime.timedelta] = datetime.timedelta(seconds=10) +ProjectLock = Lock + +ProjectLockError = redis.exceptions.LockError + + +async def _auto_extend_project_lock(project_lock: Lock) -> None: + # NOTE: the background task already catches anything that might raise here + await project_lock.reacquire() + + +@asynccontextmanager +async def lock_project( + redis_lock: Lock, + project_uuid: str | ProjectID, + status: ProjectStatus, + owner: Owner | None = None, +) -> AsyncIterator[None]: + """Context manager to lock and unlock a project by user_id + + Raises: + ProjectLockError: if project is already locked + """ + + try: + if not await redis_lock.acquire( + blocking=False, + token=ProjectLocked( + value=True, + owner=owner, + status=status, + ).json(), + ): + msg = f"Lock for project {project_uuid!r} owner {owner!r} could not be acquired" + raise ProjectLockError(msg) + + with log_context( + _logger, + logging.DEBUG, + msg=f"with lock for {owner=}:{project_uuid=}:{status=}", + ): + async with periodic_task( + _auto_extend_project_lock, + interval=0.6 * PROJECT_LOCK_TIMEOUT, + task_name=f"{PROJECT_REDIS_LOCK_KEY.format(project_uuid)}_lock_auto_extend", + project_lock=redis_lock, + ): + yield + + finally: + # let's ensure we release that stuff + try: + if await redis_lock.owned(): + await redis_lock.release() + except (redis.exceptions.LockError, redis.exceptions.LockNotOwnedError) as exc: + logger.warning( + "releasing %s unexpectedly raised an exception: %s", + f"{redis_lock=!r}", + f"{exc}", + ) diff --git a/services/director-v2/tests/unit/test_utils_distributed_identifier.py b/services/director-v2/tests/unit/test_utils_distributed_identifier.py index 6d4723507c31..ce200feef977 100644 --- a/services/director-v2/tests/unit/test_utils_distributed_identifier.py +++ b/services/director-v2/tests/unit/test_utils_distributed_identifier.py @@ -163,7 +163,6 @@ async def _destroy( self.api.delete(identifier) -# MD: here redis @pytest.fixture async def redis_client_sdk( redis_service: RedisSettings, diff --git a/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks.py b/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks.py index d5a26991be28..614b195c2cf8 100644 --- a/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks.py +++ b/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks.py @@ -1,15 +1,14 @@ import logging -from contextlib import asynccontextmanager -from datetime import datetime, timedelta, timezone -from typing import AsyncIterator, Final +from datetime import datetime, timezone -import redis from fastapi import FastAPI from models_library.projects import ProjectID -from models_library.projects_state import ProjectLocked, ProjectStatus -from redis.asyncio.lock import Lock -from servicelib.background_task import periodic_task -from servicelib.logging_utils import log_context +from models_library.projects_state import ProjectStatus +from servicelib.project_lock import ( + PROJECT_LOCK_TIMEOUT, + PROJECT_REDIS_LOCK_KEY, + lock_project, +) from simcore_postgres_database.utils_projects import ProjectsRepo from ..core.settings import ApplicationSettings @@ -19,68 +18,6 @@ _logger = logging.getLogger(__name__) -PROJECT_REDIS_LOCK_KEY: str = "project_lock:{}" -PROJECT_LOCK_TIMEOUT: Final[timedelta] = timedelta(seconds=10) - - -async def _auto_extend_project_lock(project_lock: Lock) -> None: - # NOTE: the background task already catches anything that might raise here - await project_lock.reacquire() - - -@asynccontextmanager -async def lock_project( - app: FastAPI, - project_uuid: ProjectID, - status: ProjectStatus = ProjectStatus.MAINTAINING, -) -> AsyncIterator[None]: - """Context manager to lock and unlock a project by user_id - - Raises: - ProjectLockError: if project is already locked - """ - - redis_lock = get_redis_lock_client(app).redis.lock( - PROJECT_REDIS_LOCK_KEY.format(project_uuid), - timeout=PROJECT_LOCK_TIMEOUT.total_seconds(), - ) - try: - if not await redis_lock.acquire( - blocking=False, - token=ProjectLocked( - value=True, - owner=None, - status=status, - ).json(), - ): - msg = f"Lock for project {project_uuid!r} could not be acquired" - raise ValueError(msg) - - with log_context( - _logger, - logging.DEBUG, - msg=f"with lock for {project_uuid=}:{status=}", - ): - async with periodic_task( - _auto_extend_project_lock, - interval=0.6 * PROJECT_LOCK_TIMEOUT, - task_name=f"{PROJECT_REDIS_LOCK_KEY.format(project_uuid)}_lock_auto_extend", - project_lock=redis_lock, - ): - yield - - finally: - try: - if await redis_lock.owned(): - await redis_lock.release() - except (redis.exceptions.LockError, redis.exceptions.LockNotOwnedError) as exc: - _logger.warning( - "releasing %s unexpectedly raised an exception: %s", - f"{redis_lock=!r}", - f"{exc}", - ) - - async def removal_policy_task(app: FastAPI) -> None: _logger.info("Removal policy task started") @@ -111,5 +48,11 @@ async def removal_policy_task(app: FastAPI) -> None: _project_last_change_date, app_settings.EFS_REMOVAL_POLICY_TASK_AGE_LIMIT_TIMEDELTA, ) - async with lock_project(app, project_uuid=project_id): + redis_lock = get_redis_lock_client(app).redis.lock( + PROJECT_REDIS_LOCK_KEY.format(project_id), + timeout=PROJECT_LOCK_TIMEOUT.total_seconds(), + ) + async with lock_project( + redis_lock, project_uuid=project_id, status=ProjectStatus.MAINTAINING + ): await efs_manager.remove_project_efs_data(project_id) diff --git a/services/efs-guardian/tests/unit/test_api_health.py b/services/efs-guardian/tests/unit/test_api_health.py index 1818e03093e3..a07b8e79e1f3 100644 --- a/services/efs-guardian/tests/unit/test_api_health.py +++ b/services/efs-guardian/tests/unit/test_api_health.py @@ -24,7 +24,6 @@ def app_environment( monkeypatch, { **app_environment, - # **rabbit_env_vars_dict, }, ) From 0a2b1ba6b8ad4f3b7b29a9142c7ea8002addf2ae Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Sun, 20 Oct 2024 12:59:03 +0200 Subject: [PATCH 03/17] fix pylint --- services/efs-guardian/tests/unit/conftest.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/efs-guardian/tests/unit/conftest.py b/services/efs-guardian/tests/unit/conftest.py index a51b45e5e22e..61d2daaba6df 100644 --- a/services/efs-guardian/tests/unit/conftest.py +++ b/services/efs-guardian/tests/unit/conftest.py @@ -1,3 +1,9 @@ +# pylint: disable=protected-access +# pylint: disable=redefined-outer-name +# pylint: disable=too-many-arguments +# pylint: disable=unused-argument +# pylint: disable=unused-variable + import os import shutil import stat From ea50b06ec2c0484c04ec607755ac134e23a26495 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Sun, 20 Oct 2024 13:15:45 +0200 Subject: [PATCH 04/17] refactor project lock --- .../src/servicelib/project_lock.py | 1 + .../projects/exceptions.py | 9 +-- .../projects/lock.py | 61 +++---------------- 3 files changed, 15 insertions(+), 56 deletions(-) diff --git a/packages/service-library/src/servicelib/project_lock.py b/packages/service-library/src/servicelib/project_lock.py index 2a58f31385f4..6bb9d96bc34d 100644 --- a/packages/service-library/src/servicelib/project_lock.py +++ b/packages/service-library/src/servicelib/project_lock.py @@ -6,6 +6,7 @@ from typing import Final import redis +import redis.exceptions from models_library.projects import ProjectID from models_library.projects_access import Owner from models_library.projects_state import ProjectLocked, ProjectStatus diff --git a/services/web/server/src/simcore_service_webserver/projects/exceptions.py b/services/web/server/src/simcore_service_webserver/projects/exceptions.py index 871628ee66d7..ad1ab4346895 100644 --- a/services/web/server/src/simcore_service_webserver/projects/exceptions.py +++ b/services/web/server/src/simcore_service_webserver/projects/exceptions.py @@ -2,9 +2,9 @@ from typing import Any -import redis.exceptions from models_library.projects import ProjectID from models_library.users import UserID +from servicelib.project_lock import ProjectLockError from ..errors import WebServerBaseError @@ -104,9 +104,6 @@ def __init__(self, *, project_uuid: str | None, **ctx): self.project_uuid = project_uuid -ProjectLockError = redis.exceptions.LockError - - class ProjectStartsTooManyDynamicNodesError(BaseProjectError): msg_template = "The maximal amount of concurrently running dynamic services was reached. Please manually stop a service and retry." @@ -224,3 +221,7 @@ class InvalidInputValue(WebServerBaseError): class ProjectGroupNotFoundError(BaseProjectError): msg_template = "Project group not found. {reason}" + + +assert ProjectLockError # nosec +__all__: tuple[str, ...] = ("ProjectLockError",) diff --git a/services/web/server/src/simcore_service_webserver/projects/lock.py b/services/web/server/src/simcore_service_webserver/projects/lock.py index eccab79c45a2..49a0e576a366 100644 --- a/services/web/server/src/simcore_service_webserver/projects/lock.py +++ b/services/web/server/src/simcore_service_webserver/projects/lock.py @@ -1,34 +1,21 @@ -import datetime import logging -from asyncio.log import logger from collections.abc import AsyncIterator from contextlib import asynccontextmanager -from typing import Final -import redis from aiohttp import web from models_library.projects import ProjectID from models_library.projects_access import Owner from models_library.projects_state import ProjectLocked, ProjectStatus -from redis.asyncio.lock import Lock -from servicelib.background_task import periodic_task -from servicelib.logging_utils import log_context + +# from .exceptions import ProjectLockError +from servicelib.project_lock import PROJECT_LOCK_TIMEOUT, PROJECT_REDIS_LOCK_KEY +from servicelib.project_lock import lock_project as common_lock_project from ..redis import get_redis_lock_manager_client from ..users.api import FullNameDict -from .exceptions import ProjectLockError _logger = logging.getLogger(__name__) -PROJECT_REDIS_LOCK_KEY: str = "project_lock:{}" -PROJECT_LOCK_TIMEOUT: Final[datetime.timedelta] = datetime.timedelta(seconds=10) -ProjectLock = Lock - - -async def _auto_extend_project_lock(project_lock: Lock) -> None: - # NOTE: the background task already catches anything that might raise here - await project_lock.reacquire() - @asynccontextmanager async def lock_project( @@ -48,42 +35,12 @@ async def lock_project( PROJECT_REDIS_LOCK_KEY.format(project_uuid), timeout=PROJECT_LOCK_TIMEOUT.total_seconds(), ) - try: - if not await redis_lock.acquire( - blocking=False, - token=ProjectLocked( - value=True, - owner=Owner(user_id=user_id, **user_fullname), # type: ignore[arg-type] - status=status, - ).json(), - ): - msg = f"Lock for project {project_uuid!r} user {user_id!r} could not be acquired" - raise ProjectLockError(msg) - - with log_context( - _logger, - logging.DEBUG, - msg=f"with lock for {user_id=}:{user_fullname=}:{project_uuid=}:{status=}", - ): - async with periodic_task( - _auto_extend_project_lock, - interval=0.6 * PROJECT_LOCK_TIMEOUT, - task_name=f"{PROJECT_REDIS_LOCK_KEY.format(project_uuid)}_lock_auto_extend", - project_lock=redis_lock, - ): - yield + owner = Owner(user_id=user_id, **user_fullname) # type: ignore[arg-type] - finally: - # let's ensure we release that stuff - try: - if await redis_lock.owned(): - await redis_lock.release() - except (redis.exceptions.LockError, redis.exceptions.LockNotOwnedError) as exc: - logger.warning( - "releasing %s unexpectedly raised an exception: %s", - f"{redis_lock=!r}", - f"{exc}", - ) + async with common_lock_project( + redis_lock, project_uuid=project_uuid, status=status, owner=owner + ): + yield async def is_project_locked( From bf3a36d7dddbac384f6b11d82a02b73ed736de2a Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Sun, 20 Oct 2024 13:27:19 +0200 Subject: [PATCH 05/17] fix tests --- services/efs-guardian/tests/conftest.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/efs-guardian/tests/conftest.py b/services/efs-guardian/tests/conftest.py index e0cf5167a42a..f936a1263a33 100644 --- a/services/efs-guardian/tests/conftest.py +++ b/services/efs-guardian/tests/conftest.py @@ -74,5 +74,11 @@ def app_environment( "EFS_PROJECT_SPECIFIC_DATA_DIRECTORY": "project-specific-data", "EFS_ONLY_ENABLED_FOR_USERIDS": "[]", "EFS_GUARDIAN_TRACING": "null", + "SC_USER_ID": "8004", + "SC_USER_NAME": "scu", + "EFS_USER_ID": "8006", + "EFS_USER_NAME": "efs", + "EFS_GROUP_ID": "8106", + "EFS_GROUP_NAME": "efs-group", }, ) From 5349e3fa46811eda6d306778ecd57eb63ecf64d5 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Sun, 20 Oct 2024 13:46:05 +0200 Subject: [PATCH 06/17] fix fixture with same name --- .../src/pytest_simcore/faker_projects_data.py | 2 +- services/director-v2/.env-devel | 2 +- .../tests/unit/test_efs_removal_policy_task.py | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/pytest-simcore/src/pytest_simcore/faker_projects_data.py b/packages/pytest-simcore/src/pytest_simcore/faker_projects_data.py index 5e993471ec85..4a027a42e2d5 100644 --- a/packages/pytest-simcore/src/pytest_simcore/faker_projects_data.py +++ b/packages/pytest-simcore/src/pytest_simcore/faker_projects_data.py @@ -50,7 +50,7 @@ def node_id(faker: Faker) -> NodeID: @pytest.fixture -def project( +def project_data( faker: Faker, project_id: ProjectID, user_id: UserID, diff --git a/services/director-v2/.env-devel b/services/director-v2/.env-devel index 9244f2d8723a..dfe79b57cad3 100644 --- a/services/director-v2/.env-devel +++ b/services/director-v2/.env-devel @@ -31,7 +31,7 @@ DIRECTOR_V2_GENERIC_RESOURCE_PLACEMENT_CONSTRAINTS_SUBSTITUTIONS='{}' LOG_LEVEL=DEBUG -POSTGRES_ENDPOINT=${POSTGRES_ENDPOINT} +POSTGRES_ENDPOINT=postgres:5432 POSTGRES_USER=test POSTGRES_PASSWORD=test POSTGRES_DB=test diff --git a/services/efs-guardian/tests/unit/test_efs_removal_policy_task.py b/services/efs-guardian/tests/unit/test_efs_removal_policy_task.py index 58f197a95ccc..25911367ed15 100644 --- a/services/efs-guardian/tests/unit/test_efs_removal_policy_task.py +++ b/services/efs-guardian/tests/unit/test_efs_removal_policy_task.py @@ -85,19 +85,19 @@ async def user_in_db( async def project_in_db( app: FastAPI, user_in_db: dict, - project: dict[str, Any], + project_data: dict[str, Any], project_id: ProjectID, ) -> AsyncIterator[dict[str, Any]]: """ injects a project in db """ - assert f"{project_id}" == project["uuid"] + assert f"{project_id}" == project_data["uuid"] async with insert_and_get_row_lifespan( # pylint:disable=contextmanager-generator-missing-cleanup app.state.engine, table=projects, - values=project, + values=project_data, pk_col=projects.c.uuid, - pk_value=project["uuid"], + pk_value=project_data["uuid"], ) as row: yield row From 2656b3d6d221e4392a52443ca7bf201e1c6923d2 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Sun, 20 Oct 2024 13:55:42 +0200 Subject: [PATCH 07/17] final cleanup --- services/efs-guardian/tests/conftest.py | 1 - services/efs-guardian/tests/unit/test_efs_manager.py | 2 +- .../web/server/src/simcore_service_webserver/projects/lock.py | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/services/efs-guardian/tests/conftest.py b/services/efs-guardian/tests/conftest.py index f936a1263a33..4309ea7b0780 100644 --- a/services/efs-guardian/tests/conftest.py +++ b/services/efs-guardian/tests/conftest.py @@ -30,7 +30,6 @@ "pytest_simcore.repository_paths", "pytest_simcore.aws_s3_service", "pytest_simcore.aws_server", - # "pytest_simcore.db_entries_mocks", ] diff --git a/services/efs-guardian/tests/unit/test_efs_manager.py b/services/efs-guardian/tests/unit/test_efs_manager.py index b889b4cd1668..fad03fcaa444 100644 --- a/services/efs-guardian/tests/unit/test_efs_manager.py +++ b/services/efs-guardian/tests/unit/test_efs_manager.py @@ -20,7 +20,7 @@ ProjectID, ) -pytest_simcore_core_services_selection = ["rabbit", "postgres"] +pytest_simcore_core_services_selection = ["rabbit"] pytest_simcore_ops_services_selection = [] diff --git a/services/web/server/src/simcore_service_webserver/projects/lock.py b/services/web/server/src/simcore_service_webserver/projects/lock.py index 49a0e576a366..3141b7bca8db 100644 --- a/services/web/server/src/simcore_service_webserver/projects/lock.py +++ b/services/web/server/src/simcore_service_webserver/projects/lock.py @@ -6,8 +6,6 @@ from models_library.projects import ProjectID from models_library.projects_access import Owner from models_library.projects_state import ProjectLocked, ProjectStatus - -# from .exceptions import ProjectLockError from servicelib.project_lock import PROJECT_LOCK_TIMEOUT, PROJECT_REDIS_LOCK_KEY from servicelib.project_lock import lock_project as common_lock_project From 9de2c25e910b0b39a8e8cd9d583bb35b2d5154c2 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Sun, 20 Oct 2024 18:03:53 +0200 Subject: [PATCH 08/17] fix unit tests --- .../efs-guardian/tests/unit/test_efs_removal_policy_task.py | 5 ++++- .../src/simcore_service_webserver/projects/exceptions.py | 2 +- .../web/server/tests/unit/with_dbs/02/test_project_lock.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/services/efs-guardian/tests/unit/test_efs_removal_policy_task.py b/services/efs-guardian/tests/unit/test_efs_removal_policy_task.py index 25911367ed15..ddc58bb5b8fb 100644 --- a/services/efs-guardian/tests/unit/test_efs_removal_policy_task.py +++ b/services/efs-guardian/tests/unit/test_efs_removal_policy_task.py @@ -102,9 +102,11 @@ async def project_in_db( yield row +@patch("simcore_service_efs_guardian.services.background_tasks.get_redis_lock_client") @patch("simcore_service_efs_guardian.services.background_tasks.lock_project") async def test_efs_removal_policy_task( mock_lock_project: MagicMock, + mock_get_redis_lock_client: MagicMock, faker: Faker, app: FastAPI, efs_cleanup: None, @@ -167,6 +169,7 @@ async def test_efs_removal_policy_task( # 5. Now removal policy should remove those data await removal_policy_task(app) - assert mock_lock_project.called + assert mock_lock_project.assert_called_once + assert mock_get_redis_lock_client.assert_called_once projects_list = await efs_manager.list_projects_across_whole_efs() assert projects_list == [] diff --git a/services/web/server/src/simcore_service_webserver/projects/exceptions.py b/services/web/server/src/simcore_service_webserver/projects/exceptions.py index ad1ab4346895..9741be8e9618 100644 --- a/services/web/server/src/simcore_service_webserver/projects/exceptions.py +++ b/services/web/server/src/simcore_service_webserver/projects/exceptions.py @@ -1,5 +1,5 @@ """Defines the different exceptions that may arise in the projects subpackage""" - +# mypy: disable-error-code=truthy-function from typing import Any from models_library.projects import ProjectID diff --git a/services/web/server/tests/unit/with_dbs/02/test_project_lock.py b/services/web/server/tests/unit/with_dbs/02/test_project_lock.py index 8233994b2ef0..f70a44c7bbc4 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_project_lock.py +++ b/services/web/server/tests/unit/with_dbs/02/test_project_lock.py @@ -13,9 +13,9 @@ from models_library.projects_state import ProjectLocked, ProjectStatus from models_library.users import UserID from pydantic import parse_raw_as +from simcore_service_webserver.projects.exceptions import ProjectLockError from simcore_service_webserver.projects.lock import ( PROJECT_REDIS_LOCK_KEY, - ProjectLockError, get_project_locked_state, is_project_locked, lock_project, From 2ada7e667d1502e55245e15e7fef01da9d45dcc8 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Mon, 21 Oct 2024 16:41:19 +0200 Subject: [PATCH 09/17] review @sanderegg --- .../utils_projects.py | 17 +++-- .../tests/test_utils_projects.py | 68 +++++++++++++++++++ .../services/background_tasks.py | 20 ++++-- 3 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 packages/postgres-database/tests/test_utils_projects.py diff --git a/packages/postgres-database/src/simcore_postgres_database/utils_projects.py b/packages/postgres-database/src/simcore_postgres_database/utils_projects.py index 97af0cae507e..4049b32d91a2 100644 --- a/packages/postgres-database/src/simcore_postgres_database/utils_projects.py +++ b/packages/postgres-database/src/simcore_postgres_database/utils_projects.py @@ -1,7 +1,8 @@ -import uuid from datetime import datetime, timezone import sqlalchemy as sa +from models_library.errors_classes import OsparcErrorMixin +from models_library.projects import ProjectID from pydantic import parse_obj_as from sqlalchemy.ext.asyncio import AsyncConnection @@ -9,16 +10,24 @@ from .utils_repos import transaction_context +class DBBaseProjectError(OsparcErrorMixin, Exception): + ... + + +class DBProjectNotFoundError(DBBaseProjectError): + project_uuid: ProjectID + + class ProjectsRepo: def __init__(self, engine): self.engine = engine async def get_project_last_change_date( self, - project_uuid: uuid.UUID, + project_uuid: ProjectID, *, connection: AsyncConnection | None = None, - ) -> datetime | None: + ) -> datetime: async with transaction_context(self.engine, connection) as conn: get_stmt = sa.select(projects.c.last_change_date).where( projects.c.uuid == f"{project_uuid}" @@ -27,6 +36,6 @@ async def get_project_last_change_date( result = await conn.execute(get_stmt) row = result.first() if row is None: - return None + raise DBProjectNotFoundError(project_uuid=project_uuid) date = parse_obj_as(datetime, row[0]) return date.replace(tzinfo=timezone.utc) diff --git a/packages/postgres-database/tests/test_utils_projects.py b/packages/postgres-database/tests/test_utils_projects.py new file mode 100644 index 000000000000..c4b1e001406a --- /dev/null +++ b/packages/postgres-database/tests/test_utils_projects.py @@ -0,0 +1,68 @@ +# pylint: disable=redefined-outer-name +# pylint: disable=unused-argument +# pylint: disable=unused-variable +# pylint: disable=too-many-arguments +import uuid +from collections.abc import Awaitable, Callable +from datetime import datetime +from typing import Any, AsyncIterator + +import pytest +import sqlalchemy +from aiopg.sa.connection import SAConnection +from aiopg.sa.result import RowProxy +from simcore_postgres_database.models.projects import projects +from simcore_postgres_database.utils_projects import ( + DBProjectNotFoundError, + ProjectsRepo, +) +from sqlalchemy.ext.asyncio import AsyncEngine + + +async def _delete_project(connection: SAConnection, project_uuid: uuid.UUID) -> None: + result = await connection.execute( + sqlalchemy.delete(projects).where(projects.c.uuid == f"{project_uuid}") + ) + assert result.rowcount == 1 + + +@pytest.fixture +async def registered_user( + connection: SAConnection, + create_fake_user: Callable[..., Awaitable[RowProxy]], +) -> RowProxy: + user = await create_fake_user(connection) + assert user + return user + + +@pytest.fixture +async def registered_project( + connection: SAConnection, + registered_user: RowProxy, + create_fake_project: Callable[..., Awaitable[RowProxy]], +) -> AsyncIterator[dict[str, Any]]: + project = await create_fake_project(connection, registered_user) + assert project + + yield dict(project) + + await _delete_project(connection, project["uuid"]) + + +async def test_get_project_last_change_date( + asyncpg_engine: AsyncEngine, + registered_project: dict, +): + # conn = connection + projects_repo = ProjectsRepo(asyncpg_engine) + + project_last_change_date = await projects_repo.get_project_last_change_date( + project_uuid=registered_project["uuid"] + ) + assert isinstance(project_last_change_date, datetime) + + with pytest.raises(DBProjectNotFoundError): + await projects_repo.get_project_last_change_date( + project_uuid="976b031b-828e-4d88-836a-6c2fe4823c6b" # <-- Non existing uuid in DB + ) diff --git a/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks.py b/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks.py index 614b195c2cf8..699353729c62 100644 --- a/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks.py +++ b/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks.py @@ -9,7 +9,10 @@ PROJECT_REDIS_LOCK_KEY, lock_project, ) -from simcore_postgres_database.utils_projects import ProjectsRepo +from simcore_postgres_database.utils_projects import ( + DBProjectNotFoundError, + ProjectsRepo, +) from ..core.settings import ApplicationSettings from .efs_manager import EfsManager @@ -33,12 +36,17 @@ async def removal_policy_task(app: FastAPI) -> None: projects_repo = ProjectsRepo(app.state.engine) for project_id in efs_project_ids: - _project_last_change_date = await projects_repo.get_project_last_change_date( - project_id - ) + try: + _project_last_change_date = ( + await projects_repo.get_project_last_change_date(project_id) + ) + except DBProjectNotFoundError as exc: + _logger.warning( + "Project %s not found, this should not happen, please investigate (contact MD)", + exc.project_uuid, + ) if ( - _project_last_change_date is None - or _project_last_change_date + _project_last_change_date < base_start_timestamp - app_settings.EFS_REMOVAL_POLICY_TASK_AGE_LIMIT_TIMEDELTA ): From 70020a40a53bd0742c10d8fee2a8f86a80181f93 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Mon, 21 Oct 2024 16:47:56 +0200 Subject: [PATCH 10/17] note about test --- packages/service-library/tests/test_project_lock.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/service-library/tests/test_project_lock.py diff --git a/packages/service-library/tests/test_project_lock.py b/packages/service-library/tests/test_project_lock.py new file mode 100644 index 000000000000..386c14be3fbb --- /dev/null +++ b/packages/service-library/tests/test_project_lock.py @@ -0,0 +1 @@ +# NOTE: Tested in osparc-simcore/services/web/server/tests/unit/with_dbs/02/test_project_lock.py From 10e9b7f07a1623f5e30365b6b48c7cf7dca853c1 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Mon, 21 Oct 2024 17:17:25 +0200 Subject: [PATCH 11/17] review @pcrespov --- .../src/servicelib/project_lock.py | 5 +++-- .../services/efs_manager.py | 19 ++++++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/service-library/src/servicelib/project_lock.py b/packages/service-library/src/servicelib/project_lock.py index 6bb9d96bc34d..072282dc955b 100644 --- a/packages/service-library/src/servicelib/project_lock.py +++ b/packages/service-library/src/servicelib/project_lock.py @@ -11,8 +11,9 @@ from models_library.projects_access import Owner from models_library.projects_state import ProjectLocked, ProjectStatus from redis.asyncio.lock import Lock -from servicelib.background_task import periodic_task -from servicelib.logging_utils import log_context + +from .background_task import periodic_task +from .logging_utils import log_context _logger = logging.getLogger(__name__) diff --git a/services/efs-guardian/src/simcore_service_efs_guardian/services/efs_manager.py b/services/efs-guardian/src/simcore_service_efs_guardian/services/efs_manager.py index 9767a21461a4..5e70ff73d0cb 100644 --- a/services/efs-guardian/src/simcore_service_efs_guardian/services/efs_manager.py +++ b/services/efs-guardian/src/simcore_service_efs_guardian/services/efs_manager.py @@ -110,12 +110,12 @@ async def list_projects_across_whole_efs(self) -> list[ProjectID]: _project_id = parse_obj_as(ProjectID, item) directories.append(_project_id) except ValueError: - _logger.warning( + _logger.error( "This is not a project ID. This should not happen! %s", _item_path, ) else: - _logger.warning( + _logger.error( "This is not a directory. This should not happen! %s", _item_path, ) @@ -131,7 +131,16 @@ async def remove_project_efs_data(self, project_id: ProjectID) -> None: if Path.exists(_dir_path): # Remove the directory and all its contents - shutil.rmtree(_dir_path) - _logger.info("%s has been deleted.", _dir_path) + try: + shutil.rmtree(_dir_path) + _logger.info("%s has been deleted.", _dir_path) + except FileNotFoundError as e: + _logger.error("Directory %s does not exist.", _dir_path) + except PermissionError as e: + _logger.error("Permission denied when trying to delete %s.", _dir_path) + except NotADirectoryError as e: + _logger.error("%s is not a directory.", _dir_path) + except OSError as e: + _logger.error("Error: %s. Issue with path: %s", e, _dir_path) else: - _logger.warning("%s does not exist.", _dir_path) + _logger.error("%s does not exist.", _dir_path) From dea70a6a6cb8bb865d52277fd3a33a01676bfcd1 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Mon, 21 Oct 2024 17:19:04 +0200 Subject: [PATCH 12/17] review @pcrespov --- .../services/background_tasks_setup.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks_setup.py b/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks_setup.py index 922f0d8747ca..81920ab23103 100644 --- a/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks_setup.py +++ b/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks_setup.py @@ -14,6 +14,10 @@ _logger = logging.getLogger(__name__) +_SEC = 1000 # in ms +_MIN = 60 * _SEC # in ms +_HOUR = 60 * _MIN # in ms + class EfsGuardianBackgroundTask(TypedDict): name: str @@ -39,8 +43,8 @@ async def _startup() -> None: exclusive_task = start_exclusive_periodic_task( get_redis_lock_client(app), task["task_func"], - task_period=timedelta(seconds=3600), # 1 hour - retry_after=timedelta(seconds=300), # 5 minutes + task_period=timedelta(seconds=1 * _HOUR), + retry_after=timedelta(seconds=5 * _MIN), task_name=task["name"], app=app, ) From e8a5300ec3ec3cefebb56886d96308a837f9aaf7 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Mon, 21 Oct 2024 17:25:25 +0200 Subject: [PATCH 13/17] review @sanderegg --- packages/postgres-database/tests/test_utils_projects.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/postgres-database/tests/test_utils_projects.py b/packages/postgres-database/tests/test_utils_projects.py index c4b1e001406a..9990a8a6ddab 100644 --- a/packages/postgres-database/tests/test_utils_projects.py +++ b/packages/postgres-database/tests/test_utils_projects.py @@ -11,6 +11,7 @@ import sqlalchemy from aiopg.sa.connection import SAConnection from aiopg.sa.result import RowProxy +from faker import Faker from simcore_postgres_database.models.projects import projects from simcore_postgres_database.utils_projects import ( DBProjectNotFoundError, @@ -51,10 +52,8 @@ async def registered_project( async def test_get_project_last_change_date( - asyncpg_engine: AsyncEngine, - registered_project: dict, + asyncpg_engine: AsyncEngine, registered_project: dict, faker: Faker ): - # conn = connection projects_repo = ProjectsRepo(asyncpg_engine) project_last_change_date = await projects_repo.get_project_last_change_date( @@ -64,5 +63,5 @@ async def test_get_project_last_change_date( with pytest.raises(DBProjectNotFoundError): await projects_repo.get_project_last_change_date( - project_uuid="976b031b-828e-4d88-836a-6c2fe4823c6b" # <-- Non existing uuid in DB + project_uuid=faker.uuid4() # <-- Non existing uuid in DB ) From 0831675e4b162b007d8b6dc5b948fefef34bb931 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Tue, 22 Oct 2024 10:32:05 +0200 Subject: [PATCH 14/17] review @pcrespov --- .../services/efs_manager.py | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/services/efs-guardian/src/simcore_service_efs_guardian/services/efs_manager.py b/services/efs-guardian/src/simcore_service_efs_guardian/services/efs_manager.py index 5e70ff73d0cb..b88667936d42 100644 --- a/services/efs-guardian/src/simcore_service_efs_guardian/services/efs_manager.py +++ b/services/efs-guardian/src/simcore_service_efs_guardian/services/efs_manager.py @@ -98,29 +98,25 @@ async def remove_project_node_data_write_permissions( async def list_projects_across_whole_efs(self) -> list[ProjectID]: _dir_path = self._efs_mounted_path / self._project_specific_data_base_directory - # List all items in the directory - items = os.listdir(_dir_path) - # Filter and list only directories (which should be Project UUIDs) - directories = [] - for item in items: - _item_path = _dir_path / item - if Path.is_dir(_item_path): + project_uuids = [] + for child in _dir_path.iterdir(): + if child.is_dir(): try: - _project_id = parse_obj_as(ProjectID, item) - directories.append(_project_id) + _project_id = parse_obj_as(ProjectID, child.name) + project_uuids.append(_project_id) except ValueError: _logger.error( "This is not a project ID. This should not happen! %s", - _item_path, + _dir_path / child.name, ) else: _logger.error( "This is not a directory. This should not happen! %s", - _item_path, + _dir_path / child.name, ) - return directories + return project_uuids async def remove_project_efs_data(self, project_id: ProjectID) -> None: _dir_path = ( From 7dd1a163eb064871111dc9ca99fa10252394db3e Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Tue, 22 Oct 2024 10:35:29 +0200 Subject: [PATCH 15/17] review @pcrespov --- .../services/background_tasks.py | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks.py b/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks.py index 699353729c62..5ea7f69f68af 100644 --- a/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks.py +++ b/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks.py @@ -4,6 +4,7 @@ from fastapi import FastAPI from models_library.projects import ProjectID from models_library.projects_state import ProjectStatus +from servicelib.logging_utils import log_context from servicelib.project_lock import ( PROJECT_LOCK_TIMEOUT, PROJECT_REDIS_LOCK_KEY, @@ -50,17 +51,18 @@ async def removal_policy_task(app: FastAPI) -> None: < base_start_timestamp - app_settings.EFS_REMOVAL_POLICY_TASK_AGE_LIMIT_TIMEDELTA ): - _logger.info( - "Removing data for project %s started, project last change date %s, efs removal policy task age limit timedelta %s", - project_id, - _project_last_change_date, - app_settings.EFS_REMOVAL_POLICY_TASK_AGE_LIMIT_TIMEDELTA, - ) - redis_lock = get_redis_lock_client(app).redis.lock( - PROJECT_REDIS_LOCK_KEY.format(project_id), - timeout=PROJECT_LOCK_TIMEOUT.total_seconds(), - ) - async with lock_project( - redis_lock, project_uuid=project_id, status=ProjectStatus.MAINTAINING + with log_context( + _logger, + logging.INFO, + msg=f"Removing data for project {project_id} started, project last change date {_project_last_change_date}, efs removal policy task age limit timedelta {app_settings.EFS_REMOVAL_POLICY_TASK_AGE_LIMIT_TIMEDELTA}", ): - await efs_manager.remove_project_efs_data(project_id) + redis_lock = get_redis_lock_client(app).redis.lock( + PROJECT_REDIS_LOCK_KEY.format(project_id), + timeout=PROJECT_LOCK_TIMEOUT.total_seconds(), + ) + async with lock_project( + redis_lock, + project_uuid=project_id, + status=ProjectStatus.MAINTAINING, + ): + await efs_manager.remove_project_efs_data(project_id) From b92b84507578a731c9281628816ca35c2164719b Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Tue, 22 Oct 2024 10:57:46 +0200 Subject: [PATCH 16/17] fix --- .../src/simcore_postgres_database/utils_projects.py | 12 ++++++------ .../services/efs_manager.py | 12 ++++++++---- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/postgres-database/src/simcore_postgres_database/utils_projects.py b/packages/postgres-database/src/simcore_postgres_database/utils_projects.py index 4049b32d91a2..1e44e400b5d0 100644 --- a/packages/postgres-database/src/simcore_postgres_database/utils_projects.py +++ b/packages/postgres-database/src/simcore_postgres_database/utils_projects.py @@ -1,21 +1,21 @@ +import uuid from datetime import datetime, timezone import sqlalchemy as sa -from models_library.errors_classes import OsparcErrorMixin -from models_library.projects import ProjectID from pydantic import parse_obj_as +from pydantic.errors import PydanticErrorMixin from sqlalchemy.ext.asyncio import AsyncConnection from .models.projects import projects from .utils_repos import transaction_context -class DBBaseProjectError(OsparcErrorMixin, Exception): - ... +class DBBaseProjectError(PydanticErrorMixin, Exception): + msg_template: str = "Project utils unexpected error" class DBProjectNotFoundError(DBBaseProjectError): - project_uuid: ProjectID + msg_template: str = "Project project_uuid={project_uuid!r} not found" class ProjectsRepo: @@ -24,7 +24,7 @@ def __init__(self, engine): async def get_project_last_change_date( self, - project_uuid: ProjectID, + project_uuid: uuid.UUID, *, connection: AsyncConnection | None = None, ) -> datetime: diff --git a/services/efs-guardian/src/simcore_service_efs_guardian/services/efs_manager.py b/services/efs-guardian/src/simcore_service_efs_guardian/services/efs_manager.py index b88667936d42..0c7e72679545 100644 --- a/services/efs-guardian/src/simcore_service_efs_guardian/services/efs_manager.py +++ b/services/efs-guardian/src/simcore_service_efs_guardian/services/efs_manager.py @@ -131,12 +131,16 @@ async def remove_project_efs_data(self, project_id: ProjectID) -> None: shutil.rmtree(_dir_path) _logger.info("%s has been deleted.", _dir_path) except FileNotFoundError as e: - _logger.error("Directory %s does not exist.", _dir_path) + _logger.error("Directory %s does not exist. Error: %s", _dir_path, e) except PermissionError as e: - _logger.error("Permission denied when trying to delete %s.", _dir_path) + _logger.error( + "Permission denied when trying to delete %s. Error: %s", + _dir_path, + e, + ) except NotADirectoryError as e: - _logger.error("%s is not a directory.", _dir_path) + _logger.error("%s is not a directory. Error: %s", _dir_path, e) except OSError as e: - _logger.error("Error: %s. Issue with path: %s", e, _dir_path) + _logger.error("Issue with path: %s Error: %s", _dir_path, e) else: _logger.error("%s does not exist.", _dir_path) From 3b74cfb024ac0ca9fdf2127e9d53d9efb952e642 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Tue, 22 Oct 2024 11:28:14 +0200 Subject: [PATCH 17/17] fix --- .../simcore_service_efs_guardian/services/background_tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks.py b/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks.py index 5ea7f69f68af..e6a9ee6ee15c 100644 --- a/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks.py +++ b/services/efs-guardian/src/simcore_service_efs_guardian/services/background_tasks.py @@ -44,7 +44,7 @@ async def removal_policy_task(app: FastAPI) -> None: except DBProjectNotFoundError as exc: _logger.warning( "Project %s not found, this should not happen, please investigate (contact MD)", - exc.project_uuid, + exc.msg_template, ) if ( _project_last_change_date