diff --git a/src/bot/core.py b/src/bot/core.py index 2d5e99bb3..b9edc8bbb 100644 --- a/src/bot/core.py +++ b/src/bot/core.py @@ -64,6 +64,15 @@ async def initialize(self) -> None: builder.write_timeout(30) builder.pool_timeout(30) + # Long polling uses a separate HTTP client whose connection pool holds a + # single connection by default. If a long-running getUpdates request is + # torn down mid-flight (unstable network, proxy or tunnel drop), that + # connection can stay checked out, and every later getUpdates call then + # fails with "Pool timeout: All connections in the connection pool are + # occupied", permanently, even after the network recovers. A small pool + # leaves headroom so polling can recover on its own. + builder.get_updates_connection_pool_size(8) + # Explicitly set proxy from environment variables. # This is necessary because python-telegram-bot's Application.builder() # does not automatically use HTTP_PROXY/HTTPS_PROXY environment variables. diff --git a/tests/unit/test_bot/test_core_connection_pool.py b/tests/unit/test_bot/test_core_connection_pool.py new file mode 100644 index 000000000..981ec7f02 --- /dev/null +++ b/tests/unit/test_bot/test_core_connection_pool.py @@ -0,0 +1,56 @@ +"""Tests for bot core HTTP connection pool wiring.""" + +from unittest.mock import AsyncMock, MagicMock + +import pytest + +import src.bot.core as core_module +from src.bot.core import ClaudeCodeBot +from src.config import create_test_config + + +@pytest.fixture +def bot_with_builder(monkeypatch): + """Create a bot with mocked Application builder plumbing.""" + settings = create_test_config() + deps = { + "storage": MagicMock(), + "security": MagicMock(), + } + bot = ClaudeCodeBot(settings, deps) + + builder = MagicMock() + + app = MagicMock() + app.bot = MagicMock() + app.bot.set_my_commands = AsyncMock() + app.initialize = AsyncMock() + builder.build.return_value = app + + monkeypatch.setattr( + core_module.Application, + "builder", + MagicMock(return_value=builder), + ) + monkeypatch.setattr( + core_module, + "FeatureRegistry", + MagicMock(return_value=MagicMock()), + ) + monkeypatch.setattr(bot, "_set_bot_commands", AsyncMock()) + monkeypatch.setattr(bot, "_register_handlers", MagicMock()) + monkeypatch.setattr(bot, "_add_middleware", MagicMock()) + + return bot, builder + + +@pytest.mark.asyncio +async def test_initialize_gives_get_updates_client_a_pool(bot_with_builder): + """Long polling must not run on the default single-connection pool.""" + bot, builder = bot_with_builder + + await bot.initialize() + + builder.get_updates_connection_pool_size.assert_called_once() + pool_size = builder.get_updates_connection_pool_size.call_args.args[0] + assert pool_size > 1