diff --git a/packages/service-library/src/servicelib/background_task.py b/packages/service-library/src/servicelib/background_task.py index 506dd314ea1..a283c78f606 100644 --- a/packages/service-library/src/servicelib/background_task.py +++ b/packages/service-library/src/servicelib/background_task.py @@ -42,7 +42,7 @@ def periodic( ) -> Callable[ [Callable[P, Coroutine[Any, Any, None]]], Callable[P, Coroutine[Any, Any, None]] ]: - """Calls the function periodically with a given interval. + """Calls the function periodically with a given interval or triggered by an early wake-up event. Arguments: interval -- the interval between calls @@ -58,7 +58,7 @@ def periodic( """ def _decorator( - func: Callable[P, Coroutine[Any, Any, None]], + async_fun: Callable[P, Coroutine[Any, Any, None]], ) -> Callable[P, Coroutine[Any, Any, None]]: class _InternalTryAgain(TryAgain): # Local exception to prevent reacting to similarTryAgain exceptions raised by the wrapped func @@ -82,10 +82,10 @@ class _InternalTryAgain(TryAgain): ), before_sleep=before_sleep_log(_logger, logging.DEBUG), ) - @functools.wraps(func) + @functools.wraps(async_fun) async def _wrapper(*args: P.args, **kwargs: P.kwargs) -> None: with log_catch(_logger, reraise=True): - await func(*args, **kwargs) + await async_fun(*args, **kwargs) raise _InternalTryAgain return _wrapper diff --git a/packages/service-library/src/servicelib/background_task_utils.py b/packages/service-library/src/servicelib/background_task_utils.py index 1564107420d..45119649c26 100644 --- a/packages/service-library/src/servicelib/background_task_utils.py +++ b/packages/service-library/src/servicelib/background_task_utils.py @@ -43,7 +43,7 @@ def _decorator( # Replicas will raise CouldNotAcquireLockError # SEE https://github.com/ITISFoundation/osparc-simcore/issues/7574 (CouldNotAcquireLockError,), - reason="Multiple instances of the periodic task `{coro.__module__}.{coro.__name__}` are running.", + reason=f"Multiple instances of the periodic task `{coro.__module__}.{coro.__name__}` are running.", ) @exclusive( redis_client, @@ -54,6 +54,8 @@ def _decorator( async def _wrapper(*args: P.args, **kwargs: P.kwargs) -> None: return await coro(*args, **kwargs) + # Marks with an identifier (mostly to assert a function has been decorated with this decorator) + setattr(_wrapper, "__exclusive_periodic__", True) # noqa: B010 return _wrapper return _decorator diff --git a/services/web/server/src/simcore_service_webserver/garbage_collector/_tasks_api_keys.py b/services/web/server/src/simcore_service_webserver/garbage_collector/_tasks_api_keys.py index 73c2d570ebf..6cb27316b0f 100644 --- a/services/web/server/src/simcore_service_webserver/garbage_collector/_tasks_api_keys.py +++ b/services/web/server/src/simcore_service_webserver/garbage_collector/_tasks_api_keys.py @@ -3,71 +3,51 @@ """ -import asyncio import logging -from collections.abc import AsyncIterator, Callable +from collections.abc import AsyncIterator +from datetime import timedelta from aiohttp import web -from common_library.async_tools import cancel_wait_task -from tenacity import retry -from tenacity.before_sleep import before_sleep_log -from tenacity.wait import wait_exponential +from servicelib.background_task_utils import exclusive_periodic +from servicelib.logging_utils import log_context +from simcore_service_webserver.redis import get_redis_lock_manager_client_sdk from ..api_keys import api_keys_service +from ._tasks_utils import CleanupContextFunc, periodic_task_lifespan -logger = logging.getLogger(__name__) +_logger = logging.getLogger(__name__) -CleanupContextFunc = Callable[[web.Application], AsyncIterator[None]] - -_PERIODIC_TASK_NAME = f"{__name__}.prune_expired_api_keys_periodically" -_APP_TASK_KEY = f"{_PERIODIC_TASK_NAME}.task" - - -@retry( - wait=wait_exponential(min=5, max=30), - before_sleep=before_sleep_log(logger, logging.WARNING), -) -async def _run_task(app: web.Application): - """Periodically check expiration dates and updates user status - - It is resilient, i.e. if update goes wrong, it waits a bit and retries - """ +async def _prune_expired_api_keys(app: web.Application): if deleted := await api_keys_service.prune_expired_api_keys(app): # broadcast force logout of user_id for api_key in deleted: - logger.info("API-key %s expired and was removed", f"{api_key=}") + _logger.info("API-key %s expired and was removed", f"{api_key=}") else: - logger.info("No API keys expired") - - -async def _run_periodically(app: web.Application, wait_period_s: float): - """Periodically check expiration dates and updates user status - - It is resilient, i.e. if update goes wrong, it waits a bit and retries - """ - while True: - await _run_task(app) - await asyncio.sleep(wait_period_s) + _logger.info("No API keys expired") def create_background_task_to_prune_api_keys( - wait_period_s: float, task_name: str = _PERIODIC_TASK_NAME + wait_period_s: float, ) -> CleanupContextFunc: - async def _cleanup_ctx_fun( - app: web.Application, - ) -> AsyncIterator[None]: - # setup - task = asyncio.create_task( - _run_periodically(app, wait_period_s), - name=task_name, - ) - app[_APP_TASK_KEY] = task - yield + async def _cleanup_ctx_fun(app: web.Application) -> AsyncIterator[None]: + interval = timedelta(seconds=wait_period_s) - # tear-down - await cancel_wait_task(task) + @exclusive_periodic( + # Function-exclusiveness is required to avoid multiple tasks like thisone running concurrently + get_redis_lock_manager_client_sdk(app), + task_interval=interval, + retry_after=min(timedelta(seconds=10), interval / 10), + ) + async def _prune_expired_api_keys_periodically() -> None: + with log_context(_logger, logging.INFO, "Pruning expired API keys"): + await _prune_expired_api_keys(app) + + async for _ in periodic_task_lifespan( + app, _prune_expired_api_keys_periodically + ): + yield return _cleanup_ctx_fun diff --git a/services/web/server/src/simcore_service_webserver/garbage_collector/_tasks_core.py b/services/web/server/src/simcore_service_webserver/garbage_collector/_tasks_core.py index 255c9d006dc..7097af49d9f 100644 --- a/services/web/server/src/simcore_service_webserver/garbage_collector/_tasks_core.py +++ b/services/web/server/src/simcore_service_webserver/garbage_collector/_tasks_core.py @@ -4,97 +4,43 @@ Specifics of the gc implementation should go into garbage_collector_core.py """ -import asyncio import logging -from collections.abc import AsyncGenerator +from collections.abc import AsyncIterator +from datetime import timedelta from aiohttp import web -from common_library.async_tools import cancel_wait_task +from servicelib.background_task_utils import exclusive_periodic from servicelib.logging_utils import log_context +from simcore_service_webserver.redis import get_redis_lock_manager_client_sdk from ._core import collect_garbage +from ._tasks_utils import CleanupContextFunc, periodic_task_lifespan from .settings import GarbageCollectorSettings, get_plugin_settings _logger = logging.getLogger(__name__) +_GC_TASK_NAME = f"{__name__}._collect_garbage_periodically" -_GC_TASK_NAME = f"background-task.{__name__}.collect_garbage_periodically" -_GC_TASK_CONFIG = f"{_GC_TASK_NAME}.config" -_GC_TASK = f"{_GC_TASK_NAME}.task" +def create_background_task_for_garbage_collection() -> CleanupContextFunc: -async def run_background_task(app: web.Application) -> AsyncGenerator: - # SETUP ------ - # create a background task to collect garbage periodically - assert not any( # nosec - t.get_name() == _GC_TASK_NAME for t in asyncio.all_tasks() - ), "Garbage collector task already running. ONLY ONE expected" # nosec + async def _cleanup_ctx_fun(app: web.Application) -> AsyncIterator[None]: + settings: GarbageCollectorSettings = get_plugin_settings(app) + interval = timedelta(seconds=settings.GARBAGE_COLLECTOR_INTERVAL_S) - gc_bg_task = asyncio.create_task( - _collect_garbage_periodically(app), name=_GC_TASK_NAME - ) - # attaches variable to the app's lifetime - app[_GC_TASK] = gc_bg_task + @exclusive_periodic( + # Function-exclusiveness is required to avoid multiple tasks like thisone running concurrently + get_redis_lock_manager_client_sdk(app), + task_interval=interval, + retry_after=min(timedelta(seconds=10), interval / 10), + ) + async def _collect_garbage_periodically() -> None: + with log_context(_logger, logging.INFO, "Garbage collect cycle"): + await collect_garbage(app) - # FIXME: added this config to overcome the state in which the - # task cancelation is ignored and the exceptions enter in a loop - # that never stops the background task. This flag is an additional - # mechanism to enforce stopping the background task - # - # Implemented with a mutable dict to avoid - # DeprecationWarning: Changing state of started or joined application is deprecated - # - app[_GC_TASK_CONFIG] = {"force_stop": False, "name": _GC_TASK_NAME} + async for _ in periodic_task_lifespan( + app, _collect_garbage_periodically, task_name=_GC_TASK_NAME + ): + yield - yield - - # TEAR-DOWN ----- - # controlled cancelation of the gc task - _logger.info("Stopping garbage collector...") - - ack = gc_bg_task.cancel() - assert ack # nosec - - app[_GC_TASK_CONFIG]["force_stop"] = True - - await cancel_wait_task(gc_bg_task) - - -async def _collect_garbage_periodically(app: web.Application): - settings: GarbageCollectorSettings = get_plugin_settings(app) - interval = settings.GARBAGE_COLLECTOR_INTERVAL_S - - while True: - try: - while True: - with log_context(_logger, logging.INFO, "Garbage collect cycle"): - await collect_garbage(app) - - if app[_GC_TASK_CONFIG].get("force_stop", False): - msg = "Forced to stop garbage collection" - raise RuntimeError(msg) - - _logger.info("Garbage collect cycle pauses %ss", interval) - await asyncio.sleep(interval) - - except asyncio.CancelledError: # EXIT # noqa: PERF203 - _logger.info( - "Stopped: Garbage collection task was cancelled, it will not restart!" - ) - # do not catch Cancellation errors - raise - - except Exception: # RESILIENT restart # pylint: disable=broad-except - _logger.warning( - "Stopped: There was an error during garbage collection, restarting...", - exc_info=True, - ) - - if app[_GC_TASK_CONFIG].get("force_stop", False): - _logger.warning("Forced to stop garbage collection") - break - - # will wait 5 seconds to recover before restarting to avoid restart loops - # - it might be that db/redis is down, etc - # - await asyncio.sleep(5) + return _cleanup_ctx_fun diff --git a/services/web/server/src/simcore_service_webserver/garbage_collector/_tasks_trash.py b/services/web/server/src/simcore_service_webserver/garbage_collector/_tasks_trash.py index 121b8b79ee0..48b3a16e328 100644 --- a/services/web/server/src/simcore_service_webserver/garbage_collector/_tasks_trash.py +++ b/services/web/server/src/simcore_service_webserver/garbage_collector/_tasks_trash.py @@ -3,59 +3,37 @@ """ -import asyncio import logging -from collections.abc import AsyncIterator, Callable +from collections.abc import AsyncIterator +from datetime import timedelta from aiohttp import web -from common_library.async_tools import cancel_wait_task +from servicelib.background_task_utils import exclusive_periodic from servicelib.logging_utils import log_context -from tenacity import retry -from tenacity.before_sleep import before_sleep_log -from tenacity.wait import wait_exponential +from simcore_service_webserver.redis import get_redis_lock_manager_client_sdk from ..trash import trash_service +from ._tasks_utils import CleanupContextFunc, periodic_task_lifespan _logger = logging.getLogger(__name__) -CleanupContextFunc = Callable[[web.Application], AsyncIterator[None]] +def create_background_task_to_prune_trash(wait_s: float) -> CleanupContextFunc: -_PERIODIC_TASK_NAME = f"{__name__}" -_APP_TASK_KEY = f"{_PERIODIC_TASK_NAME}.task" + async def _cleanup_ctx_fun(app: web.Application) -> AsyncIterator[None]: + interval = timedelta(seconds=wait_s) - -@retry( - wait=wait_exponential(min=5, max=20), - before_sleep=before_sleep_log(_logger, logging.WARNING), -) -async def _run_task(app: web.Application): - with log_context(_logger, logging.INFO, "Deleting expired trashed items"): - await trash_service.safe_delete_expired_trash_as_admin(app) - - -async def _run_periodically(app: web.Application, wait_interval_s: float): - while True: - await _run_task(app) - await asyncio.sleep(wait_interval_s) - - -def create_background_task_to_prune_trash( - wait_s: float, task_name: str = _PERIODIC_TASK_NAME -) -> CleanupContextFunc: - async def _cleanup_ctx_fun( - app: web.Application, - ) -> AsyncIterator[None]: - # setup - task = asyncio.create_task( - _run_periodically(app, wait_s), - name=task_name, + @exclusive_periodic( + # Function-exclusiveness is required to avoid multiple tasks like thisone running concurrently + get_redis_lock_manager_client_sdk(app), + task_interval=interval, + retry_after=min(timedelta(seconds=10), interval / 10), ) - app[_APP_TASK_KEY] = task - - yield + async def _prune_trash_periodically() -> None: + with log_context(_logger, logging.INFO, "Deleting expired trashed items"): + await trash_service.safe_delete_expired_trash_as_admin(app) - # tear-down - await cancel_wait_task(task) + async for _ in periodic_task_lifespan(app, _prune_trash_periodically): + yield return _cleanup_ctx_fun diff --git a/services/web/server/src/simcore_service_webserver/garbage_collector/_tasks_users.py b/services/web/server/src/simcore_service_webserver/garbage_collector/_tasks_users.py index 26cdb9e053c..95d5e5e3a47 100644 --- a/services/web/server/src/simcore_service_webserver/garbage_collector/_tasks_users.py +++ b/services/web/server/src/simcore_service_webserver/garbage_collector/_tasks_users.py @@ -3,30 +3,23 @@ """ -import asyncio import logging -from collections.abc import AsyncIterator, Callable +from collections.abc import AsyncIterator +from datetime import timedelta from aiohttp import web -from common_library.async_tools import cancel_wait_task from models_library.users import UserID +from servicelib.background_task_utils import exclusive_periodic from servicelib.logging_utils import get_log_record_extra, log_context -from tenacity import retry -from tenacity.before_sleep import before_sleep_log -from tenacity.wait import wait_exponential +from simcore_service_webserver.redis import get_redis_lock_manager_client_sdk from ..login import login_service from ..security import security_service from ..users.api import update_expired_users +from ._tasks_utils import CleanupContextFunc, periodic_task_lifespan _logger = logging.getLogger(__name__) -CleanupContextFunc = Callable[[web.Application], AsyncIterator[None]] - - -_PERIODIC_TASK_NAME = f"{__name__}.update_expired_users_periodically" -_APP_TASK_KEY = f"{_PERIODIC_TASK_NAME}.task" - async def notify_user_logout_all_sessions( app: web.Application, user_id: UserID @@ -50,15 +43,7 @@ async def notify_user_logout_all_sessions( ) -@retry( - wait=wait_exponential(min=5, max=20), - before_sleep=before_sleep_log(_logger, logging.WARNING), - # NOTE: this function does suppresses all exceptions and retry indefinitly -) async def _update_expired_users(app: web.Application): - """ - It is resilient, i.e. if update goes wrong, it waits a bit and retries - """ if updated := await update_expired_users(app): # expired users might be cached in the auth. If so, any request @@ -82,32 +67,22 @@ async def _update_expired_users(app: web.Application): _logger.info("No users expired") -async def _update_expired_users_periodically( - app: web.Application, wait_interval_s: float -): - """Periodically checks expiration dates and updates user status""" +def create_background_task_for_trial_accounts(wait_s: float) -> CleanupContextFunc: - while True: - await _update_expired_users(app) - await asyncio.sleep(wait_interval_s) + async def _cleanup_ctx_fun(app: web.Application) -> AsyncIterator[None]: + interval = timedelta(seconds=wait_s) - -def create_background_task_for_trial_accounts( - wait_s: float, task_name: str = _PERIODIC_TASK_NAME -) -> CleanupContextFunc: - async def _cleanup_ctx_fun( - app: web.Application, - ) -> AsyncIterator[None]: - # setup - task = asyncio.create_task( - _update_expired_users_periodically(app, wait_s), - name=task_name, + @exclusive_periodic( + # Function-exclusiveness is required to avoid multiple tasks like thisone running concurrently + get_redis_lock_manager_client_sdk(app), + task_interval=interval, + retry_after=min(timedelta(seconds=10), interval / 10), ) - app[_APP_TASK_KEY] = task - - yield + async def _update_expired_users_periodically() -> None: + with log_context(_logger, logging.INFO, "Updating expired users"): + await _update_expired_users(app) - # tear-down - await cancel_wait_task(task) + async for _ in periodic_task_lifespan(app, _update_expired_users_periodically): + yield return _cleanup_ctx_fun diff --git a/services/web/server/src/simcore_service_webserver/garbage_collector/_tasks_utils.py b/services/web/server/src/simcore_service_webserver/garbage_collector/_tasks_utils.py new file mode 100644 index 00000000000..4971389a73f --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/garbage_collector/_tasks_utils.py @@ -0,0 +1,57 @@ +""" +Common utilities for background task management in garbage collector +""" + +import asyncio +from collections.abc import AsyncIterator, Callable, Coroutine + +from aiohttp import web +from common_library.async_tools import cancel_wait_task + +CleanupContextFunc = Callable[[web.Application], AsyncIterator[None]] + + +def create_task_name(coro: Callable) -> str: + """ + Returns a unique name for the task based on its module and function name. + This is useful for logging and debugging purposes. + """ + return f"{coro.__module__}.{coro.__name__}" + + +async def periodic_task_lifespan( + app: web.Application, + periodic_async_func: Callable[[], Coroutine[None, None, None]], + *, + task_name: str | None = None, +) -> AsyncIterator[None]: + """ + Generic setup and teardown for periodic background tasks. + + Args: + app: The aiohttp web application + periodic_async_func: The periodic coroutine function (already decorated with @exclusive_periodic) + """ + assert getattr(periodic_async_func, "__exclusive_periodic__", False) # nosec + + # setup + task_name = task_name or create_task_name(periodic_async_func) + + task = asyncio.create_task( + periodic_async_func(), + name=task_name, + ) + + # Keeping a reference in app's state to prevent premature garbage collection of the task + app_task_key = f"gc-tasks/{task_name}" + if app_task_key in app: + msg = f"Task {task_name} is already registered in the app state" + raise ValueError(msg) + + app[app_task_key] = task + + yield + + # tear-down + await cancel_wait_task(task) + app.pop(app_task_key, None) diff --git a/services/web/server/src/simcore_service_webserver/garbage_collector/plugin.py b/services/web/server/src/simcore_service_webserver/garbage_collector/plugin.py index 3c42457ece5..aa5ef38fdc1 100644 --- a/services/web/server/src/simcore_service_webserver/garbage_collector/plugin.py +++ b/services/web/server/src/simcore_service_webserver/garbage_collector/plugin.py @@ -8,6 +8,7 @@ from ..login.plugin import setup_login_storage from ..products.plugin import setup_products from ..projects._projects_repository_legacy import setup_projects_db +from ..redis import setup_redis from ..socketio.plugin import setup_socketio from . import _tasks_api_keys, _tasks_core, _tasks_trash, _tasks_users from .settings import get_plugin_settings @@ -25,8 +26,12 @@ def setup_garbage_collector(app: web.Application) -> None: # for trashing setup_products(app) + # distributed exclusive periodic tasks + setup_redis(app) + # - project-api needs access to db setup_projects_db(app) + # - project needs access to socketio via notify_project_state_update setup_socketio(app) # - project needs access to user-api that is connected to login plugin @@ -34,7 +39,7 @@ def setup_garbage_collector(app: web.Application) -> None: settings = get_plugin_settings(app) - app.cleanup_ctx.append(_tasks_core.run_background_task) + app.cleanup_ctx.append(_tasks_core.create_background_task_for_garbage_collection()) set_parent_module_log_level( _logger.name, min(logging.INFO, get_application_settings(app).log_level) diff --git a/services/web/server/src/simcore_service_webserver/garbage_collector/settings.py b/services/web/server/src/simcore_service_webserver/garbage_collector/settings.py index 46863d45864..0f72f1dd2cb 100644 --- a/services/web/server/src/simcore_service_webserver/garbage_collector/settings.py +++ b/services/web/server/src/simcore_service_webserver/garbage_collector/settings.py @@ -1,3 +1,5 @@ +from typing import Annotated + from aiohttp import web from pydantic import Field, PositiveInt from servicelib.aiohttp.application_keys import APP_SETTINGS_KEY @@ -13,20 +15,28 @@ class GarbageCollectorSettings(BaseCustomSettings): - GARBAGE_COLLECTOR_INTERVAL_S: PositiveInt = Field( - 30 * _SEC, - description="Waiting time between consecutive runs of the garbage-colector", + GARBAGE_COLLECTOR_INTERVAL_S: Annotated[ + PositiveInt, + Field( + description="Waiting time between consecutive runs of the garbage-colector" + ), + ] = ( + 30 * _SEC ) - GARBAGE_COLLECTOR_EXPIRED_USERS_CHECK_INTERVAL_S: PositiveInt = Field( - 1 * _HOUR, - description="Time period between checks of expiration dates for trial users", + GARBAGE_COLLECTOR_EXPIRED_USERS_CHECK_INTERVAL_S: Annotated[ + PositiveInt, + Field( + description="Time period between checks of expiration dates for trial users" + ), + ] = ( + 1 * _HOUR ) - GARBAGE_COLLECTOR_PRUNE_APIKEYS_INTERVAL_S: PositiveInt = Field( - _HOUR, - description="Wait time between periodic pruning of expired API keys", - ) + GARBAGE_COLLECTOR_PRUNE_APIKEYS_INTERVAL_S: Annotated[ + PositiveInt, + Field(description="Wait time between periodic pruning of expired API keys"), + ] = _HOUR def get_plugin_settings(app: web.Application) -> GarbageCollectorSettings: diff --git a/services/web/server/tests/integration/01/test_garbage_collection.py b/services/web/server/tests/integration/01/test_garbage_collection.py index 0618647f01c..3b9344aec6b 100644 --- a/services/web/server/tests/integration/01/test_garbage_collection.py +++ b/services/web/server/tests/integration/01/test_garbage_collection.py @@ -27,6 +27,7 @@ from pytest_mock import MockerFixture from pytest_simcore.helpers.webserver_login import UserInfoDict, log_client_in from pytest_simcore.helpers.webserver_projects import create_project, empty_project_data +from servicelib.aiohttp import status from servicelib.aiohttp.application import create_safe_application from settings_library.rabbit import RabbitSettings from settings_library.redis import RedisDatabase, RedisSettings @@ -63,15 +64,16 @@ log = logging.getLogger(__name__) pytest_simcore_core_services_selection = [ - "migration", + "migration", # NOTE: rebuild! "postgres", "rabbit", "redis", - "storage", + "storage", # NOTE: rebuild! ] pytest_simcore_ops_services_selection = [ "minio", "adminer", + "redis-commander", ] @@ -98,11 +100,6 @@ async def _delete_all_redis_keys(redis_settings: RedisSettings): await client.aclose(close_connection_pool=True) -@pytest.fixture(scope="session") -def osparc_product_name() -> str: - return "osparc" - - @pytest.fixture async def director_v2_service_mock( mocker: MockerFixture, @@ -130,7 +127,7 @@ async def director_v2_service_mock( with aioresponses(passthrough=PASSTHROUGH_REQUESTS_PREFIXES) as mock: mock.get( get_computation_pattern, - status=202, + status=status.HTTP_202_ACCEPTED, payload={"state": str(RunningState.NOT_STARTED.value)}, repeat=True, ) @@ -177,7 +174,9 @@ async def client( setup_socketio(app) setup_projects(app) setup_director_v2(app) + assert setup_resource_manager(app) + setup_garbage_collector(app) return await aiohttp_client( @@ -190,16 +189,19 @@ async def client( def disable_garbage_collector_task(mocker: MockerFixture) -> mock.MagicMock: """patch the setup of the garbage collector so we can call it manually""" - async def _fake_background_task(app: web.Application): - # startup - await asyncio.sleep(0.1) - yield - # teardown - await asyncio.sleep(0.1) + def _fake_factory(): + async def _cleanup_ctx_fun(app: web.Application): + # startup + await asyncio.sleep(0.1) + yield + # teardown + await asyncio.sleep(0.1) + + return _cleanup_ctx_fun return mocker.patch( - "simcore_service_webserver.garbage_collector.plugin._tasks_core.run_background_task", - side_effect=_fake_background_task, + "simcore_service_webserver.garbage_collector.plugin._tasks_core.create_background_task_for_garbage_collection", + side_effect=_fake_factory, ) diff --git a/services/web/server/tests/unit/isolated/test_garbage_collector_core.py b/services/web/server/tests/unit/isolated/test_garbage_collector_core.py index b944b0d93c1..a6722b3dafc 100644 --- a/services/web/server/tests/unit/isolated/test_garbage_collector_core.py +++ b/services/web/server/tests/unit/isolated/test_garbage_collector_core.py @@ -124,7 +124,7 @@ async def test_remove_orphaned_services_with_no_running_services_does_nothing( def faker_dynamic_service_get() -> Callable[[], DynamicServiceGet]: def _() -> DynamicServiceGet: return DynamicServiceGet.model_validate( - DynamicServiceGet.model_config["json_schema_extra"]["examples"][1] + DynamicServiceGet.model_json_schema()["examples"][1] ) return _