Skip to content

Commit 04b118c

Browse files
GitHKAndrei Neagu
authored andcommitted
🎨 publish port events to frontend (ITISFoundation#6396)
Co-authored-by: Andrei Neagu <[email protected]>
1 parent 8129783 commit 04b118c

File tree

26 files changed

+866
-259
lines changed

26 files changed

+866
-259
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from enum import auto
2+
3+
from models_library.projects import ProjectID
4+
from models_library.projects_nodes_io import NodeID
5+
from models_library.services_types import ServicePortKey
6+
from models_library.utils.enums import StrAutoEnum
7+
from pydantic import BaseModel
8+
9+
10+
class OutputStatus(StrAutoEnum):
11+
UPLOAD_STARTED = auto()
12+
UPLOAD_WAS_ABORTED = auto()
13+
UPLOAD_FINISHED_SUCCESSFULLY = auto()
14+
UPLOAD_FINISHED_WITH_ERRROR = auto()
15+
16+
17+
class InputStatus(StrAutoEnum):
18+
DOWNLOAD_STARTED = auto()
19+
DOWNLOAD_WAS_ABORTED = auto()
20+
DOWNLOAD_FINISHED_SUCCESSFULLY = auto()
21+
DOWNLOAD_FINISHED_WITH_ERRROR = auto()
22+
23+
24+
class _PortStatusCommon(BaseModel):
25+
project_id: ProjectID
26+
node_id: NodeID
27+
port_key: ServicePortKey
28+
29+
30+
class OutputPortStatus(_PortStatusCommon):
31+
status: OutputStatus
32+
33+
34+
class InputPortSatus(_PortStatusCommon):
35+
status: InputStatus
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
from typing import Final
22

33
SOCKET_IO_SERVICE_DISK_USAGE_EVENT: Final[str] = "serviceDiskUsage"
4+
SOCKET_IO_STATE_OUTPUT_PORTS_EVENT: Final[str] = "stateOutputPorts"
5+
SOCKET_IO_STATE_INPUT_PORTS_EVENT: Final[str] = "stateInputPorts"

packages/simcore-sdk/src/simcore_sdk/node_ports_v2/nodeports_v2.py

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import logging
2+
from abc import ABC, abstractmethod
3+
from asyncio import CancelledError
24
from collections.abc import Callable, Coroutine
35
from pathlib import Path
46
from typing import Any
@@ -27,6 +29,20 @@
2729
log = logging.getLogger(__name__)
2830

2931

32+
class OutputsCallbacks(ABC):
33+
@abstractmethod
34+
async def aborted(self, key: ServicePortKey) -> None:
35+
pass
36+
37+
@abstractmethod
38+
async def finished_succesfully(self, key: ServicePortKey) -> None:
39+
pass
40+
41+
@abstractmethod
42+
async def finished_with_error(self, key: ServicePortKey) -> None:
43+
pass
44+
45+
3046
class Nodeports(BaseModel):
3147
"""
3248
Represents a node in a project and all its input/output ports
@@ -148,6 +164,7 @@ async def set_multiple(
148164
],
149165
*,
150166
progress_bar: ProgressBarData,
167+
outputs_callbacks: OutputsCallbacks | None,
151168
) -> None:
152169
"""
153170
Sets the provided values to the respective input or output ports
@@ -156,26 +173,44 @@ async def set_multiple(
156173
157174
raises ValidationError
158175
"""
176+
177+
async def _set_with_notifications(
178+
port_key: ServicePortKey,
179+
value: ItemConcreteValue | None,
180+
set_kwargs: SetKWargs | None,
181+
sub_progress: ProgressBarData,
182+
) -> None:
183+
try:
184+
# pylint: disable=protected-access
185+
await self.internal_outputs[port_key]._set( # noqa: SLF001
186+
value, set_kwargs=set_kwargs, progress_bar=sub_progress
187+
)
188+
if outputs_callbacks:
189+
await outputs_callbacks.finished_succesfully(port_key)
190+
except UnboundPortError:
191+
# not available try inputs
192+
# if this fails it will raise another exception
193+
# pylint: disable=protected-access
194+
await self.internal_inputs[port_key]._set( # noqa: SLF001
195+
value, set_kwargs=set_kwargs, progress_bar=sub_progress
196+
)
197+
except CancelledError:
198+
if outputs_callbacks:
199+
await outputs_callbacks.aborted(port_key)
200+
raise
201+
except Exception:
202+
if outputs_callbacks:
203+
await outputs_callbacks.finished_with_error(port_key)
204+
raise
205+
159206
tasks = []
160207
async with progress_bar.sub_progress(
161208
steps=len(port_values.items()), description=IDStr("set multiple")
162209
) as sub_progress:
163210
for port_key, (value, set_kwargs) in port_values.items():
164-
# pylint: disable=protected-access
165-
try:
166-
tasks.append(
167-
self.internal_outputs[port_key]._set(
168-
value, set_kwargs=set_kwargs, progress_bar=sub_progress
169-
)
170-
)
171-
except UnboundPortError:
172-
# not available try inputs
173-
# if this fails it will raise another exception
174-
tasks.append(
175-
self.internal_inputs[port_key]._set(
176-
value, set_kwargs=set_kwargs, progress_bar=sub_progress
177-
)
178-
)
211+
tasks.append(
212+
_set_with_notifications(port_key, value, set_kwargs, sub_progress)
213+
)
179214

180215
results = await logged_gather(*tasks)
181216
await self.save_to_db_cb(self)

packages/simcore-sdk/tests/integration/test_node_ports_v2_nodeports2.py

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from collections.abc import Awaitable, Callable, Iterable
1414
from pathlib import Path
1515
from typing import Any
16+
from unittest.mock import AsyncMock
1617
from uuid import uuid4
1718

1819
import np_helpers
@@ -28,13 +29,14 @@
2829
SimcoreS3FileID,
2930
)
3031
from models_library.services_types import ServicePortKey
32+
from pytest_mock import MockerFixture
3133
from servicelib.progress_bar import ProgressBarData
3234
from settings_library.r_clone import RCloneSettings
3335
from simcore_sdk import node_ports_v2
3436
from simcore_sdk.node_ports_common.exceptions import UnboundPortError
3537
from simcore_sdk.node_ports_v2 import exceptions
3638
from simcore_sdk.node_ports_v2.links import ItemConcreteValue, PortLink
37-
from simcore_sdk.node_ports_v2.nodeports_v2 import Nodeports
39+
from simcore_sdk.node_ports_v2.nodeports_v2 import Nodeports, OutputsCallbacks
3840
from simcore_sdk.node_ports_v2.port import Port
3941
from utils_port_v2 import CONSTANT_UUID
4042

@@ -749,6 +751,34 @@ async def _upload_create_task(item_key: str) -> None:
749751
)
750752

751753

754+
class _Callbacks(OutputsCallbacks):
755+
async def aborted(self, key: ServicePortKey) -> None:
756+
pass
757+
758+
async def finished_succesfully(self, key: ServicePortKey) -> None:
759+
pass
760+
761+
async def finished_with_error(self, key: ServicePortKey) -> None:
762+
pass
763+
764+
765+
@pytest.fixture
766+
async def output_callbacks() -> _Callbacks:
767+
return _Callbacks()
768+
769+
770+
@pytest.fixture
771+
async def spy_outputs_callbaks(
772+
mocker: MockerFixture, output_callbacks: _Callbacks
773+
) -> dict[str, AsyncMock]:
774+
return {
775+
"aborted": mocker.spy(output_callbacks, "aborted"),
776+
"finished_succesfully": mocker.spy(output_callbacks, "finished_succesfully"),
777+
"finished_with_error": mocker.spy(output_callbacks, "finished_with_error"),
778+
}
779+
780+
781+
@pytest.mark.parametrize("use_output_callbacks", [True, False])
752782
async def test_batch_update_inputs_outputs(
753783
user_id: int,
754784
project_id: str,
@@ -757,7 +787,12 @@ async def test_batch_update_inputs_outputs(
757787
port_count: int,
758788
option_r_clone_settings: RCloneSettings | None,
759789
faker: Faker,
790+
output_callbacks: _Callbacks,
791+
spy_outputs_callbaks: dict[str, AsyncMock],
792+
use_output_callbacks: bool,
760793
) -> None:
794+
callbacks = output_callbacks if use_output_callbacks else None
795+
761796
outputs = [(f"value_out_{i}", "integer", None) for i in range(port_count)]
762797
inputs = [(f"value_in_{i}", "integer", None) for i in range(port_count)]
763798
config_dict, _, _ = create_special_configuration(inputs=inputs, outputs=outputs)
@@ -771,12 +806,14 @@ async def test_batch_update_inputs_outputs(
771806
await check_config_valid(PORTS, config_dict)
772807

773808
async with ProgressBarData(num_steps=2, description=faker.pystr()) as progress_bar:
809+
port_values = (await PORTS.outputs).values()
774810
await PORTS.set_multiple(
775-
{
776-
ServicePortKey(port.key): (k, None)
777-
for k, port in enumerate((await PORTS.outputs).values())
778-
},
811+
{ServicePortKey(port.key): (k, None) for k, port in enumerate(port_values)},
779812
progress_bar=progress_bar,
813+
outputs_callbacks=callbacks,
814+
)
815+
assert len(spy_outputs_callbaks["finished_succesfully"].call_args_list) == (
816+
len(port_values) if use_output_callbacks else 0
780817
)
781818
# pylint: disable=protected-access
782819
assert progress_bar._current_steps == pytest.approx(1) # noqa: SLF001
@@ -786,6 +823,11 @@ async def test_batch_update_inputs_outputs(
786823
for k, port in enumerate((await PORTS.inputs).values(), start=1000)
787824
},
788825
progress_bar=progress_bar,
826+
outputs_callbacks=callbacks,
827+
)
828+
# inputs do not trigger callbacks
829+
assert len(spy_outputs_callbaks["finished_succesfully"].call_args_list) == (
830+
len(port_values) if use_output_callbacks else 0
789831
)
790832
assert progress_bar._current_steps == pytest.approx(2) # noqa: SLF001
791833

@@ -807,4 +849,11 @@ async def test_batch_update_inputs_outputs(
807849
await PORTS.set_multiple(
808850
{ServicePortKey("missing_key_in_both"): (123132, None)},
809851
progress_bar=progress_bar,
852+
outputs_callbacks=callbacks,
810853
)
854+
855+
assert len(spy_outputs_callbaks["finished_succesfully"].call_args_list) == (
856+
len(port_values) if use_output_callbacks else 0
857+
)
858+
assert len(spy_outputs_callbaks["aborted"].call_args_list) == 0
859+
assert len(spy_outputs_callbaks["finished_with_error"].call_args_list) == 0

packages/simcore-sdk/tests/unit/test_node_ports_v2_nodeports_v2.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from pathlib import Path
77
from typing import Any, Callable
8+
from unittest.mock import AsyncMock
89

910
import pytest
1011
from faker import Faker
@@ -138,6 +139,7 @@ async def mock_node_port_creator_cb(*args, **kwargs):
138139
+ list(original_outputs.values())
139140
},
140141
progress_bar=progress_bar,
142+
outputs_callbacks=AsyncMock(),
141143
)
142144
assert progress_bar._current_steps == pytest.approx(1) # noqa: SLF001
143145

services/director-v2/tests/integration/02/test_mixed_dynamic_sidecar_and_legacy_project.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ async def _mocked_context_manger(*args, **kwargs) -> AsyncIterator[None]:
229229
)
230230

231231

232+
@pytest.mark.flaky(max_runs=3)
232233
async def test_legacy_and_dynamic_sidecar_run(
233234
initialized_app: FastAPI,
234235
wait_for_catalog_service: Callable[[UserID, str], Awaitable[None]],

services/dynamic-sidecar/src/simcore_service_dynamic_sidecar/api/containers_long_running_tasks.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ async def ports_inputs_pull_task(
209209
request: Request,
210210
tasks_manager: Annotated[TasksManager, Depends(get_tasks_manager)],
211211
app: Annotated[FastAPI, Depends(get_application)],
212+
settings: Annotated[ApplicationSettings, Depends(get_settings)],
212213
mounted_volumes: Annotated[MountedVolumes, Depends(get_mounted_volumes)],
213214
inputs_state: Annotated[InputsState, Depends(get_inputs_state)],
214215
port_keys: list[str] | None = None,
@@ -223,6 +224,7 @@ async def ports_inputs_pull_task(
223224
port_keys=port_keys,
224225
mounted_volumes=mounted_volumes,
225226
app=app,
227+
settings=settings,
226228
inputs_pulling_enabled=inputs_state.inputs_pulling_enabled,
227229
)
228230
except TaskAlreadyRunningError as e:

services/dynamic-sidecar/src/simcore_service_dynamic_sidecar/core/application.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from ..modules.attribute_monitor import setup_attribute_monitor
2020
from ..modules.inputs import setup_inputs
2121
from ..modules.mounted_fs import MountedVolumes, setup_mounted_fs
22+
from ..modules.notifications import setup_notifications
2223
from ..modules.outputs import setup_outputs
2324
from ..modules.prometheus_metrics import setup_prometheus_metrics
2425
from ..modules.resource_tracking import setup_resource_tracking
@@ -172,6 +173,7 @@ def create_app():
172173
setup_rabbitmq(app)
173174
setup_background_log_fetcher(app)
174175
setup_resource_tracking(app)
176+
setup_notifications(app)
175177
setup_system_monitor(app)
176178

177179
setup_mounted_fs(app)

services/dynamic-sidecar/src/simcore_service_dynamic_sidecar/modules/long_running_tasks.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from ..models.shared_store import SharedStore
5353
from ..modules import nodeports, user_services_preferences
5454
from ..modules.mounted_fs import MountedVolumes
55+
from ..modules.notifications._notifications_ports import PortNotifier
5556
from ..modules.outputs import OutputsManager, event_propagation_disabled
5657
from .long_running_tasksutils import run_before_shutdown_actions
5758
from .resource_tracking import send_service_started, send_service_stopped
@@ -472,6 +473,7 @@ async def task_ports_inputs_pull(
472473
port_keys: list[str] | None,
473474
mounted_volumes: MountedVolumes,
474475
app: FastAPI,
476+
settings: ApplicationSettings,
475477
*,
476478
inputs_pulling_enabled: bool,
477479
) -> int:
@@ -505,6 +507,12 @@ async def task_ports_inputs_pull(
505507
post_sidecar_log_message, app, log_level=logging.INFO
506508
),
507509
progress_bar=root_progress,
510+
port_notifier=PortNotifier(
511+
app,
512+
settings.DY_SIDECAR_USER_ID,
513+
settings.DY_SIDECAR_PROJECT_ID,
514+
settings.DY_SIDECAR_NODE_ID,
515+
),
508516
)
509517
await post_sidecar_log_message(
510518
app, "Finished pulling inputs", log_level=logging.INFO
@@ -541,6 +549,7 @@ async def task_ports_outputs_pull(
541549
post_sidecar_log_message, app, log_level=logging.INFO
542550
),
543551
progress_bar=root_progress,
552+
port_notifier=None,
544553
)
545554
await post_sidecar_log_message(
546555
app, "Finished pulling outputs", log_level=logging.INFO

0 commit comments

Comments
 (0)