Skip to content

Commit 2912fe6

Browse files
GitHKAndrei Neagu
andauthored
🎨 dynamic-sidecar pulls user services images with states and outputs (#6301)
Co-authored-by: Andrei Neagu <[email protected]>
1 parent b1978ad commit 2912fe6

File tree

20 files changed

+434
-91
lines changed

20 files changed

+434
-91
lines changed

services/director-v2/src/simcore_service_director_v2/core/dynamic_services_settings/scheduler.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ class DynamicServicesSchedulerSettings(BaseCustomSettings):
111111
),
112112
)
113113

114+
DYNAMIC_SIDECAR_API_USER_SERVICES_PULLING_TIMEOUT: PositiveFloat = Field(
115+
60.0 * _MINUTE,
116+
description="before starting the user services pull all the images in parallel",
117+
)
118+
114119
DYNAMIC_SIDECAR_API_RESTART_CONTAINERS_TIMEOUT: PositiveFloat = Field(
115120
1.0 * _MINUTE,
116121
description=(

services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/api_client/_public.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,15 @@ async def detach_service_containers_from_project_network(
280280
]
281281
)
282282

283+
async def submit_docker_compose_spec(
284+
self,
285+
dynamic_sidecar_endpoint: AnyHttpUrl,
286+
compose_spec: str,
287+
) -> None:
288+
await self._thin_client.post_containers_compose_spec(
289+
dynamic_sidecar_endpoint, compose_spec=compose_spec
290+
)
291+
283292
def _get_client(self, dynamic_sidecar_endpoint: AnyHttpUrl) -> Client:
284293
return Client(
285294
app=self._app,
@@ -307,13 +316,11 @@ async def _await_for_result(
307316
async def create_containers(
308317
self,
309318
dynamic_sidecar_endpoint: AnyHttpUrl,
310-
compose_spec: str,
311319
metrics_params: CreateServiceMetricsAdditionalParams,
312320
progress_callback: ProgressCallback | None = None,
313321
) -> None:
314322
response = await self._thin_client.post_containers_tasks(
315323
dynamic_sidecar_endpoint,
316-
compose_spec=compose_spec,
317324
metrics_params=metrics_params,
318325
)
319326
task_id: TaskId = response.json()
@@ -355,6 +362,21 @@ async def restore_service_state(self, dynamic_sidecar_endpoint: AnyHttpUrl) -> N
355362
_debug_progress_callback,
356363
)
357364

365+
async def pull_user_services_images(
366+
self, dynamic_sidecar_endpoint: AnyHttpUrl
367+
) -> None:
368+
response = await self._thin_client.post_containers_images_pull(
369+
dynamic_sidecar_endpoint
370+
)
371+
task_id: TaskId = response.json()
372+
373+
await self._await_for_result(
374+
task_id,
375+
dynamic_sidecar_endpoint,
376+
self._dynamic_services_scheduler_settings.DYNAMIC_SIDECAR_API_USER_SERVICES_PULLING_TIMEOUT,
377+
_debug_progress_callback,
378+
)
379+
358380
async def save_service_state(
359381
self,
360382
dynamic_sidecar_endpoint: AnyHttpUrl,

services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/api_client/_thin.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
)
1919

2020

21-
class ThinSidecarsClient(BaseThinClient):
21+
class ThinSidecarsClient(BaseThinClient): # pylint: disable=too-many-public-methods
2222
"""
2323
NOTE: all calls can raise the following errors.
2424
- `UnexpectedStatusError`
@@ -168,21 +168,26 @@ async def post_containers_networks_detach(
168168

169169
@retry_on_errors()
170170
@expect_status(status.HTTP_202_ACCEPTED)
171-
async def post_containers_tasks(
171+
async def post_containers_compose_spec(
172172
self,
173173
dynamic_sidecar_endpoint: AnyHttpUrl,
174174
*,
175175
compose_spec: str,
176+
) -> Response:
177+
url = self._get_url(dynamic_sidecar_endpoint, "/containers/compose-spec")
178+
return await self.client.post(url, json={"docker_compose_yaml": compose_spec})
179+
180+
@retry_on_errors()
181+
@expect_status(status.HTTP_202_ACCEPTED)
182+
async def post_containers_tasks(
183+
self,
184+
dynamic_sidecar_endpoint: AnyHttpUrl,
185+
*,
176186
metrics_params: CreateServiceMetricsAdditionalParams,
177187
) -> Response:
178188
url = self._get_url(dynamic_sidecar_endpoint, "/containers")
179-
# change introduce in OAS version==1.1.0
180189
return await self.client.post(
181-
url,
182-
json={
183-
"docker_compose_yaml": compose_spec,
184-
"metrics_params": metrics_params.dict(),
185-
},
190+
url, json={"metrics_params": metrics_params.dict()}
186191
)
187192

188193
@retry_on_errors()
@@ -209,6 +214,14 @@ async def post_containers_tasks_state_save(
209214
url = self._get_url(dynamic_sidecar_endpoint, "/containers/state:save")
210215
return await self.client.post(url)
211216

217+
@retry_on_errors()
218+
@expect_status(status.HTTP_202_ACCEPTED)
219+
async def post_containers_images_pull(
220+
self, dynamic_sidecar_endpoint: AnyHttpUrl
221+
) -> Response:
222+
url = self._get_url(dynamic_sidecar_endpoint, "/containers/images:pull")
223+
return await self.client.post(url)
224+
212225
@retry_on_errors()
213226
@expect_status(status.HTTP_202_ACCEPTED)
214227
async def post_containers_tasks_ports_inputs_pull(

services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_events.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from ...errors import UnexpectedContainerStatusError
2020
from ._abc import DynamicSchedulerEvent
2121
from ._event_create_sidecars import CreateSidecars
22-
from ._events_user_services import create_user_services
22+
from ._events_user_services import create_user_services, submit_compose_sepc
2323
from ._events_utils import (
2424
are_all_user_services_containers_running,
2525
attach_project_networks,
@@ -139,11 +139,33 @@ async def action(cls, app: FastAPI, scheduler_data: SchedulerData) -> None:
139139
)
140140

141141

142-
class PrepareServicesEnvironment(DynamicSchedulerEvent):
142+
class SendUserServicesSpec(DynamicSchedulerEvent):
143143
"""
144144
Triggered when the dynamic-sidecar is responding to http requests.
145145
This step runs before CreateUserServices.
146146
147+
Sends over the configuration that is used for all docker compose commands.
148+
"""
149+
150+
@classmethod
151+
async def will_trigger(cls, app: FastAPI, scheduler_data: SchedulerData) -> bool:
152+
assert app # nose
153+
return (
154+
scheduler_data.dynamic_sidecar.status.current == DynamicSidecarStatus.OK
155+
and scheduler_data.dynamic_sidecar.is_ready
156+
and not scheduler_data.dynamic_sidecar.was_compose_spec_submitted
157+
)
158+
159+
@classmethod
160+
async def action(cls, app: FastAPI, scheduler_data: SchedulerData) -> None:
161+
await submit_compose_sepc(app, scheduler_data)
162+
163+
164+
class PrepareServicesEnvironment(DynamicSchedulerEvent):
165+
"""
166+
Triggered when the dynamic-sidecar has it's docker-copose spec loaded.
167+
This step runs before SendUserServicesSpec.
168+
147169
Sets up the environment on the host required by the service.
148170
- restores service state
149171
"""
@@ -174,7 +196,8 @@ async def will_trigger(cls, app: FastAPI, scheduler_data: SchedulerData) -> bool
174196
assert app # nose
175197
return (
176198
scheduler_data.dynamic_sidecar.is_service_environment_ready
177-
and not scheduler_data.dynamic_sidecar.compose_spec_submitted
199+
and not scheduler_data.dynamic_sidecar.were_containers_created
200+
and scheduler_data.dynamic_sidecar.compose_spec_submitted
178201
)
179202

180203
@classmethod
@@ -236,6 +259,7 @@ async def action(cls, app: FastAPI, scheduler_data: SchedulerData) -> None:
236259
WaitForSidecarAPI,
237260
UpdateHealth,
238261
GetStatus,
262+
SendUserServicesSpec,
239263
PrepareServicesEnvironment,
240264
CreateUserServices,
241265
AttachProjectsNetworks,

services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_events_user_services.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,7 @@
3232
_logger = logging.getLogger(__name__)
3333

3434

35-
async def create_user_services( # pylint: disable=too-many-statements
36-
app: FastAPI, scheduler_data: SchedulerData
37-
) -> None:
35+
async def submit_compose_sepc(app: FastAPI, scheduler_data: SchedulerData) -> None:
3836
_logger.debug(
3937
"Getting docker compose spec for service %s", scheduler_data.service_name
4038
)
@@ -105,10 +103,26 @@ async def create_user_services( # pylint: disable=too-many-statements
105103
)
106104

107105
_logger.debug(
108-
"Starting containers %s with compose-specs:\n%s",
106+
"Submitting to %s it's compose-specs:\n%s",
109107
scheduler_data.service_name,
110108
compose_spec,
111109
)
110+
await sidecars_client.submit_docker_compose_spec(
111+
dynamic_sidecar_endpoint, compose_spec=compose_spec
112+
)
113+
scheduler_data.dynamic_sidecar.was_compose_spec_submitted = True
114+
115+
116+
async def create_user_services( # pylint: disable=too-many-statements
117+
app: FastAPI, scheduler_data: SchedulerData
118+
) -> None:
119+
dynamic_services_scheduler_settings: DynamicServicesSchedulerSettings = (
120+
app.state.settings.DYNAMIC_SERVICES.DYNAMIC_SCHEDULER
121+
)
122+
sidecars_client = await get_sidecars_client(app, scheduler_data.node_uuid)
123+
dynamic_sidecar_endpoint = scheduler_data.endpoint
124+
125+
_logger.debug("Starting containers %s", scheduler_data.service_name)
112126

113127
async def progress_create_containers(
114128
message: str, percent: ProgressPercent | None, task_id: TaskId
@@ -159,7 +173,6 @@ async def progress_create_containers(
159173
)
160174
await sidecars_client.create_containers(
161175
dynamic_sidecar_endpoint,
162-
compose_spec,
163176
metrics_params,
164177
progress_create_containers,
165178
)
@@ -209,6 +222,4 @@ async def progress_create_containers(
209222

210223
scheduler_data.dynamic_sidecar.were_containers_created = True
211224

212-
scheduler_data.dynamic_sidecar.was_compose_spec_submitted = True
213-
214225
_logger.info("Internal state after creating user services %s", scheduler_data)

services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/scheduler/_core/_events_utils.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from servicelib.fastapi.long_running_tasks.server import TaskProgress
2828
from servicelib.logging_utils import log_context
2929
from servicelib.rabbitmq import RabbitMQClient
30-
from servicelib.utils import logged_gather
30+
from servicelib.utils import limited_gather, logged_gather
3131
from simcore_postgres_database.models.comp_tasks import NodeClass
3232
from tenacity import RetryError, TryAgain
3333
from tenacity.asyncio import AsyncRetrying
@@ -460,13 +460,16 @@ async def prepare_services_environment(
460460
)
461461
)
462462

463-
tasks = [sidecars_client.pull_service_output_ports(dynamic_sidecar_endpoint)]
463+
tasks = [
464+
sidecars_client.pull_user_services_images(dynamic_sidecar_endpoint),
465+
sidecars_client.pull_service_output_ports(dynamic_sidecar_endpoint),
466+
]
464467
# When enabled no longer downloads state via nodeports
465468
# S3 is used to store state paths
466469
if not app_settings.DIRECTOR_V2_DEV_FEATURE_R_CLONE_MOUNTS_ENABLED:
467470
tasks.append(sidecars_client.restore_service_state(dynamic_sidecar_endpoint))
468471

469-
await logged_gather(*tasks, max_concurrency=2)
472+
await limited_gather(*tasks, limit=3)
470473

471474
# inside this directory create the missing dirs, fetch those form the labels
472475
director_v0_client: DirectorV0Client = get_director_v0_client(app)

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

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ async def test_is_healthy_api_error(
166166
"get_health",
167167
side_effect=side_effect,
168168
) as client:
169-
assert await client.is_healthy(dynamic_sidecar_endpoint) == False
169+
assert await client.is_healthy(dynamic_sidecar_endpoint) is False
170170

171171

172172
async def test_containers_inspect(
@@ -282,11 +282,10 @@ async def test_get_entrypoint_container_name_api_not_found(
282282
),
283283
expecting=status.HTTP_204_NO_CONTENT,
284284
),
285-
) as client:
286-
with pytest.raises(EntrypointContainerNotFoundError):
287-
await client.get_entrypoint_container_name(
288-
dynamic_sidecar_endpoint, dynamic_sidecar_network_name
289-
)
285+
) as client, pytest.raises(EntrypointContainerNotFoundError):
286+
await client.get_entrypoint_container_name(
287+
dynamic_sidecar_endpoint, dynamic_sidecar_network_name
288+
)
290289

291290

292291
@pytest.mark.parametrize("network_aliases", [[], ["an-alias"], ["alias-1", "alias-2"]])
@@ -301,7 +300,7 @@ async def test_attach_container_to_network(
301300
) as client:
302301
assert (
303302
# pylint:disable=protected-access
304-
await client._attach_container_to_network(
303+
await client._attach_container_to_network( # noqa: SLF001
305304
dynamic_sidecar_endpoint,
306305
container_id="container_id",
307306
network_id="network_id",
@@ -321,7 +320,7 @@ async def test_detach_container_from_network(
321320
) as client:
322321
assert (
323322
# pylint:disable=protected-access
324-
await client._detach_container_from_network(
323+
await client._detach_container_from_network( # noqa: SLF001
325324
dynamic_sidecar_endpoint,
326325
container_id="container_id",
327326
network_id="network_id",

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

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
# pylint:disable=redefined-outer-name
33

44
import json
5-
from collections.abc import Callable
6-
from typing import Any, AsyncIterable
5+
from collections.abc import AsyncIterable, Callable
6+
from typing import Any
77

88
import pytest
99
from faker import Faker
@@ -282,11 +282,10 @@ async def test_put_volumes(
282282
"post_containers_tasks",
283283
"/containers",
284284
{
285-
"compose_spec": "some_fake_compose_as_str",
286285
"metrics_params": parse_obj_as(
287286
CreateServiceMetricsAdditionalParams,
288287
CreateServiceMetricsAdditionalParams.Config.schema_extra["example"],
289-
),
288+
)
290289
},
291290
id="post_containers_tasks",
292291
),
@@ -296,6 +295,12 @@ async def test_put_volumes(
296295
{},
297296
id="down",
298297
),
298+
pytest.param(
299+
"post_containers_images_pull",
300+
"/containers/images:pull",
301+
{},
302+
id="user_servces_images_pull",
303+
),
299304
pytest.param(
300305
"post_containers_tasks_state_restore",
301306
"/containers/state:restore",
@@ -387,3 +392,22 @@ async def test_post_disk_reserved_free(
387392

388393
response = await thin_client.post_disk_reserved_free(dynamic_sidecar_endpoint)
389394
assert_responses(mock_response, response)
395+
396+
397+
async def test_post_containers_compose_spec(
398+
thin_client: ThinSidecarsClient,
399+
dynamic_sidecar_endpoint: AnyHttpUrl,
400+
mock_request: MockRequestType,
401+
):
402+
mock_response = Response(status.HTTP_202_ACCEPTED)
403+
mock_request(
404+
"POST",
405+
f"{dynamic_sidecar_endpoint}/{thin_client.API_VERSION}/containers/compose-spec",
406+
mock_response,
407+
None,
408+
)
409+
410+
response = await thin_client.post_containers_compose_spec(
411+
dynamic_sidecar_endpoint, compose_spec="some_fake_compose_as_str"
412+
)
413+
assert_responses(mock_response, response)

services/dynamic-sidecar/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.1.1
1+
1.2.0

0 commit comments

Comments
 (0)