Skip to content

Commit c0b276a

Browse files
🎨 notify frontend about current efs disk space (#6520)
1 parent 1067924 commit c0b276a

File tree

8 files changed

+222
-1
lines changed

8 files changed

+222
-1
lines changed

services/efs-guardian/src/simcore_service_efs_guardian/core/application.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from ..api.rpc.routes import setup_rpc_routes
1616
from ..services.background_tasks_setup import setup as setup_background_tasks
1717
from ..services.efs_manager_setup import setup as setup_efs_manager
18+
from ..services.fire_and_forget_setup import setup as setup_fire_and_forget
1819
from ..services.modules.db import setup as setup_db
1920
from ..services.modules.rabbitmq import setup as setup_rabbitmq
2021
from ..services.modules.redis import setup as setup_redis
@@ -56,6 +57,8 @@ def create_app(settings: ApplicationSettings | None = None) -> FastAPI:
5657
setup_background_tasks(app) # requires Redis, DB
5758
setup_process_messages(app) # requires Rabbit
5859

60+
setup_fire_and_forget(app)
61+
5962
# EVENTS
6063
async def _on_startup() -> None:
6164
print(APP_STARTED_BANNER_MSG, flush=True) # noqa: T201

services/efs-guardian/src/simcore_service_efs_guardian/services/efs_manager.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,31 @@ async def get_project_node_data_size(
8383

8484
return await efs_manager_utils.get_size_bash_async(_dir_path)
8585

86+
async def list_project_node_state_names(
87+
self, project_id: ProjectID, node_id: NodeID
88+
) -> list[str]:
89+
"""
90+
These are currently state volumes that are mounted via docker volume to dynamic sidecar and user services
91+
(ex. ".data_assets" and "home_user_workspace")
92+
"""
93+
_dir_path = (
94+
self._efs_mounted_path
95+
/ self._project_specific_data_base_directory
96+
/ f"{project_id}"
97+
/ f"{node_id}"
98+
)
99+
100+
project_node_states = []
101+
for child in _dir_path.iterdir():
102+
if child.is_dir():
103+
project_node_states.append(child.name)
104+
else:
105+
_logger.error(
106+
"This is not a directory. This should not happen! %s",
107+
_dir_path / child.name,
108+
)
109+
return project_node_states
110+
86111
async def remove_project_node_data_write_permissions(
87112
self, project_id: ProjectID, node_id: NodeID
88113
) -> None:
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import logging
2+
from collections.abc import Awaitable, Callable
3+
4+
from fastapi import FastAPI
5+
from servicelib.logging_utils import log_catch, log_context
6+
7+
_logger = logging.getLogger(__name__)
8+
9+
10+
def _on_app_startup(_app: FastAPI) -> Callable[[], Awaitable[None]]:
11+
async def _startup() -> None:
12+
with log_context(
13+
_logger, logging.INFO, msg="Efs Guardian setup fire and forget tasks.."
14+
), log_catch(_logger, reraise=False):
15+
_app.state.efs_guardian_fire_and_forget_tasks = set()
16+
17+
return _startup
18+
19+
20+
def _on_app_shutdown(
21+
_app: FastAPI,
22+
) -> Callable[[], Awaitable[None]]:
23+
async def _stop() -> None:
24+
with log_context(
25+
_logger, logging.INFO, msg="Efs Guardian fire and forget tasks shutdown.."
26+
), log_catch(_logger, reraise=False):
27+
assert _app # nosec
28+
if _app.state.efs_guardian_fire_and_forget_tasks:
29+
for task in _app.state.efs_guardian_fire_and_forget_tasks:
30+
task.cancel()
31+
32+
return _stop
33+
34+
35+
def setup(app: FastAPI) -> None:
36+
app.add_event_handler("startup", _on_app_startup(app))
37+
app.add_event_handler("shutdown", _on_app_shutdown(app))

services/efs-guardian/src/simcore_service_efs_guardian/services/modules/rabbitmq.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,17 @@ async def on_startup() -> None:
2929
app.state.rabbitmq_rpc_server = await RabbitMQRPCClient.create(
3030
client_name="efs_guardian_rpc_server", settings=settings
3131
)
32+
app.state.rabbitmq_rpc_client = await RabbitMQRPCClient.create(
33+
client_name="efs_guardian_rpc_client", settings=settings
34+
)
3235

3336
async def on_shutdown() -> None:
3437
if app.state.rabbitmq_client:
3538
await app.state.rabbitmq_client.close()
3639
if app.state.rabbitmq_rpc_server:
3740
await app.state.rabbitmq_rpc_server.close()
41+
if app.state.rabbitmq_rpc_client:
42+
await app.state.rabbitmq_rpc_client.close()
3843

3944
app.add_event_handler("startup", on_startup)
4045
app.add_event_handler("shutdown", on_shutdown)
@@ -53,4 +58,9 @@ def get_rabbitmq_rpc_server(app: FastAPI) -> RabbitMQRPCClient:
5358
return cast(RabbitMQRPCClient, app.state.rabbitmq_rpc_server)
5459

5560

61+
def get_rabbitmq_rpc_client(app: FastAPI) -> RabbitMQRPCClient:
62+
assert app.state.rabbitmq_rpc_client # nosec
63+
return cast(RabbitMQRPCClient, app.state.rabbitmq_rpc_client)
64+
65+
5666
__all__ = ("RabbitMQClient",)

services/efs-guardian/src/simcore_service_efs_guardian/services/process_messages.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import logging
22

33
from fastapi import FastAPI
4+
from models_library.api_schemas_dynamic_sidecar.telemetry import DiskUsage
45
from models_library.rabbitmq_messages import DynamicServiceRunningMessage
56
from pydantic import parse_raw_as
67
from servicelib.logging_utils import log_context
7-
from simcore_service_efs_guardian.services.modules.redis import get_redis_lock_client
8+
from servicelib.rabbitmq import RabbitMQRPCClient
9+
from servicelib.rabbitmq.rpc_interfaces.dynamic_sidecar.disk_usage import (
10+
update_disk_usage,
11+
)
12+
from servicelib.utils import fire_and_forget_task
813

914
from ..core.settings import get_application_settings
1015
from ..services.efs_manager import EfsManager
16+
from ..services.modules.rabbitmq import get_rabbitmq_rpc_client
17+
from ..services.modules.redis import get_redis_lock_client
1118

1219
_logger = logging.getLogger(__name__)
1320

@@ -50,6 +57,23 @@ async def process_dynamic_service_running_message(app: FastAPI, data: bytes) ->
5057
rabbit_message.user_id,
5158
)
5259

60+
project_node_state_names = await efs_manager.list_project_node_state_names(
61+
rabbit_message.project_id, node_id=rabbit_message.node_id
62+
)
63+
rpc_client: RabbitMQRPCClient = get_rabbitmq_rpc_client(app)
64+
_used = min(size, settings.EFS_DEFAULT_USER_SERVICE_SIZE_BYTES)
65+
usage: dict[str, DiskUsage] = {}
66+
for name in project_node_state_names:
67+
usage[name] = DiskUsage.from_efs_guardian(
68+
used=_used, total=settings.EFS_DEFAULT_USER_SERVICE_SIZE_BYTES
69+
)
70+
71+
fire_and_forget_task(
72+
update_disk_usage(rpc_client, node_id=rabbit_message.node_id, usage=usage),
73+
task_suffix_name=f"update_disk_usage_efs_user_id{rabbit_message.user_id}_node_id{rabbit_message.node_id}",
74+
fire_and_forget_tasks_collection=app.state.efs_guardian_fire_and_forget_tasks,
75+
)
76+
5377
if size > settings.EFS_DEFAULT_USER_SERVICE_SIZE_BYTES:
5478
msg = f"Removing write permissions inside of EFS starts for project ID: {rabbit_message.project_id}, node ID: {rabbit_message.node_id}, current user: {rabbit_message.user_id}, size: {size}, upper limit: {settings.EFS_DEFAULT_USER_SERVICE_SIZE_BYTES}"
5579
with log_context(_logger, logging.WARNING, msg=msg):

services/efs-guardian/tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"pytest_simcore.environment_configs",
2222
"pytest_simcore.faker_projects_data",
2323
"pytest_simcore.faker_users_data",
24+
"pytest_simcore.faker_products_data",
2425
"pytest_simcore.faker_projects_data",
2526
"pytest_simcore.pydantic_models",
2627
"pytest_simcore.pytest_global_environs",

services/efs-guardian/tests/unit/test_efs_manager.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ async def test_remove_write_access_rights(
9191
is False
9292
)
9393

94+
with pytest.raises(FileNotFoundError):
95+
await efs_manager.list_project_node_state_names(
96+
project_id=project_id, node_id=node_id
97+
)
98+
9499
with patch(
95100
"simcore_service_efs_guardian.services.efs_manager.os.chown"
96101
) as mocked_chown:
@@ -108,6 +113,11 @@ async def test_remove_write_access_rights(
108113
is True
109114
)
110115

116+
project_node_state_names = await efs_manager.list_project_node_state_names(
117+
project_id=project_id, node_id=node_id
118+
)
119+
assert project_node_state_names == [_storage_directory_name]
120+
111121
size_before = await efs_manager.get_project_node_data_size(
112122
project_id=project_id, node_id=node_id
113123
)
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# pylint: disable=protected-access
2+
# pylint: disable=redefined-outer-name
3+
# pylint: disable=too-many-arguments
4+
# pylint: disable=unused-argument
5+
# pylint: disable=unused-variable
6+
7+
8+
from unittest.mock import AsyncMock, patch
9+
10+
import pytest
11+
from faker import Faker
12+
from fastapi import FastAPI
13+
from models_library.products import ProductName
14+
from models_library.rabbitmq_messages import DynamicServiceRunningMessage
15+
from models_library.users import UserID
16+
from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict
17+
from pytest_simcore.helpers.typing_env import EnvVarsDict
18+
from simcore_service_efs_guardian.services.efs_manager import NodeID, ProjectID
19+
from simcore_service_efs_guardian.services.process_messages import (
20+
process_dynamic_service_running_message,
21+
)
22+
23+
pytest_simcore_core_services_selection = ["rabbit"]
24+
pytest_simcore_ops_services_selection = []
25+
26+
27+
@pytest.fixture
28+
def app_environment(
29+
monkeypatch: pytest.MonkeyPatch,
30+
app_environment: EnvVarsDict,
31+
rabbit_env_vars_dict: EnvVarsDict,
32+
with_disabled_redis_and_background_tasks: None,
33+
with_disabled_postgres: None,
34+
) -> EnvVarsDict:
35+
return setenvs_from_dict(
36+
monkeypatch,
37+
{
38+
**app_environment,
39+
**rabbit_env_vars_dict,
40+
"EFS_DEFAULT_USER_SERVICE_SIZE_BYTES": "10000",
41+
},
42+
)
43+
44+
45+
@patch("simcore_service_efs_guardian.services.process_messages.update_disk_usage")
46+
async def test_process_msg(
47+
mock_update_disk_usage,
48+
faker: Faker,
49+
app: FastAPI,
50+
efs_cleanup: None,
51+
project_id: ProjectID,
52+
node_id: NodeID,
53+
user_id: UserID,
54+
product_name: ProductName,
55+
):
56+
# Create mock data for the message
57+
model_instance = DynamicServiceRunningMessage(
58+
project_id=project_id,
59+
node_id=node_id,
60+
user_id=user_id,
61+
product_name=product_name,
62+
)
63+
json_str = model_instance.json()
64+
model_bytes = json_str.encode("utf-8")
65+
66+
_expected_project_node_states = [".data_assets", "home_user_workspace"]
67+
# Mock efs_manager and its methods
68+
mock_efs_manager = AsyncMock()
69+
app.state.efs_manager = mock_efs_manager
70+
mock_efs_manager.check_project_node_data_directory_exits.return_value = True
71+
mock_efs_manager.get_project_node_data_size.return_value = 4000
72+
mock_efs_manager.list_project_node_state_names.return_value = (
73+
_expected_project_node_states
74+
)
75+
76+
result = await process_dynamic_service_running_message(app, data=model_bytes)
77+
78+
# Check the actual arguments passed to notify_service_efs_disk_usage
79+
_, kwargs = mock_update_disk_usage.call_args
80+
assert kwargs["usage"]
81+
assert len(kwargs["usage"]) == 2
82+
for key, value in kwargs["usage"].items():
83+
assert key in _expected_project_node_states
84+
assert value.used == 4000
85+
assert value.free == 6000
86+
assert value.total == 10000
87+
assert value.used_percent == 40.0
88+
89+
assert result is True
90+
91+
92+
async def test_process_msg__dir_not_exists(
93+
app: FastAPI,
94+
efs_cleanup: None,
95+
project_id: ProjectID,
96+
node_id: NodeID,
97+
user_id: UserID,
98+
product_name: ProductName,
99+
):
100+
# Create mock data for the message
101+
model_instance = DynamicServiceRunningMessage(
102+
project_id=project_id,
103+
node_id=node_id,
104+
user_id=user_id,
105+
product_name=product_name,
106+
)
107+
json_str = model_instance.json()
108+
model_bytes = json_str.encode("utf-8")
109+
110+
result = await process_dynamic_service_running_message(app, data=model_bytes)
111+
assert result is True

0 commit comments

Comments
 (0)