Skip to content

Повторный старт Dispatcher дублирует унаследованные inner-мидлвари #347

Description

@K1rL3s

Где в коде: 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]

Варианты решения

  1. Пересобирать унаследованные мидлвари с нуля. Хранить у observer'а собственные inner-мидлвари отдельно от унаследованных и на каждом старте строить итоговую цепочку заново: унаследованные от родителей плюс собственные. Повторный старт даёт тот же результат. Мидлвари, добавленные между shutdown и следующим стартом, тоже подхватываются.
  2. Разрешать мидлвари один раз. Пропускать resolve_middlewares, если Dispatcher уже стартовал раньше. Правка самая маленькая, но мидлвари и роутеры, добавленные после первого shutdown, не унаследуются: после BeforeShutdown роутеры снова в Empty и принимают новые хендлеры и мидлвари.
  3. Откатывать на shutdown. В _emit_before_shutdown_handler убирать из списков то, что вставил resolve_middlewares. Для этого всё равно нужно помнить, что было унаследовано, то есть это тот же первый вариант, только сложнее.
  4. Запретить повторный старт. Бросать понятную ошибку при втором BeforeStartup и описать в документации, что Dispatcher стартует один раз. Это breaking change для тех, кто перезапускает поллинг на том же диспетчере.

Первый вариант выглядит самым надёжным.

Почему это важно

Перезапуск того же Dispatcher (повторный start_polling после остановки, несколько запусков в тестах, повторный on_startup у вебхука) без ошибок удваивает мидлвари родителя в каждом дочернем роутере. Побочные эффекты мидлварей (логирование, метрики, троттлинг, открытие транзакций) начинают срабатывать по два раза, и по логам причину найти трудно.

Найдено при ревью #344.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions