Skip to content

Commit 3f8115c

Browse files
author
Andrei Neagu
committed
current WIP version with FastUI
1 parent 78debb9 commit 3f8115c

File tree

5 files changed

+176
-40
lines changed

5 files changed

+176
-40
lines changed

services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/ui/_services.py

Lines changed: 160 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,31 @@
1010
from fastui import AnyComponent, FastUI
1111
from fastui import components as c
1212
from fastui.events import GoToEvent, PageEvent
13+
from models_library.api_schemas_dynamic_scheduler.dynamic_services import (
14+
DynamicServiceStop,
15+
)
1316
from models_library.projects_nodes_io import NodeID
1417
from servicelib.background_task import start_periodic_task, stop_periodic_task
1518
from servicelib.fastapi.app_state import SingletonInAppStateMixin
1619
from servicelib.logging_utils import log_catch, log_context
20+
from servicelib.rabbitmq.rpc_interfaces.dynamic_scheduler.services import (
21+
stop_dynamic_service,
22+
)
23+
from simcore_service_dynamic_scheduler.services.service_tracker._models import (
24+
SchedulerServiceState,
25+
)
1726
from starlette import status
1827

28+
from ...core.settings import ApplicationSettings
29+
from ...services.rabbitmq import get_rabbitmq_rpc_client
1930
from ...services.service_tracker import (
2031
TrackedServiceModel,
2132
get_all_tracked_services,
2233
get_tracked_service,
2334
)
2435
from ..dependencies import get_app
2536
from . import _custom_components as cu
26-
from ._constants import API_ROOT_PATH
37+
from ._constants import API_ROOT_PATH, UI_MOUNT_PREFIX
2738
from ._sse_utils import (
2839
AbstractSSERenderer,
2940
render_items_on_change,
@@ -51,10 +62,7 @@ def _page_base(
5162
]
5263

5364

54-
@router.get(
55-
f"{API_ROOT_PATH}/", response_model=FastUI, response_model_exclude_none=True
56-
)
57-
def api_index() -> list[AnyComponent]:
65+
def _get_index_page() -> list[AnyComponent]:
5866
return _page_base(
5967
c.Heading(text="Dynamic services status", level=4),
6068
c.Paragraph(text="List of all services currently tracked by the scheduler"),
@@ -72,6 +80,13 @@ def api_index() -> list[AnyComponent]:
7280
)
7381

7482

83+
@router.get(
84+
f"{API_ROOT_PATH}/", response_model=FastUI, response_model_exclude_none=True
85+
)
86+
def api_index() -> list[AnyComponent]:
87+
return _get_index_page()
88+
89+
7590
@router.get(
7691
f"{API_ROOT_PATH}{_PREFIX}/details/",
7792
response_model=FastUI,
@@ -96,42 +111,151 @@ async def service_details(
96111
)
97112

98113

114+
@router.post(
115+
f"{API_ROOT_PATH}{_PREFIX}/stop-service/",
116+
response_model=FastUI,
117+
response_model_exclude_none=True,
118+
)
119+
async def stop_service(
120+
node_id: NodeID, app: Annotated[FastAPI, Depends(get_app)]
121+
) -> list[AnyComponent]:
122+
service_model = await get_tracked_service(app, node_id)
123+
124+
if service_model and service_model.user_id and service_model.project_id:
125+
settings: ApplicationSettings = app.state.settings
126+
await stop_dynamic_service(
127+
get_rabbitmq_rpc_client(app),
128+
dynamic_service_stop=DynamicServiceStop(
129+
user_id=service_model.user_id,
130+
project_id=service_model.project_id,
131+
node_id=node_id,
132+
simcore_user_agent="",
133+
save_state=True,
134+
),
135+
timeout_s=int(
136+
settings.DYNAMIC_SCHEDULER_STOP_SERVICE_TIMEOUT.total_seconds()
137+
),
138+
)
139+
140+
return _get_index_page()
141+
142+
143+
# TODO: add a toast for displaying the status of the stop operation
144+
# TODO: change logic since SSE is not working as expected. You can use it to detect changes in the model -> then reload the page, which is useless
145+
# TODO: add an endpoint to remove from tracking without using anything else
146+
147+
148+
def _render_partial(
149+
node_id: NodeID, service_model: TrackedServiceModel
150+
) -> AnyComponent:
151+
list_display: list[tuple[Any, Any]] = [
152+
("NodeID", node_id),
153+
("Service state", service_model.current_state),
154+
(
155+
"Last state change",
156+
arrow.get(service_model.last_state_change).isoformat(),
157+
),
158+
("Requested", service_model.requested_state),
159+
("ProjectID", service_model.project_id),
160+
("UserID", service_model.user_id),
161+
]
162+
163+
if service_model.dynamic_service_start:
164+
list_display.extend(
165+
[
166+
("Service Key", service_model.dynamic_service_start.key),
167+
("Service Version", service_model.dynamic_service_start.version),
168+
("Product", service_model.dynamic_service_start.product_name),
169+
]
170+
)
171+
components = [
172+
c.Text(text="PARTIAL"),
173+
cu.markdown_list_display(list_display),
174+
c.Button(
175+
text="Details",
176+
named_style="secondary",
177+
on_click=GoToEvent(url=f"{_PREFIX}/details/?node_id={node_id}"),
178+
),
179+
c.Button(
180+
text="Stop Service",
181+
on_click=PageEvent(name="modal-prompt"),
182+
class_name="+ ms-2",
183+
),
184+
]
185+
186+
return c.Div(components=components, class_name="border border-double")
187+
188+
189+
def _render_full(node_id: NodeID, service_model: TrackedServiceModel) -> AnyComponent:
190+
list_display: list[tuple[Any, Any]] = [
191+
("NodeID", node_id),
192+
("Service state", service_model.current_state),
193+
(
194+
"Last state change",
195+
arrow.get(service_model.last_state_change).isoformat(),
196+
),
197+
("Requested", service_model.requested_state),
198+
("ProjectID", service_model.project_id),
199+
("UserID", service_model.user_id),
200+
]
201+
202+
if service_model.dynamic_service_start:
203+
list_display.extend(
204+
[
205+
("Service Key", service_model.dynamic_service_start.key),
206+
("Service Version", service_model.dynamic_service_start.version),
207+
("Product", service_model.dynamic_service_start.product_name),
208+
]
209+
)
210+
components = [
211+
c.Text(text="FULL_RENDERED"),
212+
cu.markdown_list_display(list_display),
213+
c.Button(
214+
text="Details",
215+
named_style="secondary",
216+
on_click=GoToEvent(url=f"{_PREFIX}/details/?node_id={node_id}"),
217+
),
218+
c.Button(
219+
text="Stop Service",
220+
on_click=PageEvent(name="modal-prompt"),
221+
class_name="+ ms-2",
222+
),
223+
c.Modal(
224+
title="Stop Service",
225+
body=[
226+
c.Paragraph(text=f"Are you sure you want to stop {node_id}?"),
227+
c.Form(
228+
form_fields=[],
229+
submit_url=f"{UI_MOUNT_PREFIX}{API_ROOT_PATH}{_PREFIX}/stop-service/?node_id={node_id}",
230+
loading=[c.Spinner(text="Stopping...")],
231+
footer=[],
232+
submit_trigger=PageEvent(name="modal-form-submit"),
233+
),
234+
],
235+
footer=[
236+
c.Button(
237+
text="Cancel",
238+
named_style="secondary",
239+
on_click=PageEvent(name="modal-prompt", clear=True),
240+
),
241+
c.Button(text="Submit", on_click=PageEvent(name="modal-form-submit")),
242+
],
243+
open_trigger=PageEvent(name="modal-prompt"),
244+
),
245+
]
246+
247+
return c.Div(components=components, class_name="border border-dotted")
248+
249+
99250
class ServicesSSERenderer(AbstractSSERenderer):
100251
@staticmethod
101252
def get_component(item: tuple[NodeID, TrackedServiceModel]) -> AnyComponent:
102253
node_id, service_model = item
103254

104-
list_display: list[tuple[Any, Any]] = [
105-
("NodeID", node_id),
106-
("Service state", service_model.current_state),
107-
(
108-
"Last state change",
109-
arrow.get(service_model.last_state_change).isoformat(),
110-
),
111-
("Requested", service_model.requested_state),
112-
("ProjectID", service_model.project_id),
113-
("UserID", service_model.user_id),
114-
]
115-
116-
if service_model.dynamic_service_start:
117-
list_display.extend(
118-
[
119-
("Service Key", service_model.dynamic_service_start.key),
120-
("Service Version", service_model.dynamic_service_start.version),
121-
("Product", service_model.dynamic_service_start.product_name),
122-
]
123-
)
124-
components = [
125-
cu.markdown_list_display(list_display),
126-
c.Link(
127-
components=[c.Text(text="Details")],
128-
on_click=GoToEvent(
129-
url=f"{_PREFIX}/details/?node_id={node_id}",
130-
),
131-
),
132-
]
255+
if service_model.current_state == SchedulerServiceState.RUNNING:
256+
return _render_full(node_id, service_model)
133257

134-
return c.Div(components=components, class_name="border border-blue-500 px-4")
258+
return _render_partial(node_id, service_model)
135259

136260

137261
@router.get(f"{API_ROOT_PATH}{_PREFIX}/sse/")
@@ -173,7 +297,7 @@ def startup(self) -> None:
173297
self._task = start_periodic_task(
174298
self._task_service_state_retrieval,
175299
interval=self.poll_interval,
176-
task_name="sse_periodic_status_poll",
300+
task_name="sse_services_status_retrieval_from_redis",
177301
)
178302

179303
async def shutdown(self) -> None:

services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/services/rabbitmq.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,18 @@ async def _on_startup() -> None:
2121
app.state.rabbitmq_client = RabbitMQClient(
2222
client_name="dynamic_scheduler", settings=settings
2323
)
24+
app.state.rabbitmq_rpc_client = await RabbitMQRPCClient.create(
25+
client_name="dynamic_scheduler_rpc_client", settings=settings
26+
)
2427
app.state.rabbitmq_rpc_server = await RabbitMQRPCClient.create(
2528
client_name="dynamic_scheduler_rpc_server", settings=settings
2629
)
2730

2831
async def _on_shutdown() -> None:
2932
if app.state.rabbitmq_client:
3033
await app.state.rabbitmq_client.close()
34+
if app.state.rabbitmq_rpc_client:
35+
await app.state.rabbitmq_rpc_client.close()
3136
if app.state.rabbitmq_rpc_server:
3237
await app.state.rabbitmq_rpc_server.close()
3338

@@ -40,6 +45,11 @@ def get_rabbitmq_client(app: FastAPI) -> RabbitMQClient:
4045
return cast(RabbitMQClient, app.state.rabbitmq_client)
4146

4247

48+
def get_rabbitmq_rpc_client(app: FastAPI) -> RabbitMQRPCClient:
49+
assert app.state.rabbitmq_rpc_client
50+
return cast(RabbitMQRPCClient, app.state.rabbitmq_rpc_client)
51+
52+
4353
def get_rabbitmq_rpc_server(app: FastAPI) -> RabbitMQRPCClient:
4454
assert app.state.rabbitmq_rpc_server # nosec
4555
return cast(RabbitMQRPCClient, app.state.rabbitmq_rpc_server)

services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/services/service_tracker/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
get_tracked_service,
55
get_user_id_for_service,
66
remove_tracked_service,
7-
set_frontned_notified_for_service,
7+
set_frontend_notified_for_service,
88
set_if_status_changed_for_service,
99
set_request_as_running,
1010
set_request_as_stopped,
@@ -21,7 +21,7 @@
2121
"get_user_id_for_service",
2222
"NORMAL_RATE_POLL_INTERVAL",
2323
"remove_tracked_service",
24-
"set_frontned_notified_for_service",
24+
"set_frontend_notified_for_service",
2525
"set_if_status_changed_for_service",
2626
"set_request_as_running",
2727
"set_request_as_stopped",

services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/services/service_tracker/_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ async def should_notify_frontend_for_service(
184184
)
185185

186186

187-
async def set_frontned_notified_for_service(app: FastAPI, node_id: NodeID) -> None:
187+
async def set_frontend_notified_for_service(app: FastAPI, node_id: NodeID) -> None:
188188
tracker = get_tracker(app)
189189
model: TrackedServiceModel | None = await tracker.load(node_id)
190190
if model is None:

services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/services/status_monitor/_deferred_get_status.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ async def on_result(
7676
)
7777
if user_id:
7878
await notify_service_status_change(app, user_id, result)
79-
await service_tracker.set_frontned_notified_for_service(app, node_id)
79+
# TODO: also notify the UI renderer about the change
80+
# Write this using pubsub and a queue. We render if stuff changes?
81+
await service_tracker.set_frontend_notified_for_service(app, node_id)
8082
else:
8183
_logger.info(
8284
"Did not find a user for '%s', skipping status delivery of: %s",

0 commit comments

Comments
 (0)