|
1 | 1 | import asyncio |
| 2 | +import datetime |
2 | 3 | import logging |
3 | 4 | import threading |
4 | 5 | from typing import Final |
5 | 6 |
|
6 | 7 | from asgi_lifespan import LifespanManager |
7 | 8 | from celery import Celery # type: ignore[import-untyped] |
8 | 9 | from fastapi import FastAPI |
9 | | -from servicelib.async_utils import cancel_wait_task |
| 10 | +from servicelib.logging_utils import log_context |
10 | 11 |
|
11 | 12 | from ...core.application import create_app |
12 | 13 | from ...core.settings import ApplicationSettings |
13 | | -from ...modules.celery import get_event_loop, set_event_loop |
| 14 | +from ...modules.celery import set_event_loop |
14 | 15 | from ...modules.celery.utils import ( |
15 | 16 | get_fastapi_app, |
16 | 17 | set_celery_worker, |
|
20 | 21 |
|
21 | 22 | _logger = logging.getLogger(__name__) |
22 | 23 |
|
23 | | -_LIFESPAN_TIMEOUT: Final[int] = 10 |
| 24 | +_SHUTDOWN_TIMEOUT: Final[float] = datetime.timedelta(seconds=10).total_seconds() |
| 25 | +_STARTUP_TIMEOUT: Final[float] = datetime.timedelta(minutes=1).total_seconds() |
24 | 26 |
|
25 | 27 |
|
26 | 28 | def on_worker_init(sender, **_kwargs) -> None: |
27 | | - def _init_fastapi() -> None: |
| 29 | + startup_complete_event = threading.Event() |
| 30 | + |
| 31 | + def _init_fastapi(startup_complete_event: threading.Event) -> None: |
28 | 32 | loop = asyncio.new_event_loop() |
29 | 33 | asyncio.set_event_loop(loop) |
30 | 34 | shutdown_event = asyncio.Event() |
31 | 35 |
|
32 | 36 | fastapi_app = create_app(ApplicationSettings.create_from_envs()) |
33 | 37 |
|
34 | | - async def lifespan(): |
| 38 | + async def lifespan( |
| 39 | + startup_complete_event: threading.Event, shutdown_event: asyncio.Event |
| 40 | + ) -> None: |
35 | 41 | async with LifespanManager( |
36 | 42 | fastapi_app, |
37 | | - startup_timeout=_LIFESPAN_TIMEOUT, |
38 | | - shutdown_timeout=_LIFESPAN_TIMEOUT, |
| 43 | + startup_timeout=_STARTUP_TIMEOUT, |
| 44 | + shutdown_timeout=_SHUTDOWN_TIMEOUT, |
39 | 45 | ): |
40 | 46 | try: |
| 47 | + _logger.info("fastapi APP started!") |
| 48 | + startup_complete_event.set() |
41 | 49 | await shutdown_event.wait() |
42 | 50 | except asyncio.CancelledError: |
43 | 51 | _logger.warning("Lifespan task cancelled") |
44 | 52 |
|
45 | | - lifespan_task = loop.create_task(lifespan()) |
46 | | - fastapi_app.state.lifespan_task = lifespan_task |
47 | 53 | fastapi_app.state.shutdown_event = shutdown_event |
48 | 54 | set_event_loop(fastapi_app, loop) |
49 | 55 |
|
50 | 56 | set_fastapi_app(sender.app, fastapi_app) |
51 | 57 | set_celery_worker(sender.app, CeleryTaskQueueWorker(sender.app)) |
52 | | - |
53 | | - loop.run_forever() |
54 | | - |
55 | | - thread = threading.Thread(target=_init_fastapi, daemon=True) |
| 58 | + loop.run_until_complete(lifespan(startup_complete_event, shutdown_event)) |
| 59 | + |
| 60 | + thread = threading.Thread( |
| 61 | + group=None, |
| 62 | + target=_init_fastapi, |
| 63 | + name="fastapi_app", |
| 64 | + args=(startup_complete_event,), |
| 65 | + daemon=True, |
| 66 | + ) |
56 | 67 | thread.start() |
| 68 | + # ensure the fastapi app is ready before going on |
| 69 | + startup_complete_event.wait(_STARTUP_TIMEOUT * 1.1) |
57 | 70 |
|
58 | 71 |
|
59 | | -def on_worker_shutdown(sender, **_kwargs): |
60 | | - assert isinstance(sender.app, Celery) |
61 | | - |
62 | | - fastapi_app = get_fastapi_app(sender.app) |
63 | | - assert isinstance(fastapi_app, FastAPI) |
64 | | - event_loop = get_event_loop(fastapi_app) |
65 | | - |
66 | | - async def shutdown(): |
| 72 | +def on_worker_shutdown(sender, **_kwargs) -> None: |
| 73 | + with log_context(_logger, logging.INFO, "Worker Shuts-down"): |
| 74 | + assert isinstance(sender.app, Celery) |
| 75 | + fastapi_app = get_fastapi_app(sender.app) |
| 76 | + assert isinstance(fastapi_app, FastAPI) |
67 | 77 | fastapi_app.state.shutdown_event.set() |
68 | | - |
69 | | - await cancel_wait_task(fastapi_app.state.lifespan_task, max_delay=5) |
70 | | - |
71 | | - asyncio.run_coroutine_threadsafe(shutdown(), event_loop) |
|
0 commit comments