Skip to content

Commit 51d6c06

Browse files
GitHKAndrei Neagu
andauthored
♻️ lifespans cleanup (#7496)
Co-authored-by: Andrei Neagu <[email protected]>
1 parent ab69603 commit 51d6c06

File tree

27 files changed

+210
-175
lines changed

27 files changed

+210
-175
lines changed

packages/service-library/src/servicelib/fastapi/docker.py

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,51 +11,55 @@
1111
from fastapi import FastAPI
1212
from fastapi_lifespan_manager import State
1313
from pydantic import NonNegativeInt
14-
from servicelib.fastapi.lifespan_utils import LifespanGenerator
1514
from settings_library.docker_api_proxy import DockerApiProxysettings
1615

1716
_logger = logging.getLogger(__name__)
1817

1918
_DEFAULT_DOCKER_API_PROXY_HEALTH_TIMEOUT: Final[NonNegativeInt] = 5
2019

2120

22-
def get_lifespan_remote_docker_client(
23-
settings: DockerApiProxysettings,
24-
) -> LifespanGenerator:
25-
async def _(app: FastAPI) -> AsyncIterator[State]:
21+
_DOCKER_API_PROXY_SETTINGS: Final[str] = "docker_api_proxy_settings"
2622

27-
session: ClientSession | None = None
28-
if settings.DOCKER_API_PROXY_USER and settings.DOCKER_API_PROXY_PASSWORD:
29-
session = ClientSession(
30-
auth=aiohttp.BasicAuth(
31-
login=settings.DOCKER_API_PROXY_USER,
32-
password=settings.DOCKER_API_PROXY_PASSWORD.get_secret_value(),
33-
)
23+
24+
def create_remote_docker_client_input_state(settings: DockerApiProxysettings) -> State:
25+
return {_DOCKER_API_PROXY_SETTINGS: settings}
26+
27+
28+
async def remote_docker_client_lifespan(
29+
app: FastAPI, state: State
30+
) -> AsyncIterator[State]:
31+
settings: DockerApiProxysettings = state[_DOCKER_API_PROXY_SETTINGS]
32+
33+
session: ClientSession | None = None
34+
if settings.DOCKER_API_PROXY_USER and settings.DOCKER_API_PROXY_PASSWORD:
35+
session = ClientSession(
36+
auth=aiohttp.BasicAuth(
37+
login=settings.DOCKER_API_PROXY_USER,
38+
password=settings.DOCKER_API_PROXY_PASSWORD.get_secret_value(),
3439
)
40+
)
3541

36-
async with AsyncExitStack() as exit_stack:
37-
if settings.DOCKER_API_PROXY_USER and settings.DOCKER_API_PROXY_PASSWORD:
38-
await exit_stack.enter_async_context(
39-
ClientSession(
40-
auth=aiohttp.BasicAuth(
41-
login=settings.DOCKER_API_PROXY_USER,
42-
password=settings.DOCKER_API_PROXY_PASSWORD.get_secret_value(),
43-
)
42+
async with AsyncExitStack() as exit_stack:
43+
if settings.DOCKER_API_PROXY_USER and settings.DOCKER_API_PROXY_PASSWORD:
44+
await exit_stack.enter_async_context(
45+
ClientSession(
46+
auth=aiohttp.BasicAuth(
47+
login=settings.DOCKER_API_PROXY_USER,
48+
password=settings.DOCKER_API_PROXY_PASSWORD.get_secret_value(),
4449
)
4550
)
46-
47-
client = await exit_stack.enter_async_context(
48-
aiodocker.Docker(url=settings.base_url, session=session)
4951
)
5052

51-
app.state.remote_docker_client = client
53+
client = await exit_stack.enter_async_context(
54+
aiodocker.Docker(url=settings.base_url, session=session)
55+
)
5256

53-
await wait_till_docker_api_proxy_is_responsive(app)
57+
app.state.remote_docker_client = client
5458

55-
# NOTE this has to be inside exit_stack scope
56-
yield {}
59+
await wait_till_docker_api_proxy_is_responsive(app)
5760

58-
return _
61+
# NOTE this has to be inside exit_stack scope
62+
yield {}
5963

6064

6165
@tenacity.retry(
Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
from collections.abc import AsyncIterator
2-
from typing import Protocol
3-
41
from common_library.errors_classes import OsparcErrorMixin
5-
from fastapi import FastAPI
6-
from fastapi_lifespan_manager import LifespanManager, State
72

83

94
class LifespanError(OsparcErrorMixin, RuntimeError): ...
@@ -15,17 +10,3 @@ class LifespanOnStartupError(LifespanError):
1510

1611
class LifespanOnShutdownError(LifespanError):
1712
msg_template = "Failed during shutdown of {module}"
18-
19-
20-
class LifespanGenerator(Protocol):
21-
def __call__(self, app: FastAPI) -> AsyncIterator["State"]: ...
22-
23-
24-
def combine_lifespans(*generators: LifespanGenerator) -> LifespanManager:
25-
26-
manager = LifespanManager()
27-
28-
for generator in generators:
29-
manager.add(generator)
30-
31-
return manager

packages/service-library/src/servicelib/fastapi/postgres_lifespan.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from collections.abc import AsyncIterator
44
from enum import Enum
55

6+
from fastapi import FastAPI
67
from fastapi_lifespan_manager import State
78
from servicelib.logging_utils import log_catch, log_context
89
from settings_library.postgres import PostgresSettings
@@ -23,11 +24,11 @@ class PostgresConfigurationError(LifespanOnStartupError):
2324
msg_template = "Invalid postgres settings [={settings}] on startup. Note that postgres cannot be disabled using settings"
2425

2526

26-
def create_input_state(settings: PostgresSettings) -> State:
27+
def create_postgres_database_input_state(settings: PostgresSettings) -> State:
2728
return {PostgresLifespanState.POSTGRES_SETTINGS: settings}
2829

2930

30-
async def postgres_database_lifespan(_, state: State) -> AsyncIterator[State]:
31+
async def postgres_database_lifespan(_: FastAPI, state: State) -> AsyncIterator[State]:
3132

3233
with log_context(_logger, logging.INFO, f"{__name__}"):
3334

packages/service-library/src/servicelib/fastapi/prometheus_instrumentation.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# pylint: disable=protected-access
22

3-
43
from collections.abc import AsyncIterator
4+
from typing import Final
55

66
from fastapi import FastAPI
77
from fastapi_lifespan_manager import State
@@ -54,9 +54,23 @@ def _on_shutdown() -> None:
5454
return get_prometheus_instrumentator(app)
5555

5656

57-
async def lifespan_prometheus_instrumentation(app: FastAPI) -> AsyncIterator[State]:
57+
_PROMETHEUS_INSTRUMENTATION_ENABLED: Final[str] = "prometheus_instrumentation_enabled"
58+
59+
60+
def create_prometheus_instrumentationmain_input_state(*, enabled: bool) -> State:
61+
return {_PROMETHEUS_INSTRUMENTATION_ENABLED: enabled}
62+
63+
64+
async def prometheus_instrumentation_lifespan(
65+
app: FastAPI, state: State
66+
) -> AsyncIterator[State]:
5867
# NOTE: requires ``initialize_prometheus_instrumentation`` to be called before the
5968
# lifespan of the applicaiton runs, usually rigth after the ``FastAPI`` instance is created
60-
_startup(app)
69+
70+
instrumentaiton_enabled = state.get(_PROMETHEUS_INSTRUMENTATION_ENABLED, False)
71+
if instrumentaiton_enabled:
72+
73+
_startup(app)
6174
yield {}
62-
_shutdown(app)
75+
if instrumentaiton_enabled:
76+
_shutdown(app)

packages/service-library/tests/fastapi/test_lifespan_utils.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from servicelib.fastapi.lifespan_utils import (
1919
LifespanOnShutdownError,
2020
LifespanOnStartupError,
21-
combine_lifespans,
2221
)
2322

2423

@@ -35,7 +34,11 @@ async def cache_lifespan(app: FastAPI) -> AsyncIterator[State]:
3534
yield {}
3635
print("shutdown CACHE")
3736

38-
app = FastAPI(lifespan=combine_lifespans(database_lifespan, cache_lifespan))
37+
lifespan_manager = LifespanManager()
38+
lifespan_manager.add(database_lifespan)
39+
lifespan_manager.add(cache_lifespan)
40+
41+
app = FastAPI(lifespan=lifespan_manager)
3942

4043
capsys.readouterr()
4144

services/catalog/src/simcore_service_catalog/core/events.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
from fastapi import FastAPI
55
from fastapi_lifespan_manager import LifespanManager, State
66
from servicelib.fastapi.postgres_lifespan import (
7-
PostgresLifespanState,
7+
create_postgres_database_input_state,
88
)
99
from servicelib.fastapi.prometheus_instrumentation import (
10-
lifespan_prometheus_instrumentation,
10+
create_prometheus_instrumentationmain_input_state,
11+
prometheus_instrumentation_lifespan,
1112
)
1213

1314
from .._meta import APP_FINISHED_BANNER_MSG, APP_STARTED_BANNER_MSG
@@ -38,27 +39,21 @@ async def _banners_lifespan(_) -> AsyncIterator[State]:
3839
_flush_finished_banner()
3940

4041

41-
async def _main_lifespan(app: FastAPI) -> AsyncIterator[State]:
42+
async def _settings_lifespan(app: FastAPI) -> AsyncIterator[State]:
4243
settings: ApplicationSettings = app.state.settings
4344

4445
yield {
45-
PostgresLifespanState.POSTGRES_SETTINGS: settings.CATALOG_POSTGRES,
46-
"prometheus_instrumentation_enabled": settings.CATALOG_PROMETHEUS_INSTRUMENTATION_ENABLED,
46+
**create_postgres_database_input_state(settings.CATALOG_POSTGRES),
47+
**create_prometheus_instrumentationmain_input_state(
48+
enabled=settings.CATALOG_PROMETHEUS_INSTRUMENTATION_ENABLED
49+
),
4750
}
4851

4952

50-
async def _prometheus_instrumentation_lifespan(
51-
app: FastAPI, state: State
52-
) -> AsyncIterator[State]:
53-
if state.get("prometheus_instrumentation_enabled", False):
54-
async for prometheus_state in lifespan_prometheus_instrumentation(app):
55-
yield prometheus_state
56-
57-
58-
def create_app_lifespan():
53+
def create_app_lifespan() -> LifespanManager:
5954
# WARNING: order matters
6055
app_lifespan = LifespanManager()
61-
app_lifespan.add(_main_lifespan)
56+
app_lifespan.add(_settings_lifespan)
6257

6358
# - postgres
6459
app_lifespan.include(repository_lifespan_manager)
@@ -79,7 +74,7 @@ def create_app_lifespan():
7974
app_lifespan.add(background_task_lifespan)
8075

8176
# - prometheus instrumentation
82-
app_lifespan.add(_prometheus_instrumentation_lifespan)
77+
app_lifespan.add(prometheus_instrumentation_lifespan)
8378

8479
app_lifespan.add(_banners_lifespan)
8580

services/catalog/src/simcore_service_catalog/repository/events.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,6 @@
1313
_logger = logging.getLogger(__name__)
1414

1515

16-
repository_lifespan_manager = LifespanManager()
17-
repository_lifespan_manager.add(postgres_database_lifespan)
18-
19-
20-
@repository_lifespan_manager.add
2116
async def _database_lifespan(app: FastAPI, state: State) -> AsyncIterator[State]:
2217
app.state.engine = state[PostgresLifespanState.POSTGRES_ASYNC_ENGINE]
2318

@@ -26,3 +21,8 @@ async def _database_lifespan(app: FastAPI, state: State) -> AsyncIterator[State]
2621
app.state.default_product_name = await repo.get_default_product_name()
2722

2823
yield {}
24+
25+
26+
repository_lifespan_manager = LifespanManager()
27+
repository_lifespan_manager.add(postgres_database_lifespan)
28+
repository_lifespan_manager.add(_database_lifespan)

services/docker-api-proxy/tests/integration/conftest.py

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@
66

77
import aiodocker
88
import pytest
9-
from asgi_lifespan import LifespanManager
9+
from asgi_lifespan import LifespanManager as ASGILifespanManager
1010
from fastapi import FastAPI
11+
from fastapi_lifespan_manager import LifespanManager, State
1112
from pydantic import Field
1213
from pytest_simcore.helpers.monkeypatch_envs import EnvVarsDict, setenvs_from_dict
1314
from servicelib.fastapi.docker import (
14-
get_lifespan_remote_docker_client,
15+
create_remote_docker_client_input_state,
1516
get_remote_docker_client,
17+
remote_docker_client_lifespan,
1618
)
17-
from servicelib.fastapi.lifespan_utils import combine_lifespans
1819
from settings_library.application import BaseApplicationSettings
1920
from settings_library.docker_api_proxy import DockerApiProxysettings
2021

@@ -32,20 +33,29 @@ def pytest_configure(config):
3233
config.option.asyncio_mode = "auto"
3334

3435

35-
def _get_test_app() -> FastAPI:
36-
class ApplicationSetting(BaseApplicationSettings):
37-
DOCKER_API_PROXY: Annotated[
38-
DockerApiProxysettings,
39-
Field(json_schema_extra={"auto_default_from_env": True}),
40-
]
36+
class ApplicationSetting(BaseApplicationSettings):
37+
DOCKER_API_PROXY: Annotated[
38+
DockerApiProxysettings,
39+
Field(json_schema_extra={"auto_default_from_env": True}),
40+
]
41+
42+
43+
async def _settings_lifespan(app: FastAPI) -> AsyncIterator[State]:
44+
settings: ApplicationSetting = app.state.settings
45+
46+
yield {
47+
**create_remote_docker_client_input_state(settings.DOCKER_API_PROXY),
48+
}
4149

50+
51+
def _get_test_app() -> FastAPI:
4252
settings = ApplicationSetting.create_from_envs()
4353

44-
app = FastAPI(
45-
lifespan=combine_lifespans(
46-
get_lifespan_remote_docker_client(settings.DOCKER_API_PROXY)
47-
)
48-
)
54+
lifespan_manager = LifespanManager()
55+
lifespan_manager.add(_settings_lifespan)
56+
lifespan_manager.add(remote_docker_client_lifespan)
57+
58+
app = FastAPI(lifespan=lifespan_manager)
4959
app.state.settings = settings
5060

5161
return app
@@ -61,7 +71,7 @@ async def _(env_vars: EnvVarsDict) -> AsyncIterator[aiodocker.Docker]:
6171

6272
app = _get_test_app()
6373

64-
async with LifespanManager(app, startup_timeout=30, shutdown_timeout=30):
74+
async with ASGILifespanManager(app, startup_timeout=30, shutdown_timeout=30):
6575
yield get_remote_docker_client(app)
6676

6777
return _

services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/rpc/routes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
]
1414

1515

16-
async def lifespan_rpc_api_routes(app: FastAPI) -> AsyncIterator[State]:
16+
async def rpc_api_routes_lifespan(app: FastAPI) -> AsyncIterator[State]:
1717
rpc_server = get_rabbitmq_rpc_server(app)
1818
for router in ROUTERS:
1919
await rpc_server.register_router(router, DYNAMIC_SCHEDULER_RPC_NAMESPACE, app)

0 commit comments

Comments
 (0)