Где в коде: src/maxo/routing/utils/_resolving_inner_middlewares.py (resolve_middlewares, _resolving_middlewares), вызов в src/maxo/routing/dispatcher.py (Dispatcher._emit_before_startup_handler)
Проблема
Dispatcher на каждом BeforeStartup вызывает resolve_middlewares(self), а _resolving_middlewares вставляет inner-мидлвари родителя в начало списка дочернего observer'а:
observer.middleware.inner.middlewares[:0] = new_inners
Список меняется на месте, а на shutdown ничего не откатывается. Поэтому повторный старт того же Dispatcher вставляет унаследованные мидлвари ещё раз, и они выполняются дважды на каждом апдейте дочернего роутера. Каждый следующий старт добавляет ещё одну копию.
Как повторить
import asyncio
from typing import Any
from maxo import Dispatcher, Router
from maxo.routing.ctx import Ctx
from maxo.routing.interfaces.middleware import BaseMiddleware, NextMiddleware
from maxo.routing.signals.shutdown import AfterShutdown, BeforeShutdown
from maxo.routing.signals.startup import AfterStartup, BeforeStartup
class Named(BaseMiddleware[Any]):
def __init__(self, name: str) -> None:
self.name = name
async def __call__(self, update: Any, ctx: Ctx, next: NextMiddleware[Any]) -> Any:
return await next(ctx)
def __repr__(self) -> str:
return self.name
async def main() -> None:
dp = Dispatcher()
child = Router()
dp.message_created.middleware.inner(Named("dp"))
child.message_created.middleware.inner(Named("child"))
dp.include(child)
for i in range(2):
await dp.feed_signal(BeforeStartup())
await dp.feed_signal(AfterStartup())
print(f"start {i + 1}:", child.message_created.middleware.inner.middlewares)
await dp.feed_signal(BeforeShutdown())
await dp.feed_signal(AfterShutdown())
asyncio.run(main())
Проверено на master:
start 1: [dp, child]
start 2: [dp, dp, child]
Варианты решения
- Пересобирать унаследованные мидлвари с нуля. Хранить у observer'а собственные inner-мидлвари отдельно от унаследованных и на каждом старте строить итоговую цепочку заново: унаследованные от родителей плюс собственные. Повторный старт даёт тот же результат. Мидлвари, добавленные между shutdown и следующим стартом, тоже подхватываются.
- Разрешать мидлвари один раз. Пропускать
resolve_middlewares, если Dispatcher уже стартовал раньше. Правка самая маленькая, но мидлвари и роутеры, добавленные после первого shutdown, не унаследуются: после BeforeShutdown роутеры снова в Empty и принимают новые хендлеры и мидлвари.
- Откатывать на shutdown. В
_emit_before_shutdown_handler убирать из списков то, что вставил resolve_middlewares. Для этого всё равно нужно помнить, что было унаследовано, то есть это тот же первый вариант, только сложнее.
- Запретить повторный старт. Бросать понятную ошибку при втором
BeforeStartup и описать в документации, что Dispatcher стартует один раз. Это breaking change для тех, кто перезапускает поллинг на том же диспетчере.
Первый вариант выглядит самым надёжным.
Почему это важно
Перезапуск того же Dispatcher (повторный start_polling после остановки, несколько запусков в тестах, повторный on_startup у вебхука) без ошибок удваивает мидлвари родителя в каждом дочернем роутере. Побочные эффекты мидлварей (логирование, метрики, троттлинг, открытие транзакций) начинают срабатывать по два раза, и по логам причину найти трудно.
Найдено при ревью #344.
Где в коде:
src/maxo/routing/utils/_resolving_inner_middlewares.py(resolve_middlewares,_resolving_middlewares), вызов вsrc/maxo/routing/dispatcher.py(Dispatcher._emit_before_startup_handler)Проблема
Dispatcherна каждомBeforeStartupвызываетresolve_middlewares(self), а_resolving_middlewaresвставляет inner-мидлвари родителя в начало списка дочернего observer'а:Список меняется на месте, а на shutdown ничего не откатывается. Поэтому повторный старт того же
Dispatcherвставляет унаследованные мидлвари ещё раз, и они выполняются дважды на каждом апдейте дочернего роутера. Каждый следующий старт добавляет ещё одну копию.Как повторить
Проверено на master:
Варианты решения
resolve_middlewares, еслиDispatcherуже стартовал раньше. Правка самая маленькая, но мидлвари и роутеры, добавленные после первого shutdown, не унаследуются: послеBeforeShutdownроутеры снова вEmptyи принимают новые хендлеры и мидлвари._emit_before_shutdown_handlerубирать из списков то, что вставилresolve_middlewares. Для этого всё равно нужно помнить, что было унаследовано, то есть это тот же первый вариант, только сложнее.BeforeStartupи описать в документации, чтоDispatcherстартует один раз. Это breaking change для тех, кто перезапускает поллинг на том же диспетчере.Первый вариант выглядит самым надёжным.
Почему это важно
Перезапуск того же
Dispatcher(повторныйstart_pollingпосле остановки, несколько запусков в тестах, повторныйon_startupу вебхука) без ошибок удваивает мидлвари родителя в каждом дочернем роутере. Побочные эффекты мидлварей (логирование, метрики, троттлинг, открытие транзакций) начинают срабатывать по два раза, и по логам причину найти трудно.Найдено при ревью #344.