Skip to content

Commit aaf1079

Browse files
authored
🐛 Mitigates "Projects not being listed on osparc-staging.io" (#7836)
1 parent 186af88 commit aaf1079

16 files changed

+101
-87
lines changed

services/web/server/src/simcore_service_webserver/catalog/_catalog_rest_client_service.py

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import logging
44
import urllib.parse
5-
from collections.abc import Callable, Iterator
5+
from collections.abc import Iterator
66
from contextlib import contextmanager
77
from typing import Any, Final
88

@@ -30,20 +30,11 @@
3030
from yarl import URL
3131

3232
from .._meta import api_version_prefix
33+
from ._models import ServiceKeyVersionDict
3334
from .settings import CatalogSettings, get_plugin_settings
3435

3536
_logger = logging.getLogger(__name__)
3637

37-
# Cache settings
38-
_SECOND = 1 # in seconds
39-
_MINUTE = 60 * _SECOND
40-
_CACHE_TTL: Final = 1 * _MINUTE
41-
42-
43-
def _create_service_cache_key(_f: Callable[..., Any], *_args, **kw):
44-
assert len(_args) == 1, f"Expected only app, got {_args}" # nosec
45-
return f"get_service_{kw['user_id']}_{kw['service_key']}_{kw['service_version']}_{kw['product_name']}"
46-
4738

4839
@contextmanager
4940
def _handle_client_exceptions(app: web.Application) -> Iterator[ClientSession]:
@@ -96,10 +87,27 @@ def to_backend_service(rel_url: URL, origin: URL, version_prefix: str) -> URL:
9687
return origin.with_path(new_path).with_query(rel_url.query)
9788

9889

90+
# Cache settings for services rest API
91+
_SECOND = 1 # in seconds
92+
_MINUTE = 60 * _SECOND
93+
_CACHE_TTL: Final = 1 * _MINUTE
94+
95+
96+
@cached(
97+
ttl=_CACHE_TTL,
98+
key_builder=lambda _f, *_args, **kw: f"get_services_for_user_in_product_{kw['user_id']}_{kw['product_name']}",
99+
cache=Cache.MEMORY,
100+
)
99101
async def get_services_for_user_in_product(
100-
app: web.Application, user_id: UserID, product_name: str, *, only_key_versions: bool
101-
) -> list[dict]:
102+
app: web.Application, *, user_id: UserID, product_name: str
103+
) -> list[ServiceKeyVersionDict]:
104+
"""
105+
DEPRECATED: see instead RPC interface.
106+
SEE https://github.com/ITISFoundation/osparc-simcore/issues/7838
107+
"""
102108
settings: CatalogSettings = get_plugin_settings(app)
109+
only_key_versions = True
110+
103111
url = (URL(settings.api_base_url) / "services").with_query(
104112
{"user_id": user_id, "details": f"{not only_key_versions}"}
105113
)
@@ -115,13 +123,18 @@ async def get_services_for_user_in_product(
115123
user_id,
116124
)
117125
return []
118-
body: list[dict] = await response.json()
119-
return body
126+
services: list[dict] = await response.json()
127+
128+
# This reduces the size cached in the memory
129+
return [
130+
ServiceKeyVersionDict(key=service["key"], version=service["version"])
131+
for service in services
132+
]
120133

121134

122135
@cached(
123136
ttl=_CACHE_TTL,
124-
key_builder=_create_service_cache_key,
137+
key_builder=lambda _f, *_args, **kw: f"get_service_{kw['user_id']}_{kw['service_key']}_{kw['service_version']}_{kw['product_name']}",
125138
cache=Cache.MEMORY,
126139
# SEE https://github.com/ITISFoundation/osparc-simcore/pull/7802
127140
)
@@ -133,6 +146,10 @@ async def get_service(
133146
service_version: ServiceVersion,
134147
product_name: ProductName,
135148
) -> dict[str, Any]:
149+
"""
150+
DEPRECATED: see instead RPC interface.
151+
SEE https://github.com/ITISFoundation/osparc-simcore/issues/7838
152+
"""
136153
settings: CatalogSettings = get_plugin_settings(app)
137154
url = URL(
138155
f"{settings.api_base_url}/services/{urllib.parse.quote_plus(service_key)}/{service_version}",
@@ -144,8 +161,8 @@ async def get_service(
144161
url, headers={X_PRODUCT_NAME_HEADER: product_name}
145162
) as response:
146163
response.raise_for_status()
147-
body: dict[str, Any] = await response.json()
148-
return body
164+
service: dict[str, Any] = await response.json()
165+
return service
149166

150167

151168
async def get_service_resources(

services/web/server/src/simcore_service_webserver/catalog/_controller_rest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
from ..utils_aiohttp import envelope_json_response
3838
from . import _catalog_rest_client_service, _service
3939
from ._controller_rest_exceptions import (
40-
DefaultPricingUnitForServiceNotFoundError,
4140
handle_plugin_requests_exceptions,
4241
)
4342
from ._controller_rest_schemas import (
@@ -50,6 +49,7 @@
5049
ServiceTagPathParams,
5150
ToServiceInputsQueryParams,
5251
)
52+
from .errors import DefaultPricingUnitForServiceNotFoundError
5353

5454
_logger = logging.getLogger(__name__)
5555

services/web/server/src/simcore_service_webserver/catalog/_controller_rest_exceptions.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -105,19 +105,12 @@ async def _handler_catalog_client_errors(
105105
}
106106

107107

108-
_exceptions_handlers_map: ExceptionHandlersMap = {
108+
catalog_exceptions_handlers_map: ExceptionHandlersMap = {
109109
CatalogResponseError: _handler_catalog_client_errors,
110110
CatalogConnectionError: _handler_catalog_client_errors,
111111
}
112-
_exceptions_handlers_map.update(to_exceptions_handlers_map(_TO_HTTP_ERROR_MAP))
112+
catalog_exceptions_handlers_map.update(to_exceptions_handlers_map(_TO_HTTP_ERROR_MAP))
113113

114114
handle_plugin_requests_exceptions = exception_handling_decorator(
115-
_exceptions_handlers_map
116-
)
117-
118-
119-
__all__: tuple[str, ...] = (
120-
"CatalogForbiddenError",
121-
"CatalogItemNotFoundError",
122-
"DefaultPricingUnitForServiceNotFoundError",
115+
catalog_exceptions_handlers_map
123116
)
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
# NOTE: missing. @bisgaard-itis will follow up here
1+
from typing import TypedDict
2+
3+
4+
class ServiceKeyVersionDict(TypedDict):
5+
key: str
6+
version: str

services/web/server/src/simcore_service_webserver/catalog/catalog_service.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
is_catalog_service_responsive,
77
to_backend_service,
88
)
9+
from ._models import ServiceKeyVersionDict
910
from ._service import batch_get_my_services
1011

1112
__all__: tuple[str, ...] = (
@@ -16,5 +17,6 @@
1617
"get_services_for_user_in_product",
1718
"is_catalog_service_responsive",
1819
"to_backend_service",
20+
"ServiceKeyVersionDict",
1921
)
2022
# nopycln: file

services/web/server/src/simcore_service_webserver/projects/_controller/_rest_exceptions.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
CatalogItemNotFoundError,
99
CatalogNotAvailableError,
1010
)
11+
from simcore_service_webserver.exception_handling._base import ExceptionHandlersMap
1112

13+
from ...catalog._controller_rest_exceptions import catalog_exceptions_handlers_map
1214
from ...conversations.errors import (
1315
ConversationErrorNotFoundError,
1416
ConversationMessageErrorNotFoundError,
@@ -239,6 +241,9 @@ def _assert_duplicate():
239241
}
240242

241243

242-
handle_plugin_requests_exceptions = exception_handling_decorator(
243-
to_exceptions_handlers_map(_TO_HTTP_ERROR_MAP)
244-
)
244+
_handlers: ExceptionHandlersMap = {
245+
**catalog_exceptions_handlers_map,
246+
**to_exceptions_handlers_map(_TO_HTTP_ERROR_MAP),
247+
}
248+
249+
handle_plugin_requests_exceptions = exception_handling_decorator(_handlers)

services/web/server/src/simcore_service_webserver/projects/_controller/projects_rest.py

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
from .. import _crud_api_create, _crud_api_read, _projects_service
4040
from .._permalink_service import update_or_pop_permalink_in_project
4141
from ..models import ProjectDict
42-
from ..utils import get_project_unavailable_services, project_uses_available_services
42+
from ..utils import are_project_services_available, get_project_unavailable_services
4343
from . import _rest_utils
4444
from ._rest_exceptions import handle_plugin_requests_exceptions
4545
from ._rest_schemas import (
@@ -55,12 +55,6 @@
5555
ProjectsSearchQueryParams,
5656
)
5757

58-
# When the user requests a project with a repo, the working copy might differ from
59-
# the repo project. A middleware in the meta module (if active) will resolve
60-
# the working copy and redirect to the appropriate project entrypoint. Nonetheless, the
61-
# response needs to refer to the uuid of the request and this is passed through this request key
62-
RQ_REQUESTED_REPO_PROJECT_UUID_KEY = f"{__name__}.RQT_REQUESTED_REPO_PROJECT_UUID_KEY"
63-
6458
_logger = logging.getLogger(__name__)
6559

6660

@@ -277,10 +271,8 @@ async def get_project(request: web.Request):
277271
req_ctx = RequestContext.model_validate(request)
278272
path_params = parse_request_path_parameters_as(ProjectPathParams, request)
279273

280-
user_available_services: list[dict] = (
281-
await catalog_service.get_services_for_user_in_product(
282-
request.app, req_ctx.user_id, req_ctx.product_name, only_key_versions=True
283-
)
274+
user_available_services = await catalog_service.get_services_for_user_in_product(
275+
request.app, user_id=req_ctx.user_id, product_name=req_ctx.product_name
284276
)
285277

286278
project = await _projects_service.get_project_for_user(
@@ -290,7 +282,8 @@ async def get_project(request: web.Request):
290282
include_state=True,
291283
include_trashed_by_primary_gid=True,
292284
)
293-
if not await project_uses_available_services(project, user_available_services):
285+
286+
if not are_project_services_available(project, user_available_services):
294287
unavilable_services = get_project_unavailable_services(
295288
project, user_available_services
296289
)
@@ -305,9 +298,6 @@ async def get_project(request: web.Request):
305298
)
306299
)
307300

308-
if new_uuid := request.get(RQ_REQUESTED_REPO_PROJECT_UUID_KEY):
309-
project["uuid"] = new_uuid
310-
311301
# Adds permalink
312302
await update_or_pop_permalink_in_project(request, project)
313303

services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,8 @@ async def list_projects( # pylint: disable=too-many-arguments
120120
) -> tuple[list[ProjectDict], int]:
121121
db = ProjectDBAPI.get_from_app_context(app)
122122

123-
user_available_services: list[dict] = (
124-
await catalog_service.get_services_for_user_in_product(
125-
app, user_id, product_name, only_key_versions=True
126-
)
123+
user_available_services = await catalog_service.get_services_for_user_in_product(
124+
app, user_id=user_id, product_name=product_name
127125
)
128126

129127
workspace_is_private = True
@@ -204,10 +202,8 @@ async def list_projects_full_depth(
204202
) -> tuple[list[ProjectDict], int]:
205203
db = ProjectDBAPI.get_from_app_context(app)
206204

207-
user_available_services: list[dict] = (
208-
await catalog_service.get_services_for_user_in_product(
209-
app, user_id, product_name, only_key_versions=True
210-
)
205+
user_available_services = await catalog_service.get_services_for_user_in_product(
206+
app, user_id=user_id, product_name=product_name
211207
)
212208

213209
db_projects, db_project_types, total_number_projects = await db.list_projects_dicts(

services/web/server/src/simcore_service_webserver/projects/_projects_repository_legacy_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
ProjectNotFoundError,
3333
)
3434
from .models import ProjectDict
35-
from .utils import find_changed_node_keys, project_uses_available_services
35+
from .utils import are_project_services_available, find_changed_node_keys
3636

3737
logger = logging.getLogger(__name__)
3838

@@ -222,7 +222,7 @@ async def _execute_without_permission_check(
222222
filter_by_services is not None
223223
# This checks only old projects that are not in the projects_to_products table.
224224
and row[projects_to_products.c.product_name] is None
225-
and not await project_uses_available_services(prj, filter_by_services)
225+
and not are_project_services_available(prj, filter_by_services)
226226
):
227227
logger.warning(
228228
"Project %s will not be listed for user %s since it has no access rights"

services/web/server/src/simcore_service_webserver/projects/utils.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -189,20 +189,21 @@ def is_graph_equal(
189189
return True
190190

191191

192-
async def project_uses_available_services(
193-
project: dict[str, Any], available_services: list[dict[str, Any]]
192+
def are_project_services_available(
193+
project: dict[str, Any], available_services: list[dict[str, str]]
194194
) -> bool:
195195
if not project["workbench"]:
196196
# empty project
197197
return True
198-
# get project services
199-
needed_services: set[tuple[str, str]] = {
200-
(s["key"], s["version"]) for _, s in project["workbench"].items()
198+
199+
# list services in project
200+
needed_services = {
201+
(srv["key"], srv["version"]) for _, srv in project["workbench"].items()
201202
}
202203

203-
# get available services
204-
available_services_set: set[tuple[str, str]] = {
205-
(s["key"], s["version"]) for s in available_services
204+
# list available services
205+
available_services_set = {
206+
(srv["key"], srv["version"]) for srv in available_services
206207
}
207208

208209
return needed_services.issubset(available_services_set)

0 commit comments

Comments
 (0)