Skip to content

Commit d200c57

Browse files
GitHKAndrei Neagu
andauthored
♻️ Refactor DiskUsage functionality to support efs-guardian (#6536)
Co-authored-by: Andrei Neagu <[email protected]>
1 parent 1032d56 commit d200c57

38 files changed

+665
-108
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from typing import Final
2+
3+
from pydantic import parse_obj_as
4+
5+
from ..rabbitmq_basic_types import RPCNamespace
6+
7+
DYNAMIC_SIDECAR_RPC_NAMESPACE: Final[RPCNamespace] = parse_obj_as(
8+
RPCNamespace, "dynamic-sidecar"
9+
)

packages/models-library/src/models_library/api_schemas_dynamic_sidecar/telemetry.py

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
11
from abc import abstractmethod
2-
from pathlib import Path
3-
from typing import Protocol
2+
from enum import auto
3+
from typing import Any, Final, Protocol
44

5-
from models_library.projects_nodes_io import NodeID
6-
from pydantic import BaseModel, ByteSize, Field
5+
from pydantic import (
6+
BaseModel,
7+
ByteSize,
8+
Field,
9+
NonNegativeFloat,
10+
NonNegativeInt,
11+
root_validator,
12+
validator,
13+
)
14+
15+
from ..projects_nodes_io import NodeID
16+
from ..utils.enums import StrAutoEnum
17+
18+
_EPSILON: Final[NonNegativeFloat] = 1e-16
19+
20+
21+
class MountPathCategory(StrAutoEnum):
22+
HOST = auto()
23+
STATES_VOLUMES = auto()
24+
INPUTS_VOLUMES = auto()
25+
OUTPUTS_VOLUMES = auto()
726

827

928
class SDiskUsageProtocol(Protocol):
@@ -28,31 +47,71 @@ def percent(self) -> float:
2847
...
2948

3049

50+
def _get_percent(used: float, total: float) -> float:
51+
return round(used * 100 / (total + _EPSILON), 2)
52+
53+
3154
class DiskUsage(BaseModel):
3255
used: ByteSize = Field(description="used space")
3356
free: ByteSize = Field(description="remaining space")
3457

3558
total: ByteSize = Field(description="total space = free + used")
36-
used_percent: float = Field(
59+
used_percent: NonNegativeFloat = Field(
3760
gte=0.00,
3861
lte=100.00,
3962
description="Percent of used space relative to the total space",
4063
)
4164

65+
@validator("free")
4266
@classmethod
43-
def from_ps_util_disk_usage(
44-
cls, ps_util_disk_usage: SDiskUsageProtocol
67+
def _free_positive(cls, v: float) -> float:
68+
if v < 0:
69+
msg = f"free={v} cannot be a negative value"
70+
raise ValueError(msg)
71+
return v
72+
73+
@validator("used")
74+
@classmethod
75+
def _used_positive(cls, v: float) -> float:
76+
if v < 0:
77+
msg = f"used={v} cannot be a negative value"
78+
raise ValueError(msg)
79+
return v
80+
81+
@root_validator(pre=True)
82+
@classmethod
83+
def _check_total(cls, values: dict[str, Any]) -> dict[str, Any]:
84+
total = values["total"]
85+
free = values["free"]
86+
used = values["used"]
87+
if total != free + used:
88+
msg = f"{total=} is different than the sum of {free=}+{used=} => sum={free+used}"
89+
raise ValueError(msg)
90+
return values
91+
92+
@classmethod
93+
def from_efs_guardian(
94+
cls, used: NonNegativeInt, total: NonNegativeInt
4595
) -> "DiskUsage":
46-
total = ps_util_disk_usage.free + ps_util_disk_usage.used
47-
used_percent = round(ps_util_disk_usage.used * 100 / total, 2)
96+
free = total - used
4897
return cls(
49-
used=ByteSize(ps_util_disk_usage.used),
50-
free=ByteSize(ps_util_disk_usage.free),
98+
used=ByteSize(used),
99+
free=ByteSize(free),
51100
total=ByteSize(total),
52-
used_percent=used_percent,
101+
used_percent=_get_percent(used, total),
53102
)
54103

104+
@classmethod
105+
def from_ps_util_disk_usage(
106+
cls, ps_util_disk_usage: SDiskUsageProtocol
107+
) -> "DiskUsage":
108+
total = ps_util_disk_usage.free + ps_util_disk_usage.used
109+
return cls.from_efs_guardian(ps_util_disk_usage.used, total)
110+
111+
def __hash__(self):
112+
return hash((self.used, self.free, self.total, self.used_percent))
113+
55114

56115
class ServiceDiskUsage(BaseModel):
57116
node_id: NodeID
58-
usage: dict[Path, DiskUsage]
117+
usage: dict[MountPathCategory, DiskUsage]

packages/models-library/tests/test_api_schemas_dynamic_sidecar_telemetry.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import pytest
33
from models_library.api_schemas_dynamic_sidecar.telemetry import DiskUsage
44
from psutil._common import sdiskusage
5+
from pydantic import ByteSize, ValidationError
56

67

78
def _assert_same_value(ps_util_disk_usage: sdiskusage) -> None:
@@ -27,3 +28,35 @@ def test_disk_usage_regression_cases(ps_util_disk_usage: sdiskusage):
2728
def test_disk_usage():
2829
ps_util_disk_usage = psutil.disk_usage("/")
2930
_assert_same_value(ps_util_disk_usage)
31+
32+
33+
def test_from_efs_guardian_constructor():
34+
result = DiskUsage.from_efs_guardian(10, 100)
35+
assert result.used == ByteSize(10)
36+
assert result.free == ByteSize(90)
37+
assert result.total == ByteSize(100)
38+
assert result.used_percent == 10
39+
40+
41+
def test_failing_validation():
42+
with pytest.raises(ValidationError) as exc:
43+
assert DiskUsage.from_efs_guardian(100, 10)
44+
45+
assert "free=" in f"{exc.value}"
46+
assert "negative value" in f"{exc.value}"
47+
48+
with pytest.raises(ValidationError) as exc:
49+
assert DiskUsage(
50+
used=-10, # type: ignore
51+
free=ByteSize(10),
52+
total=ByteSize(0),
53+
used_percent=-10,
54+
)
55+
assert "used=" in f"{exc.value}"
56+
assert "negative value" in f"{exc.value}"
57+
58+
with pytest.raises(ValidationError) as exc:
59+
DiskUsage(
60+
used=ByteSize(10), free=ByteSize(10), total=ByteSize(21), used_percent=0
61+
)
62+
assert "is different than the sum of" in f"{exc.value}"

packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/dynamic_sidecar/__init__.py

Whitespace-only changes.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import logging
2+
3+
from models_library.api_schemas_dynamic_sidecar import DYNAMIC_SIDECAR_RPC_NAMESPACE
4+
from models_library.api_schemas_dynamic_sidecar.telemetry import DiskUsage
5+
from models_library.rabbitmq_basic_types import RPCMethodName
6+
from pydantic import parse_obj_as
7+
from servicelib.logging_utils import log_decorator
8+
from servicelib.rabbitmq import RabbitMQRPCClient
9+
10+
_logger = logging.getLogger(__name__)
11+
12+
13+
@log_decorator(_logger, level=logging.DEBUG)
14+
async def update_disk_usage(
15+
rabbitmq_rpc_client: RabbitMQRPCClient, *, usage: dict[str, DiskUsage]
16+
) -> None:
17+
result = await rabbitmq_rpc_client.request(
18+
DYNAMIC_SIDECAR_RPC_NAMESPACE,
19+
parse_obj_as(RPCMethodName, "update_disk_usage"),
20+
usage=usage,
21+
)
22+
assert result is None # nosec
Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +0,0 @@
1-
from ._routing import get_main_router
2-
3-
__all__: tuple[str, ...] = ("get_main_router",)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from ._routing import get_main_router
2+
3+
__all__: tuple[str, ...] = ("get_main_router",)

services/dynamic-sidecar/src/simcore_service_dynamic_sidecar/api/_dependencies.py renamed to services/dynamic-sidecar/src/simcore_service_dynamic_sidecar/api/rest/_dependencies.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@
77
from fastapi import Depends, FastAPI, Request
88
from fastapi.datastructures import State
99
from servicelib.rabbitmq import RabbitMQClient
10+
from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient
1011

11-
from ..core import rabbitmq
12-
from ..core.settings import ApplicationSettings
13-
from ..models.schemas.application_health import ApplicationHealth
14-
from ..models.shared_store import SharedStore
15-
from ..modules.inputs import InputsState
16-
from ..modules.mounted_fs import MountedVolumes
17-
from ..modules.outputs import OutputsContext, OutputsManager
18-
from ..modules.prometheus_metrics import UserServicesMetrics
12+
from ...core import rabbitmq
13+
from ...core.settings import ApplicationSettings
14+
from ...models.schemas.application_health import ApplicationHealth
15+
from ...models.shared_store import SharedStore
16+
from ...modules.inputs import InputsState
17+
from ...modules.mounted_fs import MountedVolumes
18+
from ...modules.outputs import OutputsContext, OutputsManager
19+
from ...modules.prometheus_metrics import UserServicesMetrics
1920

2021

2122
def get_application(request: Request) -> FastAPI:
@@ -84,3 +85,9 @@ def get_rabbitmq_client(
8485
app: Annotated[FastAPI, Depends(get_application)]
8586
) -> RabbitMQClient:
8687
return rabbitmq.get_rabbitmq_client(app)
88+
89+
90+
def get_rabbitmq_rpc_server(
91+
app: Annotated[FastAPI, Depends(get_application)]
92+
) -> RabbitMQRPCClient:
93+
return rabbitmq.get_rabbitmq_rpc_server(app)

services/dynamic-sidecar/src/simcore_service_dynamic_sidecar/api/_routing.py renamed to services/dynamic-sidecar/src/simcore_service_dynamic_sidecar/api/rest/_routing.py

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

66
from fastapi import APIRouter, FastAPI
77

8-
from .._meta import API_VTAG
9-
from ..core.settings import ApplicationSettings
8+
from ..._meta import API_VTAG
9+
from ...core.settings import ApplicationSettings
1010
from . import (
1111
containers,
1212
containers_extension,

services/dynamic-sidecar/src/simcore_service_dynamic_sidecar/api/containers.py renamed to services/dynamic-sidecar/src/simcore_service_dynamic_sidecar/api/rest/containers.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,22 @@
1515
from pydantic import parse_raw_as
1616
from servicelib.fastapi.requests_decorators import cancel_on_disconnect
1717

18-
from ..core.docker_utils import docker_client
19-
from ..core.errors import (
18+
from ...core.docker_utils import docker_client
19+
from ...core.errors import (
2020
ContainerExecCommandFailedError,
2121
ContainerExecContainerNotFoundError,
2222
ContainerExecTimeoutError,
2323
)
24-
from ..core.settings import ApplicationSettings
25-
from ..core.validation import (
24+
from ...core.settings import ApplicationSettings
25+
from ...core.validation import (
2626
ComposeSpecValidation,
2727
parse_compose_spec,
2828
validate_compose_spec,
2929
)
30-
from ..models.schemas.containers import ContainersComposeSpec
31-
from ..models.shared_store import SharedStore
32-
from ..modules.container_utils import run_command_in_container
33-
from ..modules.mounted_fs import MountedVolumes
30+
from ...models.schemas.containers import ContainersComposeSpec
31+
from ...models.shared_store import SharedStore
32+
from ...modules.container_utils import run_command_in_container
33+
from ...modules.mounted_fs import MountedVolumes
3434
from ._dependencies import (
3535
get_container_restart_lock,
3636
get_mounted_volumes,

0 commit comments

Comments
 (0)