Skip to content

Commit 927caed

Browse files
authored
🎨 catalog: lifespan managers for fastapi apps (#7483)
1 parent e162382 commit 927caed

File tree

24 files changed

+670
-158
lines changed

24 files changed

+670
-158
lines changed

packages/pytest-simcore/src/pytest_simcore/helpers/logging_tools.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def _timedelta_as_minute_second_ms(delta: datetime.timedelta) -> str:
2222
if int(milliseconds * 1000) != 0:
2323
result += f"{int(milliseconds*1000)}ms"
2424

25-
sign = "-" if total_seconds < 0 else ""
25+
sign = "-" if total_seconds < 0 else "<1ms"
2626

2727
return f"{sign}{result.strip()}"
2828

@@ -32,10 +32,11 @@ class DynamicIndentFormatter(logging.Formatter):
3232
_cls_indent_level: int = 0
3333
_instance_indent_level: int = 0
3434

35-
def __init__(self, fmt=None, datefmt=None, style="%"):
35+
def __init__(self, *args, **kwargs):
36+
fmt = args[0] if args else None
3637
dynamic_fmt = fmt or "%(asctime)s %(levelname)s %(message)s"
3738
assert "message" in dynamic_fmt
38-
super().__init__(dynamic_fmt, datefmt, style)
39+
super().__init__(dynamic_fmt, *args, **kwargs)
3940

4041
def format(self, record) -> str:
4142
original_message = record.msg

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
SEE migration aiopg->asyncpg https://github.com/ITISFoundation/osparc-simcore/issues/4529
55
"""
66

7-
87
import logging
98
from typing import Final
109

@@ -16,7 +15,7 @@
1615
)
1716
from sqlalchemy.ext.asyncio import AsyncEngine
1817

19-
from ..db_asyncpg_utils import create_async_engine_and_pg_database_ready
18+
from ..db_asyncpg_utils import create_async_engine_and_database_ready
2019
from ..logging_utils import log_context
2120

2221
APP_DB_ASYNC_ENGINE_KEY: Final[str] = f"{__name__ }.AsyncEngine"
@@ -56,7 +55,7 @@ async def connect_to_db(app: web.Application, settings: PostgresSettings) -> Non
5655
"Connecting app[APP_DB_ASYNC_ENGINE_KEY] to postgres with %s",
5756
f"{settings=}",
5857
):
59-
engine = await create_async_engine_and_pg_database_ready(settings)
58+
engine = await create_async_engine_and_database_ready(settings)
6059
_set_async_engine_to_app_state(app, engine)
6160

6261
_logger.info(

packages/service-library/src/servicelib/db_async_engine.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import warnings
23

34
from fastapi import FastAPI
45
from settings_library.postgres import PostgresSettings
@@ -17,6 +18,12 @@
1718

1819
@retry(**PostgresRetryPolicyUponInitialization(_logger).kwargs)
1920
async def connect_to_db(app: FastAPI, settings: PostgresSettings) -> None:
21+
warnings.warn(
22+
"The 'connect_to_db' function is deprecated and will be removed in a future release. "
23+
"Please use 'postgres_lifespan' instead for managing the database connection lifecycle.",
24+
DeprecationWarning,
25+
stacklevel=2,
26+
)
2027
with log_context(
2128
_logger, logging.DEBUG, f"connection to db {settings.dsn_with_async_sqlalchemy}"
2229
):

packages/service-library/src/servicelib/db_asyncpg_utils.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44

55
from models_library.healthchecks import IsNonResponsive, IsResponsive, LivenessResult
66
from settings_library.postgres import PostgresSettings
7-
from simcore_postgres_database.utils_aiosqlalchemy import ( # type: ignore[import-not-found] # this on is unclear
8-
raise_if_migration_not_ready,
9-
)
107
from sqlalchemy.exc import SQLAlchemyError
118
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
129
from tenacity import retry
@@ -17,7 +14,7 @@
1714

1815

1916
@retry(**PostgresRetryPolicyUponInitialization(_logger).kwargs)
20-
async def create_async_engine_and_pg_database_ready(
17+
async def create_async_engine_and_database_ready(
2118
settings: PostgresSettings,
2219
) -> AsyncEngine:
2320
"""
@@ -26,13 +23,17 @@ async def create_async_engine_and_pg_database_ready(
2623
- waits until db data is migrated (i.e. ready to use)
2724
- returns engine
2825
"""
26+
from simcore_postgres_database.utils_aiosqlalchemy import ( # type: ignore[import-not-found] # this on is unclear
27+
raise_if_migration_not_ready,
28+
)
29+
2930
server_settings = None
3031
if settings.POSTGRES_CLIENT_NAME:
3132
server_settings = {
3233
"application_name": settings.POSTGRES_CLIENT_NAME,
3334
}
3435

35-
engine: AsyncEngine = create_async_engine(
36+
engine = create_async_engine(
3637
settings.dsn_with_async_sqlalchemy,
3738
pool_size=settings.POSTGRES_MINSIZE,
3839
max_overflow=settings.POSTGRES_MAXSIZE - settings.POSTGRES_MINSIZE,
@@ -43,9 +44,10 @@ async def create_async_engine_and_pg_database_ready(
4344

4445
try:
4546
await raise_if_migration_not_ready(engine)
46-
except Exception:
47+
except Exception as exc:
4748
# NOTE: engine must be closed because retry will create a new engine
4849
await engine.dispose()
50+
exc.add_note("Failed during migration check. Created engine disposed.")
4951
raise
5052

5153
return engine

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import warnings
23

34
from fastapi import FastAPI
45
from settings_library.postgres import PostgresSettings
@@ -7,19 +8,26 @@
78
)
89
from sqlalchemy.ext.asyncio import AsyncEngine
910

10-
from ..db_asyncpg_utils import create_async_engine_and_pg_database_ready
11+
from ..db_asyncpg_utils import create_async_engine_and_database_ready
1112
from ..logging_utils import log_context
1213

1314
_logger = logging.getLogger(__name__)
1415

1516

1617
async def connect_to_db(app: FastAPI, settings: PostgresSettings) -> None:
18+
warnings.warn(
19+
"The 'connect_to_db' function is deprecated and will be removed in a future release. "
20+
"Please use 'postgres_lifespan' instead for managing the database connection lifecycle.",
21+
DeprecationWarning,
22+
stacklevel=2,
23+
)
24+
1725
with log_context(
1826
_logger,
1927
logging.DEBUG,
2028
f"Connecting and migraging {settings.dsn_with_async_sqlalchemy}",
2129
):
22-
engine = await create_async_engine_and_pg_database_ready(settings)
30+
engine = await create_async_engine_and_database_ready(settings)
2331

2432
app.state.engine = engine
2533
_logger.debug(

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
from collections.abc import AsyncIterator
22
from typing import Protocol
33

4+
from common_library.errors_classes import OsparcErrorMixin
45
from fastapi import FastAPI
56
from fastapi_lifespan_manager import LifespanManager, State
67

78

9+
class LifespanError(OsparcErrorMixin, RuntimeError): ...
10+
11+
12+
class LifespanOnStartupError(LifespanError):
13+
msg_template = "Failed during startup of {module}"
14+
15+
16+
class LifespanOnShutdownError(LifespanError):
17+
msg_template = "Failed during shutdown of {module}"
18+
19+
820
class LifespanGenerator(Protocol):
9-
def __call__(self, app: FastAPI) -> AsyncIterator["State"]:
10-
...
21+
def __call__(self, app: FastAPI) -> AsyncIterator["State"]: ...
1122

1223

1324
def combine_lifespans(*generators: LifespanGenerator) -> LifespanManager:
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import asyncio
2+
import logging
3+
from collections.abc import AsyncIterator
4+
from enum import Enum
5+
6+
from fastapi_lifespan_manager import State
7+
from servicelib.logging_utils import log_catch, log_context
8+
from settings_library.postgres import PostgresSettings
9+
from sqlalchemy.ext.asyncio import AsyncEngine
10+
11+
from ..db_asyncpg_utils import create_async_engine_and_database_ready
12+
from .lifespan_utils import LifespanOnStartupError
13+
14+
_logger = logging.getLogger(__name__)
15+
16+
17+
class PostgresLifespanState(str, Enum):
18+
POSTGRES_SETTINGS = "postgres_settings"
19+
POSTGRES_ASYNC_ENGINE = "postgres.async_engine"
20+
21+
22+
class PostgresConfigurationError(LifespanOnStartupError):
23+
msg_template = "Invalid postgres settings [={settings}] on startup. Note that postgres cannot be disabled using settings"
24+
25+
26+
def create_input_state(settings: PostgresSettings) -> State:
27+
return {PostgresLifespanState.POSTGRES_SETTINGS: settings}
28+
29+
30+
async def postgres_database_lifespan(_, state: State) -> AsyncIterator[State]:
31+
32+
with log_context(_logger, logging.INFO, f"{__name__}"):
33+
34+
settings = state[PostgresLifespanState.POSTGRES_SETTINGS]
35+
36+
if settings is None or not isinstance(settings, PostgresSettings):
37+
raise PostgresConfigurationError(settings=settings)
38+
39+
assert isinstance(settings, PostgresSettings) # nosec
40+
41+
# connect to database
42+
async_engine: AsyncEngine = await create_async_engine_and_database_ready(
43+
settings
44+
)
45+
46+
try:
47+
48+
yield {
49+
PostgresLifespanState.POSTGRES_ASYNC_ENGINE: async_engine,
50+
}
51+
52+
finally:
53+
with log_catch(_logger, reraise=False):
54+
await asyncio.wait_for(async_engine.dispose(), timeout=10)

0 commit comments

Comments
 (0)