Build 060.27: implement Persistent Market Data Storage

This commit is contained in:
2026-08-01 03:22:25 +03:00
parent cb8acfe5fe
commit 58e5a12a4d
54 changed files with 10243 additions and 101 deletions

View File

@@ -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],
*,