Skip to content

Commit 636bbe9

Browse files
GitHKAndrei Neagupcrespov
authored
✨adds new cli options to director-v2 (ITISFoundation#3269)
Co-authored-by: Andrei Neagu <[email protected]> Co-authored-by: Pedro Crespo-Valero <[email protected]>
1 parent f4d3395 commit 636bbe9

File tree

8 files changed

+335
-10
lines changed

8 files changed

+335
-10
lines changed

packages/service-library/src/servicelib/fastapi/long_running_tasks/_client.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import functools
33
import logging
44
import warnings
5-
from typing import Any, Awaitable, Callable, Optional
5+
from typing import Any, Awaitable, Callable, Final, Optional
66

77
from fastapi import FastAPI, status
88
from httpx import AsyncClient, HTTPError
@@ -21,6 +21,8 @@
2121
TaskStatus,
2222
)
2323

24+
DEFAULT_HTTP_REQUESTS_TIMEOUT: Final[PositiveFloat] = 15
25+
2426
logger = logging.getLogger(__name__)
2527

2628

@@ -212,7 +214,7 @@ def setup(
212214
app: FastAPI,
213215
*,
214216
router_prefix: str = "",
215-
http_requests_timeout: PositiveFloat = 15,
217+
http_requests_timeout: PositiveFloat = DEFAULT_HTTP_REQUESTS_TIMEOUT,
216218
):
217219
"""
218220
- `router_prefix` by default it is assumed the server mounts the APIs on

packages/service-library/src/servicelib/fastapi/long_running_tasks/client.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,19 @@
44

55
from ...long_running_tasks._errors import TaskClientResultError
66
from ...long_running_tasks._models import (
7+
ClientConfiguration,
78
ProgressCallback,
89
ProgressMessage,
910
ProgressPercent,
1011
)
1112
from ...long_running_tasks._task import TaskId, TaskResult
12-
from ._client import Client, setup
13+
from ._client import DEFAULT_HTTP_REQUESTS_TIMEOUT, Client, setup
1314
from ._context_manager import periodic_task_result
1415

1516
__all__: tuple[str, ...] = (
1617
"Client",
18+
"ClientConfiguration",
19+
"DEFAULT_HTTP_REQUESTS_TIMEOUT",
1720
"periodic_task_result",
1821
"ProgressCallback",
1922
"ProgressMessage",

services/director-v2/setup.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ def read_reqs(reqs_path: Path) -> Set[str]:
5757
test_suite="tests",
5858
tests_require=TEST_REQUIREMENTS,
5959
extras_require={"test": TEST_REQUIREMENTS},
60+
entry_points={
61+
"console_scripts": [
62+
"simcore-service-director-v2=simcore_service_director_v2.cli:main",
63+
],
64+
},
6065
)
6166

6267

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import asyncio
2+
import logging
3+
import sys
4+
from contextlib import asynccontextmanager
5+
from typing import AsyncIterator, Final
6+
from uuid import UUID
7+
8+
import typer
9+
from fastapi import FastAPI
10+
from models_library.projects import NodeIDStr, ProjectID
11+
from models_library.projects_nodes_io import NodeID
12+
from pydantic import AnyHttpUrl, parse_obj_as
13+
from settings_library.utils_cli import create_settings_command
14+
from tenacity._asyncio import AsyncRetrying
15+
from tenacity.stop import stop_after_attempt
16+
from tenacity.wait import wait_random_exponential
17+
18+
from .core.application import create_base_app
19+
from .core.settings import AppSettings
20+
from .meta import PROJECT_NAME
21+
from .models.schemas.dynamic_services import DynamicSidecarNames
22+
from .modules import db, director_v0, dynamic_sidecar
23+
from .modules.db.repositories.projects import ProjectsRepository
24+
from .modules.director_v0 import DirectorV0Client
25+
from .modules.dynamic_sidecar import api_client
26+
from .modules.dynamic_sidecar.scheduler.events_utils import (
27+
fetch_repo_outside_of_request,
28+
)
29+
from .modules.projects_networks import requires_dynamic_sidecar
30+
31+
DEFAULT_NODE_SAVE_RETRY: Final[int] = 3
32+
33+
main = typer.Typer(name=PROJECT_NAME)
34+
35+
log = logging.getLogger(__name__)
36+
main.command()(create_settings_command(settings_cls=AppSettings, logger=log))
37+
38+
39+
@asynccontextmanager
40+
async def _initialized_app() -> AsyncIterator[FastAPI]:
41+
app = create_base_app()
42+
settings: AppSettings = app.state.settings
43+
44+
# Initialize minimal required components for the application
45+
db.setup(app, settings.POSTGRES)
46+
dynamic_sidecar.setup(app)
47+
director_v0.setup(app, settings.DIRECTOR_V0)
48+
49+
await app.router.startup()
50+
yield app
51+
await app.router.shutdown()
52+
53+
54+
def _get_dynamic_sidecar_endpoint(
55+
settings: AppSettings, node_id: NodeIDStr
56+
) -> AnyHttpUrl:
57+
dynamic_sidecar_names = DynamicSidecarNames.make(UUID(node_id))
58+
hostname = dynamic_sidecar_names.service_name_dynamic_sidecar
59+
port = settings.DYNAMIC_SERVICES.DYNAMIC_SIDECAR.DYNAMIC_SIDECAR_PORT
60+
return parse_obj_as(AnyHttpUrl, f"http://{hostname}:{port}") # NOSONAR
61+
62+
63+
async def _save_node_state(
64+
app,
65+
dynamic_sidecar_client: api_client.DynamicSidecarClient,
66+
retry_save: int,
67+
node_uuid: NodeIDStr,
68+
label: str = "",
69+
) -> None:
70+
typer.echo(f"Saving state for {node_uuid} {label}")
71+
async for attempt in AsyncRetrying(
72+
wait=wait_random_exponential(),
73+
stop=stop_after_attempt(retry_save),
74+
reraise=True,
75+
):
76+
with attempt:
77+
typer.echo(f"Attempting to save {node_uuid} {label}")
78+
await dynamic_sidecar_client.save_service_state(
79+
_get_dynamic_sidecar_endpoint(app.state.settings, node_uuid)
80+
)
81+
82+
83+
async def _async_project_save_state(project_id: ProjectID, retry_save: int) -> None:
84+
async with _initialized_app() as app:
85+
projects_repository: ProjectsRepository = fetch_repo_outside_of_request(
86+
app, ProjectsRepository
87+
)
88+
project_at_db = await projects_repository.get_project(project_id)
89+
90+
typer.echo(f"Saving project '{project_at_db.uuid}' - '{project_at_db.name}'")
91+
92+
dynamic_sidecar_client = api_client.get_dynamic_sidecar_client(app)
93+
nodes_failed_to_save: list[NodeIDStr] = []
94+
for node_uuid, node_content in project_at_db.workbench.items():
95+
# onl dynamic-sidecars are used
96+
if not await requires_dynamic_sidecar(
97+
service_key=node_content.key,
98+
service_version=node_content.version,
99+
director_v0_client=DirectorV0Client.instance(app),
100+
):
101+
continue
102+
103+
try:
104+
await _save_node_state(
105+
app,
106+
dynamic_sidecar_client,
107+
retry_save,
108+
node_uuid,
109+
node_content.label,
110+
)
111+
except Exception: # pylint: disable=broad-except
112+
nodes_failed_to_save.append(node_uuid)
113+
114+
if nodes_failed_to_save:
115+
typer.echo(
116+
"The following nodes failed to save:"
117+
+ "\n- "
118+
+ "\n- ".join(nodes_failed_to_save)
119+
+ "\nPlease try to save them individually!"
120+
)
121+
sys.exit(1)
122+
123+
typer.echo(f"Save complete for project {project_id}")
124+
125+
126+
@main.command()
127+
def project_save_state(
128+
project_id: ProjectID, retry_save: int = DEFAULT_NODE_SAVE_RETRY
129+
):
130+
"""
131+
Saves the state of all dy-sidecars in a project.
132+
In case of error while saving the state of an individual node,
133+
it will retry to save.
134+
If errors persist it will produce a list of nodes which failed to save.
135+
"""
136+
asyncio.run(_async_project_save_state(project_id, retry_save))
137+
138+
139+
async def _async_node_save_state(node_id: NodeID, retry_save: int) -> None:
140+
async with _initialized_app() as app:
141+
dynamic_sidecar_client = api_client.get_dynamic_sidecar_client(app)
142+
await _save_node_state(
143+
app, dynamic_sidecar_client, retry_save, NodeIDStr(f"{node_id}")
144+
)
145+
146+
typer.echo(f"Node {node_id} save completed")
147+
148+
149+
@main.command()
150+
def node_save_state(node_id: NodeID, retry_save: int = DEFAULT_NODE_SAVE_RETRY):
151+
"""
152+
Saves the state of an individual node in the project.
153+
"""
154+
asyncio.run(_async_node_save_state(node_id, retry_save))

services/director-v2/src/simcore_service_director_v2/core/application.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,11 @@ def _set_exception_handlers(app: FastAPI):
8686
)
8787

8888

89-
def init_app(settings: Optional[AppSettings] = None) -> FastAPI:
89+
def create_base_app(settings: Optional[AppSettings] = None) -> FastAPI:
9090
if settings is None:
9191
settings = AppSettings.create_from_envs()
9292
assert settings # nosec
93+
9394
logging.basicConfig(level=settings.LOG_LEVEL.value)
9495
logging.root.setLevel(settings.LOG_LEVEL.value)
9596
logger.debug(settings.json(indent=2))
@@ -103,9 +104,18 @@ def init_app(settings: Optional[AppSettings] = None) -> FastAPI:
103104
**get_common_oas_options(settings.SC_BOOT_MODE.is_devel_mode()),
104105
)
105106
override_fastapi_openapi_method(app)
106-
107107
app.state.settings = settings
108108

109+
app.include_router(api_router)
110+
return app
111+
112+
113+
def init_app(settings: Optional[AppSettings] = None) -> FastAPI:
114+
app = create_base_app(settings)
115+
if settings is None:
116+
settings = app.state.settings
117+
assert settings # nosec
118+
109119
if settings.SC_BOOT_MODE == BootModeEnum.DEBUG:
110120
remote_debug.setup(app)
111121

@@ -149,8 +159,6 @@ def init_app(settings: Optional[AppSettings] = None) -> FastAPI:
149159
app.add_event_handler("shutdown", on_shutdown)
150160
_set_exception_handlers(app)
151161

152-
app.include_router(api_router)
153-
154162
config_all_loggers()
155163

156164
return app

services/director-v2/src/simcore_service_director_v2/modules/projects_networks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def _network_name(project_id: ProjectID, user_defined: str) -> DockerNetworkName
4848
return parse_obj_as(DockerNetworkName, network_name)
4949

5050

51-
async def _requires_dynamic_sidecar(
51+
async def requires_dynamic_sidecar(
5252
service_key: str,
5353
service_version: str,
5454
director_v0_client: DirectorV0Client,
@@ -189,7 +189,7 @@ async def _get_networks_with_aliases_for_default_network(
189189
for node_uuid, node_content in new_workbench.items():
190190

191191
# only add dynamic-sidecar nodes
192-
if not await _requires_dynamic_sidecar(
192+
if not await requires_dynamic_sidecar(
193193
service_key=node_content.key,
194194
service_version=node_content.version,
195195
director_v0_client=director_v0_client,

services/director-v2/tests/unit/test_modules_project_networks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ def mock_docker_calls(mocker: MockerFixture) -> Iterable[Dict[str, AsyncMock]]:
211211
"attach": mocker.patch(f"{class_base}.attach_project_network", AsyncMock()),
212212
"detach": mocker.patch(f"{class_base}.detach_project_network", AsyncMock()),
213213
"requires_dynamic_sidecar": mocker.patch(
214-
"simcore_service_director_v2.modules.projects_networks._requires_dynamic_sidecar",
214+
"simcore_service_director_v2.modules.projects_networks.requires_dynamic_sidecar",
215215
requires_dynamic_sidecar_mock,
216216
),
217217
}

0 commit comments

Comments
 (0)