Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

♻️ reroute get project inactivity via dynamic-scheduler #6949

Merged
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,5 @@ class DynamicServiceCreate(ServiceDetails):

class GetProjectInactivityResponse(BaseModel):
is_inactive: bool

model_config = ConfigDict(json_schema_extra={"example": {"is_inactive": "false"}})
GitHK marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import logging
from typing import Final

from models_library.api_schemas_directorv2.dynamic_services import DynamicServiceGet
from models_library.api_schemas_directorv2.dynamic_services import (
DynamicServiceGet,
GetProjectInactivityResponse,
)
from models_library.api_schemas_dynamic_scheduler import DYNAMIC_SCHEDULER_RPC_NAMESPACE
from models_library.api_schemas_dynamic_scheduler.dynamic_services import (
DynamicServiceStart,
Expand Down Expand Up @@ -93,3 +96,21 @@ async def stop_dynamic_service(
timeout_s=timeout_s,
)
assert result is None # nosec


@log_decorator(_logger, level=logging.DEBUG)
async def get_project_inactivity(
rabbitmq_rpc_client: RabbitMQRPCClient,
*,
project_id: ProjectID,
max_inactivity_seconds: NonNegativeInt,
) -> GetProjectInactivityResponse:
result = await rabbitmq_rpc_client.request(
DYNAMIC_SCHEDULER_RPC_NAMESPACE,
_RPC_METHOD_NAME_ADAPTER.validate_python("get_project_inactivity"),
project_id=project_id,
max_inactivity_seconds=max_inactivity_seconds,
timeout_s=_RPC_DEFAULT_TIMEOUT_S,
)
assert isinstance(result, GetProjectInactivityResponse) # nosec
return result
5 changes: 4 additions & 1 deletion services/director-v2/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2057,7 +2057,10 @@
"required": [
"is_inactive"
],
"title": "GetProjectInactivityResponse"
"title": "GetProjectInactivityResponse",
"example": {
"is_inactive": "false"
}
},
"HTTPValidationError": {
"properties": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from fastapi import FastAPI
from models_library.api_schemas_directorv2.dynamic_services import DynamicServiceGet
from models_library.api_schemas_directorv2.dynamic_services import (
DynamicServiceGet,
GetProjectInactivityResponse,
)
from models_library.api_schemas_dynamic_scheduler.dynamic_services import (
DynamicServiceStart,
DynamicServiceStop,
Expand All @@ -8,6 +11,7 @@
from models_library.projects import ProjectID
from models_library.projects_nodes_io import NodeID
from models_library.users import UserID
from pydantic import NonNegativeInt
from servicelib.rabbitmq import RPCRouter
from servicelib.rabbitmq.rpc_interfaces.dynamic_scheduler.errors import (
ServiceWaitingForManualInterventionError,
Expand Down Expand Up @@ -56,3 +60,12 @@ async def stop_dynamic_service(
return await scheduler_interface.stop_dynamic_service(
app, dynamic_service_stop=dynamic_service_stop
)


@router.expose()
async def get_project_inactivity(
app: FastAPI, *, project_id: ProjectID, max_inactivity_seconds: NonNegativeInt
) -> GetProjectInactivityResponse:
return await scheduler_interface.get_project_inactivity(
app, project_id=project_id, max_inactivity_seconds=max_inactivity_seconds
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@
from typing import Any

from fastapi import FastAPI, status
from models_library.api_schemas_directorv2.dynamic_services import DynamicServiceGet
from models_library.api_schemas_directorv2.dynamic_services import (
DynamicServiceGet,
GetProjectInactivityResponse,
)
from models_library.api_schemas_dynamic_scheduler.dynamic_services import (
DynamicServiceStart,
)
from models_library.api_schemas_webserver.projects_nodes import NodeGet, NodeGetIdle
from models_library.projects import ProjectID
from models_library.projects_nodes_io import NodeID
from models_library.users import UserID
from pydantic import TypeAdapter
from pydantic import NonNegativeInt, TypeAdapter
from servicelib.fastapi.app_state import SingletonInAppStateMixin
from servicelib.fastapi.http_client import AttachLifespanMixin, HasClientSetupInterface
from servicelib.fastapi.http_client_thin import UnexpectedStatusError
Expand Down Expand Up @@ -108,6 +111,16 @@ async def list_tracked_dynamic_services(
)
return TypeAdapter(list[DynamicServiceGet]).validate_python(response.json())

async def get_project_inactivity(
self, *, project_id: ProjectID, max_inactivity_seconds: NonNegativeInt
) -> GetProjectInactivityResponse:
response = await self.thin_client.get_projects_inactivity(
project_id=project_id, max_inactivity_seconds=max_inactivity_seconds
)
return TypeAdapter(GetProjectInactivityResponse).validate_python(
response.json()
)


def setup_director_v2(app: FastAPI) -> None:
public_client = DirectorV2Client(app)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from models_library.projects_nodes_io import NodeID
from models_library.services_resources import ServiceResourcesDictHelpers
from models_library.users import UserID
from pydantic import NonNegativeInt
from servicelib.common_headers import (
X_DYNAMIC_SIDECAR_REQUEST_DNS,
X_DYNAMIC_SIDECAR_REQUEST_SCHEME,
Expand Down Expand Up @@ -124,3 +125,13 @@ async def get_dynamic_services(
"/dynamic_services",
params=as_dict_exclude_unset(user_id=user_id, project_id=project_id),
)

@retry_on_errors()
@expect_status(status.HTTP_200_OK)
async def get_projects_inactivity(
self, *, project_id: ProjectID, max_inactivity_seconds: NonNegativeInt
) -> Response:
return await self.client.get(
f"/dynamic_services/projects/{project_id}/inactivity",
params={"max_inactivity_seconds": max_inactivity_seconds},
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from fastapi import FastAPI
from models_library.api_schemas_directorv2.dynamic_services import DynamicServiceGet
from models_library.api_schemas_directorv2.dynamic_services import (
DynamicServiceGet,
GetProjectInactivityResponse,
)
from models_library.api_schemas_dynamic_scheduler.dynamic_services import (
DynamicServiceStart,
DynamicServiceStop,
Expand All @@ -8,6 +11,7 @@
from models_library.projects import ProjectID
from models_library.projects_nodes_io import NodeID
from models_library.users import UserID
from pydantic import NonNegativeInt

from ..core.settings import ApplicationSettings
from .director_v2 import DirectorV2Client
Expand Down Expand Up @@ -73,3 +77,19 @@ async def stop_dynamic_service(
)

await set_request_as_stopped(app, dynamic_service_stop)


async def get_project_inactivity(
app: FastAPI, *, project_id: ProjectID, max_inactivity_seconds: NonNegativeInt
) -> GetProjectInactivityResponse:
settings: ApplicationSettings = app.state.settings
if settings.DYNAMIC_SCHEDULER_USE_INTERNAL_SCHEDULER:
raise NotImplementedError

director_v2_client = DirectorV2Client.get_from_app_state(app)
response: GetProjectInactivityResponse = (
await director_v2_client.get_project_inactivity(
project_id=project_id, max_inactivity_seconds=max_inactivity_seconds
)
)
return response
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
from faker import Faker
from fastapi import FastAPI, status
from fastapi.encoders import jsonable_encoder
from models_library.api_schemas_directorv2.dynamic_services import DynamicServiceGet
from models_library.api_schemas_directorv2.dynamic_services import (
DynamicServiceGet,
GetProjectInactivityResponse,
)
from models_library.api_schemas_dynamic_scheduler.dynamic_services import (
DynamicServiceStart,
DynamicServiceStop,
Expand Down Expand Up @@ -490,3 +493,37 @@ async def test_stop_dynamic_service_serializes_generic_errors(
),
timeout_s=5,
)


@pytest.fixture
def inactivity_response() -> GetProjectInactivityResponse:
return TypeAdapter(GetProjectInactivityResponse).validate_python(
GetProjectInactivityResponse.model_json_schema()["example"]
)


@pytest.fixture
def mock_director_v2_get_project_inactivity(
project_id: ProjectID, inactivity_response: GetProjectInactivityResponse
) -> Iterator[None]:
with respx.mock(
base_url="http://director-v2:8000/v2",
assert_all_called=False,
assert_all_mocked=True, # IMPORTANT: KEEP always True!
) as mock:
mock.get(f"/dynamic_services/projects/{project_id}/inactivity").respond(
status.HTTP_200_OK, text=inactivity_response.model_dump_json()
)
yield None


async def test_get_project_inactivity(
mock_director_v2_get_project_inactivity: None,
rpc_client: RabbitMQRPCClient,
project_id: ProjectID,
inactivity_response: GetProjectInactivityResponse,
):
result = await services.get_project_inactivity(
rpc_client, project_id=project_id, max_inactivity_seconds=5
)
assert result == inactivity_response
Original file line number Diff line number Diff line change
Expand Up @@ -9952,6 +9952,8 @@ components:
required:
- is_inactive
title: GetProjectInactivityResponse
example:
is_inactive: 'false'
GetWalletAutoRecharge:
properties:
enabled:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from aiohttp import web
from models_library.projects import ProjectID
from models_library.services import ServicePortKey
from pydantic import NonNegativeInt
from servicelib.logging_utils import log_decorator
from yarl import URL

Expand Down Expand Up @@ -94,20 +93,3 @@ async def update_dynamic_service_networks_in_project(
await request_director_v2(
app, "PATCH", backend_url, expected_status=web.HTTPNoContent
)


@log_decorator(logger=_log)
async def get_project_inactivity(
app: web.Application,
project_id: ProjectID,
max_inactivity_seconds: NonNegativeInt,
) -> DataType:
settings: DirectorV2Settings = get_plugin_settings(app)
backend_url = (
URL(settings.base_url) / f"dynamic_services/projects/{project_id}/inactivity"
).update_query(max_inactivity_seconds=max_inactivity_seconds)
result = await request_director_v2(
app, "GET", backend_url, expected_status=web.HTTPOk
)
assert isinstance(result, dict) # nosec
return result
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
stop_pipeline,
)
from ._core_dynamic_services import (
get_project_inactivity,
request_retrieve_dyn_service,
restart_dynamic_service,
retrieve,
Expand All @@ -34,7 +33,6 @@
"DirectorServiceError",
"get_batch_tasks_outputs",
"get_computation_task",
"get_project_inactivity",
"get_project_run_policy",
"is_healthy",
"is_pipeline_running",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
from functools import partial

from aiohttp import web
from models_library.api_schemas_directorv2.dynamic_services import DynamicServiceGet
from models_library.api_schemas_directorv2.dynamic_services import (
DynamicServiceGet,
GetProjectInactivityResponse,
)
from models_library.api_schemas_dynamic_scheduler.dynamic_services import (
DynamicServiceStart,
DynamicServiceStop,
Expand All @@ -19,6 +22,7 @@
from models_library.projects_nodes_io import NodeID
from models_library.rabbitmq_messages import ProgressRabbitMessageProject, ProgressType
from models_library.users import UserID
from pydantic import NonNegativeInt
from pydantic.types import PositiveInt
from servicelib.progress_bar import ProgressBarData
from servicelib.rabbitmq import RabbitMQClient, RPCServerError
Expand Down Expand Up @@ -148,3 +152,16 @@ async def stop_dynamic_services_in_project(
]

await logged_gather(*services_to_stop)


async def get_project_inactivity(
app: web.Application,
*,
project_id: ProjectID,
max_inactivity_seconds: NonNegativeInt,
) -> GetProjectInactivityResponse:
return await services.get_project_inactivity(
get_rabbitmq_rpc_client(app),
project_id=project_id,
max_inactivity_seconds=max_inactivity_seconds,
)
Original file line number Diff line number Diff line change
Expand Up @@ -1870,13 +1870,12 @@ async def get_project_inactivity(
app: web.Application, project_id: ProjectID
) -> GetProjectInactivityResponse:
project_settings: ProjectsSettings = get_plugin_settings(app)
project_inactivity = await director_v2_api.get_project_inactivity(
return await dynamic_scheduler_api.get_project_inactivity(
app,
project_id,
project_id=project_id,
# NOTE: project is considered inactive if all services exposing an /inactivity
# endpoint were inactive since at least PROJECTS_INACTIVITY_INTERVAL
max_inactivity_seconds=int(
project_settings.PROJECTS_INACTIVITY_INTERVAL.total_seconds()
),
)
return GetProjectInactivityResponse.model_validate(project_inactivity)
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
# pylint: disable=unused-argument
# pylint: disable=unused-variable

import re
import uuid as uuidlib
from collections.abc import Awaitable, Callable, Iterator
from http import HTTPStatus
Expand All @@ -16,9 +15,13 @@
from aiohttp.test_utils import TestClient
from aioresponses import aioresponses
from faker import Faker
from models_library.api_schemas_directorv2.dynamic_services import (
GetProjectInactivityResponse,
)
from models_library.products import ProductName
from models_library.projects_state import ProjectState
from pydantic import TypeAdapter
from pytest_mock import MockerFixture
from pytest_simcore.helpers.assert_checks import assert_status
from pytest_simcore.helpers.webserver_login import UserInfoDict
from pytest_simcore.helpers.webserver_parametrizations import (
Expand Down Expand Up @@ -651,18 +654,10 @@ async def test_new_template_from_project(


@pytest.fixture
def mock_director_v2_inactivity(
aioresponses_mocker: aioresponses, is_inactive: bool
) -> None:
aioresponses_mocker.clear()
get_services_pattern = re.compile(
r"^http://[a-z\-_]*director-v2:[0-9]+/v2/dynamic_services/projects/.*/inactivity.*$"
)
aioresponses_mocker.get(
get_services_pattern,
status=status.HTTP_200_OK,
repeat=True,
payload={"is_inactive": is_inactive},
def mock_dynamic_scheduler_inactivity(mocker: MockerFixture, is_inactive: bool) -> None:
mocker.patch(
"simcore_service_webserver.projects.projects_api.dynamic_scheduler_api.get_project_inactivity",
return_value=GetProjectInactivityResponse(is_inactive=is_inactive),
)


Expand All @@ -675,7 +670,7 @@ def mock_director_v2_inactivity(
)
@pytest.mark.parametrize("is_inactive", [True, False])
async def test_get_project_inactivity(
mock_director_v2_inactivity: None,
mock_dynamic_scheduler_inactivity: None,
logged_user: UserInfoDict,
client: TestClient,
faker: Faker,
Expand Down
Loading