Skip to content

Commit 19b4fbf

Browse files
authored
♻️ gc as a service (preparation) (ITISFoundation#2828)
1 parent 137234a commit 19b4fbf

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+584
-418
lines changed

.codeclimate.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ plugins:
5656
channel: "eslint-6"
5757
config:
5858
extensions:
59-
- .js
59+
- .js
6060

6161
exclude_patterns:
6262
- "config/"

packages/service-library/src/servicelib/aiohttp/application_setup.py

Lines changed: 123 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55
from datetime import datetime
66
from distutils.util import strtobool
77
from enum import Enum
8-
from typing import Any, Callable, Dict, List, Optional, Protocol
8+
from typing import Any, Callable, Dict, List, Optional, Protocol, Tuple
99

1010
from aiohttp import web
1111

12-
from .application_keys import APP_CONFIG_KEY
12+
from .application_keys import APP_CONFIG_KEY, APP_SETTINGS_KEY
1313

1414
log = logging.getLogger(__name__)
1515

16-
APP_SETUP_KEY = f"{__name__ }.setup"
16+
APP_SETUP_COMPLETED_KEY = f"{__name__ }.setup"
1717

1818

1919
class _SetupFunc(Protocol):
@@ -23,44 +23,101 @@ def __call__(self, app: web.Application, *args: Any, **kwds: Any) -> bool:
2323
...
2424

2525

26+
class _ApplicationSettings(Protocol):
27+
def is_enabled(self, field_name: str) -> bool:
28+
...
29+
30+
2631
class ModuleCategory(Enum):
2732
SYSTEM = 0
2833
ADDON = 1
2934

3035

36+
# ERRORS ------------------------------------------------------------------
37+
38+
3139
class SkipModuleSetup(Exception):
3240
def __init__(self, *, reason) -> None:
3341
self.reason = reason
3442
super().__init__(reason)
3543

3644

3745
class ApplicationSetupError(Exception):
38-
pass
46+
...
3947

4048

4149
class DependencyError(ApplicationSetupError):
42-
pass
50+
...
51+
52+
53+
# HELPERS ------------------------------------------------------------------
54+
55+
56+
def _is_addon_enabled_from_config(
57+
cfg: Dict[str, Any], dotted_section: str, section
58+
) -> bool:
59+
try:
60+
parts: List[str] = dotted_section.split(".")
61+
# navigates app_config (cfg) searching for section
62+
searched_config = deepcopy(cfg)
63+
for part in parts:
64+
if section and part == "enabled":
65+
# if section exists, no need to explicitly enable it
66+
return strtobool(f"{searched_config.get(part, True)}")
67+
searched_config = searched_config[part]
4368

69+
except KeyError as ee:
70+
raise ApplicationSetupError(
71+
f"Cannot find required option '{dotted_section}' in app config's section '{ee}'"
72+
) from ee
73+
else:
74+
assert isinstance(searched_config, bool) # nosec
75+
return searched_config
76+
77+
78+
def _get_app_settings_and_field_name(
79+
app: web.Application,
80+
arg_module_name: str,
81+
arg_settings_name: Optional[str],
82+
setup_func_name: str,
83+
logger: logging.Logger,
84+
) -> Tuple[Optional[_ApplicationSettings], Optional[str]]:
85+
86+
app_settings: Optional[_ApplicationSettings] = app.get(APP_SETTINGS_KEY)
87+
settings_field_name = arg_settings_name
88+
89+
if app_settings:
90+
91+
if not settings_field_name:
92+
# FIXME: hard-coded WEBSERVER_ temporary
93+
settings_field_name = f"WEBSERVER_{arg_module_name.split('.')[-1].upper()}"
94+
95+
logger.debug("Checking addon's %s ", f"{settings_field_name=}")
96+
97+
if not hasattr(app_settings, settings_field_name):
98+
raise ValueError(
99+
f"Invalid option {arg_settings_name=} in module's setup {setup_func_name}. "
100+
f"It must be a field in {app_settings.__class__}"
101+
)
44102

45-
def _is_app_module_enabled(cfg: Dict, parts: List[str], section) -> bool:
46-
# navigates app_config (cfg) searching for section
47-
searched_config = deepcopy(cfg)
48-
for part in parts:
49-
if section and part == "enabled":
50-
# if section exists, no need to explicitly enable it
51-
return strtobool(f"{searched_config.get(part, True)}")
52-
searched_config = searched_config[part]
53-
assert isinstance(searched_config, bool) # nosec
54-
return searched_config
103+
return app_settings, settings_field_name
104+
105+
106+
# PUBLIC API ------------------------------------------------------------------
107+
108+
109+
def is_setup_completed(module_name: str, app: web.Application) -> bool:
110+
return module_name in app[APP_SETUP_COMPLETED_KEY]
55111

56112

57113
def app_module_setup(
58114
module_name: str,
59115
category: ModuleCategory,
60116
*,
61117
depends: Optional[List[str]] = None,
62-
config_section: str = None,
63-
config_enabled: str = None,
118+
config_section: Optional[str] = None,
119+
config_enabled: Optional[str] = None,
120+
settings_name: Optional[str] = None,
64121
logger: logging.Logger = log,
65122
) -> Callable:
66123
"""Decorator that marks a function as 'a setup function' for a given module in an application
@@ -77,6 +134,7 @@ def app_module_setup(
77134
:param depends: list of module_names that must be called first, defaults to None
78135
:param config_section: explicit configuration section, defaults to None (i.e. the name of the module, or last entry of the name if dotted)
79136
:param config_enabled: option in config to enable, defaults to None which is '$(module-section).enabled' (config_section and config_enabled are mutually exclusive)
137+
:param settings_name: field name in the app's settings that corresponds to this module. Defaults to the name of the module with app prefix.
80138
:raises DependencyError
81139
:raises ApplicationSetupError
82140
:return: True if setup was completed or False if setup was skipped
@@ -111,7 +169,7 @@ def _decorate(setup_func: _SetupFunc):
111169
logger.warning("Rename '%s' to contain 'setup'", setup_func.__name__)
112170

113171
# metadata info
114-
def setup_metadata() -> Dict:
172+
def setup_metadata() -> Dict[str, Any]:
115173
return {
116174
"module_name": module_name,
117175
"dependencies": depends,
@@ -132,56 +190,74 @@ def _wrapper(app: web.Application, *args, **kargs) -> bool:
132190
f"{depends}",
133191
)
134192

135-
if APP_SETUP_KEY not in app:
136-
app[APP_SETUP_KEY] = []
193+
if APP_SETUP_COMPLETED_KEY not in app:
194+
app[APP_SETUP_COMPLETED_KEY] = []
137195

138196
if category == ModuleCategory.ADDON:
139197
# NOTE: ONLY addons can be enabled/disabled
140-
# TODO: sometimes section is optional, check in config schema
141-
cfg = app[APP_CONFIG_KEY]
142-
143-
try:
144-
is_enabled = _is_app_module_enabled(
145-
cfg, config_enabled.split("."), section
146-
)
147-
except KeyError as ee:
148-
raise ApplicationSetupError(
149-
f"Cannot find required option '{config_enabled}' in app config's section '{ee}'"
150-
) from ee
151198

199+
# TODO: cfg will be fully replaced by app_settings section below
200+
cfg = app[APP_CONFIG_KEY]
201+
is_enabled = _is_addon_enabled_from_config(cfg, config_enabled, section)
152202
if not is_enabled:
153203
logger.info(
154204
"Skipping '%s' setup. Explicitly disabled in config",
155205
module_name,
156206
)
157207
return False
158208

209+
# NOTE: if not disabled by config, it can be disabled by settings (tmp while legacy maintained)
210+
app_settings, module_settings_name = _get_app_settings_and_field_name(
211+
app,
212+
module_name,
213+
settings_name,
214+
setup_func.__name__,
215+
logger,
216+
)
217+
218+
if (
219+
app_settings
220+
and module_settings_name
221+
and not app_settings.is_enabled(module_settings_name)
222+
):
223+
logger.info(
224+
"Skipping setup %s. %s disabled in settings",
225+
f"{module_name=}",
226+
f"{module_settings_name=}",
227+
)
228+
return False
229+
159230
if depends:
231+
# TODO: no need to enforce. Use to deduce order instead.
160232
uninitialized = [
161-
dep for dep in depends if dep not in app[APP_SETUP_KEY]
233+
dep for dep in depends if not is_setup_completed(dep, app)
162234
]
163235
if uninitialized:
164-
msg = f"Cannot setup app module '{module_name}' because the following dependencies are still uninitialized: {uninitialized}"
165-
log.error(msg)
166-
raise DependencyError(msg)
167-
168-
if module_name in app[APP_SETUP_KEY]:
169-
msg = f"'{module_name}' was already initialized in {app}. Setup can only be executed once per app."
170-
logger.error(msg)
171-
raise ApplicationSetupError(msg)
236+
raise DependencyError(
237+
f"Cannot setup app module '{module_name}' because the "
238+
f"following dependencies are still uninitialized: {uninitialized}"
239+
)
172240

173241
# execution of setup
174242
try:
243+
if is_setup_completed(module_name, app):
244+
raise SkipModuleSetup(
245+
reason=f"'{module_name}' was already initialized in {app}."
246+
" Setup can only be executed once per app."
247+
)
248+
175249
completed = setup_func(app, *args, **kargs)
176250

177251
# post-setup
178252
if completed is None:
179253
completed = True
180254

181-
if completed:
182-
app[APP_SETUP_KEY].append(module_name)
255+
if completed: # registers completed setup
256+
app[APP_SETUP_COMPLETED_KEY].append(module_name)
183257
else:
184-
raise SkipModuleSetup(reason="Undefined")
258+
raise SkipModuleSetup(
259+
reason="Undefined (setup function returned false)"
260+
)
185261

186262
except SkipModuleSetup as exc:
187263
logger.warning("Skipping '%s' setup: %s", module_name, exc.reason)
@@ -197,17 +273,18 @@ def _wrapper(app: web.Application, *args, **kargs) -> bool:
197273
return completed
198274

199275
_wrapper.metadata = setup_metadata
200-
_wrapper.MARK = "setup"
276+
_wrapper.mark_as_simcore_servicelib_setup_func = True
201277

202278
return _wrapper
203279

204280
return _decorate
205281

206282

207-
def is_setup_function(fun):
283+
def is_setup_function(fun: Callable) -> bool:
284+
# TODO: use _SetupFunc protocol to check in runtime
208285
return (
209286
inspect.isfunction(fun)
210-
and getattr(fun, "MARK", None) == "setup"
287+
and hasattr(fun, "mark_as_simcore_servicelib_setup_func")
211288
and any(
212289
param.annotation == web.Application
213290
for _, param in inspect.signature(fun).parameters.items()

packages/service-library/tests/aiohttp/test_application_setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
from aiohttp import web
1010
from servicelib.aiohttp.application_keys import APP_CONFIG_KEY
1111
from servicelib.aiohttp.application_setup import (
12-
APP_SETUP_KEY,
1312
DependencyError,
1413
ModuleCategory,
1514
SkipModuleSetup,
1615
app_module_setup,
16+
is_setup_completed,
1717
)
1818

1919
log = Mock()
@@ -91,7 +91,7 @@ def test_marked_setup(app_config, app):
9191
assert setup_foo(app, 1)
9292

9393
assert setup_foo.metadata()["module_name"] == "package.foo"
94-
assert setup_foo.metadata()["module_name"] in app[APP_SETUP_KEY]
94+
assert is_setup_completed(setup_foo.metadata()["module_name"], app)
9595

9696
app_config["foo"]["enabled"] = False
9797
assert not setup_foo(app, 2)

services/docker-compose.devel.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,19 @@ services:
6060

6161
webserver:
6262
volumes:
63+
&webserver-volumes
6364
- ./web/server:/devel/services/web/server
6465
- ./web/client/source-output:/devel/services/web/client
6566
- ../packages:/devel/packages
6667
environment:
68+
&webserver-environment
6769
- SC_BOOT_MODE=debug-ptvsd
6870
- WEBSERVER_RESOURCES_DELETION_TIMEOUT_SECONDS=15
6971
- WEBSERVER_LOGLEVEL=${LOG_LEVEL:-DEBUG}
7072

7173
dask-sidecar:
72-
volumes: &dev-dask-sidecar-volumes
74+
volumes:
75+
&dev-dask-sidecar-volumes
7376
- ./dask-sidecar:/devel/services/dask-sidecar
7477
- ../packages:/devel/packages
7578
- ${ETC_HOSTNAME:-/etc/hostname}:/home/scu/hostname:ro

services/docker-compose.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -358,8 +358,7 @@ services:
358358
- default
359359

360360
postgres:
361-
image: "postgres:10.11@sha256:2aef165ab4f30fbb109e88959271d8b57489790ea13a77d27\
362-
c02d8adb8feb20f"
361+
image: "postgres:10.11@sha256:2aef165ab4f30fbb109e88959271d8b57489790ea13a77d27c02d8adb8feb20f"
363362
init: true
364363
hostname: "{{.Node.Hostname}}-{{.Service.Name}}-{{.Task.Slot}}"
365364
environment:
@@ -414,8 +413,7 @@ services:
414413
]
415414

416415
redis:
417-
image: "redis:5.0.9-alpine@sha256:b011c1ca7fa97ed92d6c5995e5dd752dc37fe157c1b60\
418-
ce96a6e35701851dabc"
416+
image: "redis:5.0.9-alpine@sha256:b011c1ca7fa97ed92d6c5995e5dd752dc37fe157c1b60ce96a6e35701851dabc"
419417
init: true
420418
hostname: "{{.Node.Hostname}}-{{.Service.Name}}-{{.Task.Slot}}"
421419
networks:

services/web/server/src/simcore_service_webserver/application.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@
2020
from .director_v2 import setup_director_v2
2121
from .email import setup_email
2222
from .exporter.module_setup import setup_exporter
23+
from .garbage_collector import setup_garbage_collector
2324
from .groups import setup_groups
2425
from .login.module_setup import setup_login
2526
from .meta_modeling import setup_meta_modeling
2627
from .products import setup_products
2728
from .projects.module_setup import setup_projects
2829
from .publications import setup_publications
30+
from .redis import setup_redis
2931
from .remote_debug import setup_remote_debugging
3032
from .resource_manager.module_setup import setup_resource_manager
3133
from .rest import setup_rest
@@ -75,9 +77,17 @@ def create_application(config: Dict[str, Any]) -> web.Application:
7577
setup_computation(app)
7678
setup_socketio(app)
7779
setup_login(app)
80+
81+
# interaction with other backend services
7882
setup_director(app)
7983
setup_director_v2(app)
8084
setup_storage(app)
85+
setup_catalog(app)
86+
setup_redis(app)
87+
88+
# resource management
89+
setup_resource_manager(app)
90+
setup_garbage_collector(app)
8191

8292
# users
8393
setup_users(app)
@@ -91,9 +101,7 @@ def create_application(config: Dict[str, Any]) -> web.Application:
91101

92102
# TODO: classify
93103
setup_activity(app)
94-
setup_resource_manager(app)
95104
setup_tags(app)
96-
setup_catalog(app)
97105
setup_publications(app)
98106
setup_products(app)
99107
setup_studies_access(app)

0 commit comments

Comments
 (0)