Build 060.27: implement Persistent Market Data Storage
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from aiogram import Bot, Dispatcher
|
||||
|
||||
from src.bootstrap.market_data_storage import (
|
||||
MarketDataStorageLifecycleProtocol,
|
||||
)
|
||||
from src.market_data.acquisition.runtime.trade_stream_production_runtime import (
|
||||
TradeStreamProductionRuntimeProtocol,
|
||||
)
|
||||
@@ -19,6 +23,9 @@ class ApplicationComposition:
|
||||
trade_stream_runtime: (
|
||||
TradeStreamProductionRuntimeProtocol | None
|
||||
)
|
||||
market_data_storage_lifecycle: (
|
||||
MarketDataStorageLifecycleProtocol | None
|
||||
) = None
|
||||
|
||||
|
||||
async def run_application(
|
||||
@@ -31,25 +38,37 @@ async def run_application(
|
||||
приложения. Остановка Telegram, Trade Stream и bot session
|
||||
выполняется одним владельцем и не оставляет фоновых root tasks.
|
||||
"""
|
||||
polling_task = asyncio.create_task(
|
||||
application.dispatcher.start_polling(
|
||||
application.bot,
|
||||
close_bot_session=False,
|
||||
),
|
||||
name="telegram-polling",
|
||||
)
|
||||
runtime_task = (
|
||||
asyncio.create_task(
|
||||
application.trade_stream_runtime.run(),
|
||||
name="trade-stream-runtime",
|
||||
)
|
||||
if application.trade_stream_runtime is not None
|
||||
else None
|
||||
)
|
||||
|
||||
polling_task: asyncio.Task[None] | None = None
|
||||
runtime_task: asyncio.Task[None] | None = None
|
||||
primary_error: BaseException | None = None
|
||||
|
||||
try:
|
||||
storage_lifecycle = (
|
||||
application.market_data_storage_lifecycle
|
||||
)
|
||||
|
||||
if storage_lifecycle is not None:
|
||||
await _run_blocking_lifecycle_operation(
|
||||
storage_lifecycle.start,
|
||||
task_name="market-data-storage-startup",
|
||||
)
|
||||
|
||||
polling_task = asyncio.create_task(
|
||||
application.dispatcher.start_polling(
|
||||
application.bot,
|
||||
close_bot_session=False,
|
||||
),
|
||||
name="telegram-polling",
|
||||
)
|
||||
runtime_task = (
|
||||
asyncio.create_task(
|
||||
application.trade_stream_runtime.run(),
|
||||
name="trade-stream-runtime",
|
||||
)
|
||||
if application.trade_stream_runtime is not None
|
||||
else None
|
||||
)
|
||||
|
||||
await _wait_for_root_tasks(
|
||||
polling_task=polling_task,
|
||||
runtime_task=runtime_task,
|
||||
@@ -128,7 +147,7 @@ async def _wait_for_root_tasks(
|
||||
async def _shutdown_application(
|
||||
*,
|
||||
application: ApplicationComposition,
|
||||
polling_task: asyncio.Task[None],
|
||||
polling_task: asyncio.Task[None] | None,
|
||||
runtime_task: asyncio.Task[None] | None,
|
||||
primary_error: BaseException | None,
|
||||
) -> BaseException | None:
|
||||
@@ -136,7 +155,7 @@ async def _shutdown_application(
|
||||
polling_cancelled = False
|
||||
runtime_cancelled = False
|
||||
|
||||
if not polling_task.done():
|
||||
if polling_task is not None and not polling_task.done():
|
||||
polling_cancelled = True
|
||||
polling_task.cancel()
|
||||
|
||||
@@ -155,12 +174,13 @@ async def _shutdown_application(
|
||||
runtime_cancelled = True
|
||||
runtime_task.cancel()
|
||||
|
||||
cleanup_error = await _observe_root_task(
|
||||
polling_task,
|
||||
expected_cancellation=polling_cancelled,
|
||||
primary_error=primary_error,
|
||||
cleanup_error=cleanup_error,
|
||||
)
|
||||
if polling_task is not None:
|
||||
cleanup_error = await _observe_root_task(
|
||||
polling_task,
|
||||
expected_cancellation=polling_cancelled,
|
||||
primary_error=primary_error,
|
||||
cleanup_error=cleanup_error,
|
||||
)
|
||||
|
||||
if runtime_task is not None:
|
||||
cleanup_error = await _observe_root_task(
|
||||
@@ -170,6 +190,20 @@ async def _shutdown_application(
|
||||
cleanup_error=cleanup_error,
|
||||
)
|
||||
|
||||
storage_lifecycle = application.market_data_storage_lifecycle
|
||||
|
||||
if storage_lifecycle is not None:
|
||||
try:
|
||||
await _run_blocking_lifecycle_operation(
|
||||
storage_lifecycle.stop,
|
||||
task_name="market-data-storage-shutdown",
|
||||
)
|
||||
except BaseException as error:
|
||||
cleanup_error = _merge_cleanup_error(
|
||||
cleanup_error,
|
||||
error,
|
||||
)
|
||||
|
||||
try:
|
||||
await application.bot.session.close()
|
||||
except BaseException as error:
|
||||
@@ -181,6 +215,44 @@ async def _shutdown_application(
|
||||
return cleanup_error
|
||||
|
||||
|
||||
async def _run_blocking_lifecycle_operation(
|
||||
operation: Callable[[], None],
|
||||
*,
|
||||
task_name: str,
|
||||
) -> None:
|
||||
operation_task = asyncio.create_task(
|
||||
asyncio.to_thread(operation),
|
||||
name=task_name,
|
||||
)
|
||||
|
||||
try:
|
||||
await asyncio.shield(operation_task)
|
||||
except asyncio.CancelledError as cancellation:
|
||||
try:
|
||||
await _wait_for_blocking_operation(operation_task)
|
||||
except BaseException as operation_error:
|
||||
cancellation.add_note(
|
||||
"Application blocking lifecycle operation also failed: "
|
||||
f"{type(operation_error).__name__}."
|
||||
)
|
||||
|
||||
raise
|
||||
|
||||
|
||||
async def _wait_for_blocking_operation(
|
||||
operation_task: asyncio.Task[None],
|
||||
) -> None:
|
||||
while not operation_task.done():
|
||||
try:
|
||||
await asyncio.shield(operation_task)
|
||||
except asyncio.CancelledError:
|
||||
continue
|
||||
except BaseException:
|
||||
break
|
||||
|
||||
operation_task.result()
|
||||
|
||||
|
||||
async def _observe_root_task(
|
||||
task: asyncio.Task[None],
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user