Skip to content

Commit

Permalink
✨ allows frontend to check if a project is inactive (ITISFoundation#4895
Browse files Browse the repository at this point in the history
)

Co-authored-by: Andrei Neagu <[email protected]>
  • Loading branch information
GitHK and Andrei Neagu authored Oct 25, 2023
1 parent 1a37096 commit ef08908
Show file tree
Hide file tree
Showing 32 changed files with 862 additions and 69 deletions.
14 changes: 14 additions & 0 deletions api/specs/web-server/_projects_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
from typing import Annotated

from fastapi import APIRouter, Depends, status
from models_library.api_schemas_directorv2.dynamic_services import (
GetProjectInactivityResponse,
)
from models_library.api_schemas_long_running_tasks.tasks import TaskGet
from models_library.api_schemas_webserver.projects import (
ProjectCopyOverride,
Expand Down Expand Up @@ -109,3 +112,14 @@ async def clone_project(
_params: Annotated[ProjectPathParams, Depends()],
):
...


@router.get(
"/projects/{project_id}/inactivity",
response_model=Envelope[GetProjectInactivityResponse],
status_code=status.HTTP_200_OK,
)
async def get_project_inactivity(
_params: Annotated[ProjectPathParams, Depends()],
):
...
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,7 @@ class Config:


DynamicServiceGet: TypeAlias = RunningDynamicServiceDetails


class GetProjectInactivityResponse(BaseModel):
is_inactive: bool
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from pydantic import BaseModel, NonNegativeFloat


class InactivityResponse(BaseModel):
seconds_inactive: NonNegativeFloat | None = None

@property
def is_inactive(self) -> bool:
return self.seconds_inactive is not None
37 changes: 35 additions & 2 deletions packages/models-library/src/models_library/callbacks_mapping.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from collections.abc import Sequence
from typing import Any, ClassVar
from typing import Any, ClassVar, Final

from pydantic import BaseModel, Extra, Field, NonNegativeFloat
from pydantic import BaseModel, Extra, Field, NonNegativeFloat, validator

INACTIVITY_TIMEOUT_CAP: Final[NonNegativeFloat] = 5
TIMEOUT_MIN: Final[NonNegativeFloat] = 1


class UserServiceCommand(BaseModel):
Expand Down Expand Up @@ -36,6 +39,28 @@ class CallbacksMapping(BaseModel):
"user services are allowed"
),
)
inactivity: UserServiceCommand | None = Field(
None,
description=(
"command used to figure out for how much time the "
"user service(s) were inactive for"
),
)

@validator("inactivity")
@classmethod
def ensure_inactivity_timeout_is_capped(
cls, v: UserServiceCommand
) -> UserServiceCommand:
if v is not None and (
v.timeout < TIMEOUT_MIN or v.timeout > INACTIVITY_TIMEOUT_CAP
):
msg = (
f"Constraint not respected for inactivity timeout={v.timeout}: "
f"interval=({TIMEOUT_MIN}, {INACTIVITY_TIMEOUT_CAP})"
)
raise ValueError(msg)
return v

class Config:
extra = Extra.forbid
Expand All @@ -56,5 +81,13 @@ class Config:
UserServiceCommand.Config.schema_extra["examples"][1],
],
},
{
"metrics": UserServiceCommand.Config.schema_extra["examples"][0],
"before_shutdown": [
UserServiceCommand.Config.schema_extra["examples"][0],
UserServiceCommand.Config.schema_extra["examples"][1],
],
"inactivity": UserServiceCommand.Config.schema_extra["examples"][0],
},
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from typing import Any

import pytest
from models_library.api_schemas_dynamic_sidecar.containers import InactivityResponse


@pytest.mark.parametrize(
"data, is_inactive",
[
pytest.param({"seconds_inactive": None}, False),
pytest.param({"seconds_inactive": 0}, True),
pytest.param({"seconds_inactive": 100}, True),
],
)
def test_expected(data: dict[str, Any], is_inactive: bool):
assert InactivityResponse.parse_obj(data).is_inactive == is_inactive
27 changes: 27 additions & 0 deletions packages/models-library/tests/test_callbacks_mapping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from typing import Any

import pytest
from models_library.callbacks_mapping import (
INACTIVITY_TIMEOUT_CAP,
TIMEOUT_MIN,
CallbacksMapping,
)
from pydantic import ValidationError, parse_obj_as


def _format_with_timeout(timeout: float) -> dict[str, Any]:
return {"inactivity": {"service": "a-service", "command": "", "timeout": timeout}}


def test_inactivity_time_out_is_max_capped():
for in_bounds in [
TIMEOUT_MIN,
TIMEOUT_MIN + 1,
INACTIVITY_TIMEOUT_CAP - 1,
INACTIVITY_TIMEOUT_CAP,
]:
parse_obj_as(CallbacksMapping, _format_with_timeout(in_bounds))

for out_of_bounds in [INACTIVITY_TIMEOUT_CAP + 1, TIMEOUT_MIN - 1]:
with pytest.raises(ValidationError):
parse_obj_as(CallbacksMapping, _format_with_timeout(out_of_bounds))
139 changes: 139 additions & 0 deletions services/director-v2/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,59 @@
}
}
},
"/v2/dynamic_services/projects/{project_id}/inactivity": {
"get": {
"tags": [
"dynamic services"
],
"summary": "returns if the project is inactive",
"operationId": "get_project_inactivity_v2_dynamic_services_projects__project_id__inactivity_get",
"parameters": [
{
"required": true,
"schema": {
"type": "string",
"format": "uuid",
"title": "Project Id"
},
"name": "project_id",
"in": "path"
},
{
"required": true,
"schema": {
"type": "number",
"minimum": 0.0,
"title": "Max Inactivity Seconds"
},
"name": "max_inactivity_seconds",
"in": "query"
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetProjectInactivityResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/v2/clusters": {
"get": {
"tags": [
Expand Down Expand Up @@ -2011,6 +2064,24 @@
],
"title": "Wallet Info",
"description": "contains information about the wallet used to bill the running service"
},
"pricing_info": {
"allOf": [
{
"$ref": "#/components/schemas/PricingInfo"
}
],
"title": "Pricing Info",
"description": "contains pricing information (ex. pricing plan and unit ids)"
},
"hardware_info": {
"allOf": [
{
"$ref": "#/components/schemas/HardwareInfo"
}
],
"title": "Hardware Info",
"description": "contains harware information (ex. aws_ec2_instances)"
}
},
"type": "object",
Expand Down Expand Up @@ -2055,9 +2126,32 @@
"wallet_info": {
"wallet_id": 1,
"wallet_name": "My Wallet"
},
"pricing_info": {
"pricing_plan_id": 1,
"pricing_unit_id": 1,
"pricing_unit_cost_id": 1
},
"hardware_info": {
"aws_ec2_instances": [
"c6a.4xlarge"
]
}
}
},
"GetProjectInactivityResponse": {
"properties": {
"is_inactive": {
"type": "boolean",
"title": "Is Inactive"
}
},
"type": "object",
"required": [
"is_inactive"
],
"title": "GetProjectInactivityResponse"
},
"HTTPValidationError": {
"properties": {
"errors": {
Expand All @@ -2071,6 +2165,22 @@
"type": "object",
"title": "HTTPValidationError"
},
"HardwareInfo": {
"properties": {
"aws_ec2_instances": {
"items": {
"type": "string"
},
"type": "array",
"title": "Aws Ec2 Instances"
}
},
"type": "object",
"required": [
"aws_ec2_instances"
],
"title": "HardwareInfo"
},
"HealthCheckGet": {
"properties": {
"timestamp": {
Expand Down Expand Up @@ -2324,6 +2434,35 @@
],
"title": "PipelineDetails"
},
"PricingInfo": {
"properties": {
"pricing_plan_id": {
"type": "integer",
"exclusiveMinimum": true,
"title": "Pricing Plan Id",
"minimum": 0
},
"pricing_unit_id": {
"type": "integer",
"exclusiveMinimum": true,
"title": "Pricing Unit Id",
"minimum": 0
},
"pricing_unit_cost_id": {
"type": "integer",
"exclusiveMinimum": true,
"title": "Pricing Unit Cost Id",
"minimum": 0
}
},
"type": "object",
"required": [
"pricing_plan_id",
"pricing_unit_id",
"pricing_unit_cost_id"
],
"title": "PricingInfo"
},
"ResourceValue": {
"properties": {
"limit": {
Expand Down
Loading

0 comments on commit ef08908

Please sign in to comment.