From eeb2f764a5ef3c14f274cbe710af2b0fb3df780a Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sat, 26 Jul 2025 17:29:06 +0200 Subject: [PATCH 1/9] Improve Internal Logic for Network Retries --- .../4880.42PW26hGpLypMJMxTeiQZ6.toml | 6 +++ src/telegram/ext/_application.py | 1 + src/telegram/ext/_updater.py | 15 +++---- src/telegram/ext/_utils/networkloop.py | 40 +++++++++++++++---- 4 files changed, 47 insertions(+), 15 deletions(-) create mode 100644 changes/unreleased/4880.42PW26hGpLypMJMxTeiQZ6.toml diff --git a/changes/unreleased/4880.42PW26hGpLypMJMxTeiQZ6.toml b/changes/unreleased/4880.42PW26hGpLypMJMxTeiQZ6.toml new file mode 100644 index 00000000000..fdcc0952a37 --- /dev/null +++ b/changes/unreleased/4880.42PW26hGpLypMJMxTeiQZ6.toml @@ -0,0 +1,6 @@ +internal = "Improve Internal Logic for Network Retries" + +[[pull_requests]] +uid = "4880" +author_uid = "Bibo-Joshi" +closes_threads = ["4871"] diff --git a/src/telegram/ext/_application.py b/src/telegram/ext/_application.py index 51f739b3e27..c0b064d2b30 100644 --- a/src/telegram/ext/_application.py +++ b/src/telegram/ext/_application.py @@ -1016,6 +1016,7 @@ async def _bootstrap_initialize(self, max_retries: int) -> None: description="Bootstrap Initialize Application", max_retries=max_retries, interval=1, + infinite_loop=False, ) def __run( diff --git a/src/telegram/ext/_updater.py b/src/telegram/ext/_updater.py index c67d147d6d0..212beffaf89 100644 --- a/src/telegram/ext/_updater.py +++ b/src/telegram/ext/_updater.py @@ -335,7 +335,7 @@ async def _start_polling( _LOGGER.debug("Bootstrap done") - async def polling_action_cb() -> bool: + async def polling_action_cb() -> None: try: updates = await self.bot.get_updates( offset=self._last_update_id, @@ -352,7 +352,7 @@ async def polling_action_cb() -> bool: "Received data was *not* processed!", exc_info=exc, ) - return True + return if updates: if not self.running: @@ -365,7 +365,7 @@ async def polling_action_cb() -> bool: await self.update_queue.put(update) self._last_update_id = updates[-1].update_id + 1 # Add one to 'confirm' it - return True # Keep fetching updates & don't quit. Polls with poll_interval. + return def default_error_callback(exc: TelegramError) -> None: _LOGGER.exception("Exception happened while polling for updates.", exc_info=exc) @@ -382,6 +382,7 @@ def default_error_callback(exc: TelegramError) -> None: interval=poll_interval, stop_event=self.__polling_task_stop_event, max_retries=-1, + infinite_loop=True, ), name="Updater:start_polling:polling_task", ) @@ -678,14 +679,13 @@ async def _bootstrap( :paramref:`max_retries`. """ - async def bootstrap_del_webhook() -> bool: + async def bootstrap_del_webhook() -> None: _LOGGER.debug("Deleting webhook") if drop_pending_updates: _LOGGER.debug("Dropping pending updates from Telegram server") await self.bot.delete_webhook(drop_pending_updates=drop_pending_updates) - return False - async def bootstrap_set_webhook() -> bool: + async def bootstrap_set_webhook() -> None: _LOGGER.debug("Setting webhook") if drop_pending_updates: _LOGGER.debug("Dropping pending updates from Telegram server") @@ -698,7 +698,6 @@ async def bootstrap_set_webhook() -> bool: max_connections=max_connections, secret_token=secret_token, ) - return False # Dropping pending updates from TG can be efficiently done with the drop_pending_updates # parameter of delete/start_webhook, even in the case of polling. Also, we want to make @@ -712,6 +711,7 @@ async def bootstrap_set_webhook() -> bool: interval=bootstrap_interval, stop_event=None, max_retries=max_retries, + infinite_loop=False, ) # Restore/set webhook settings, if needed. Again, we don't know ahead if a webhook is set, @@ -724,6 +724,7 @@ async def bootstrap_set_webhook() -> bool: interval=bootstrap_interval, stop_event=None, max_retries=max_retries, + infinite_loop=False, ) async def stop(self) -> None: diff --git a/src/telegram/ext/_utils/networkloop.py b/src/telegram/ext/_utils/networkloop.py index 2cc93113272..c17e88fe094 100644 --- a/src/telegram/ext/_utils/networkloop.py +++ b/src/telegram/ext/_utils/networkloop.py @@ -50,14 +50,19 @@ async def network_retry_loop( stop_event: Optional[asyncio.Event] = None, is_running: Optional[Callable[[], bool]] = None, max_retries: int, + infinite_loop: bool, ) -> None: """Perform a loop calling `action_cb`, retrying after network errors. - Stop condition for loop: + Stop condition for loop in case of `infinite_loop` is True: * `is_running()` evaluates :obj:`False` or - * return value of `action_cb` evaluates :obj:`False` * or `stop_event` is set. - * or `max_retries` is reached. + + Stop condition for loop in case of `infinite_loop` is False: + * a call to `action_cb` succeeds or + * `is_running()` evaluates :obj:`False` or + * or `stop_event` is set or + * `max_retries` is reached. Args: action_cb (:term:`coroutine function`): Network oriented callback function to call. @@ -82,15 +87,29 @@ async def network_retry_loop( * < 0: Retry indefinitely. * 0: No retries. - * > 0: Number of retries. + * > 0: Number of retries + + Must be negative if `infinite_loop` is set to True.. + infinite_loop (:obj:`bool`): If :obj:`True`, the loop will run indefinitely until + `is_running()` evaluates to :obj:`False` or `stop_event` is set. Otherwise, the loop + will stop after a successful call to `action_cb`, or when `is_running()` evaluates to + :obj:`False`, or `stop_event` is set, or `max_retries` is reached. """ + if infinite_loop and max_retries >= 0: + raise ValueError( + "max_retries must be negative if infinite_loop is True. " + "Use -1 for infinite retries." + ) + log_prefix = f"Network Retry Loop ({description}):" effective_is_running = is_running or (lambda: True) async def do_action() -> bool: + """Return value indicating whether the action was successful.""" if not stop_event: - return await action_cb() + await action_cb() + return True action_cb_task = asyncio.create_task(action_cb()) stop_task = asyncio.create_task(stop_event.wait()) @@ -105,15 +124,20 @@ async def do_action() -> bool: _LOGGER.debug("%s Cancelled", log_prefix) return False - return action_cb_task.result() + # Calling `result()` on `action_cb_task` will raise an exception if the task failed. + # this is important to propagate the error to the caller. + action_cb_task.result() + return True _LOGGER.debug("%s Starting", log_prefix) cur_interval = interval retries = 0 while effective_is_running(): try: - if not await do_action(): - break + await do_action() + if not infinite_loop: + _LOGGER.debug("%s Action succeeded. Stopping loop.", log_prefix) + return except RetryAfter as exc: slack_time = 0.5 _LOGGER.info( From 822c447ab58d54e139750ecd91efb1a99c12991d Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 27 Jul 2025 10:00:20 +0200 Subject: [PATCH 2/9] review pool --- src/telegram/ext/_utils/networkloop.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/telegram/ext/_utils/networkloop.py b/src/telegram/ext/_utils/networkloop.py index c17e88fe094..132dc8c15e2 100644 --- a/src/telegram/ext/_utils/networkloop.py +++ b/src/telegram/ext/_utils/networkloop.py @@ -54,15 +54,13 @@ async def network_retry_loop( ) -> None: """Perform a loop calling `action_cb`, retrying after network errors. - Stop condition for loop in case of `infinite_loop` is True: - * `is_running()` evaluates :obj:`False` or + Stop condition for loop in case of `infinite_loop` is :obj:`True`: + * `is_running()` evaluates :obj:`False` * or `stop_event` is set. - Stop condition for loop in case of `infinite_loop` is False: - * a call to `action_cb` succeeds or - * `is_running()` evaluates :obj:`False` or - * or `stop_event` is set or - * `max_retries` is reached. + Additional stop condition for loop in case of `infinite_loop` is :obj:`False`: + * a call to `action_cb` succeeds + * or `max_retries` is reached. Args: action_cb (:term:`coroutine function`): Network oriented callback function to call. @@ -89,7 +87,7 @@ async def network_retry_loop( * 0: No retries. * > 0: Number of retries - Must be negative if `infinite_loop` is set to True.. + Must be negative if `infinite_loop` is set to :obj:`True`. infinite_loop (:obj:`bool`): If :obj:`True`, the loop will run indefinitely until `is_running()` evaluates to :obj:`False` or `stop_event` is set. Otherwise, the loop will stop after a successful call to `action_cb`, or when `is_running()` evaluates to From 65e6cc9993c9ecf6a4954637023fb1318b2b7cd3 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Wed, 30 Jul 2025 21:22:08 +0200 Subject: [PATCH 3/9] Review: remove `infinite_loop` parameter --- src/telegram/ext/_application.py | 1 - src/telegram/ext/_updater.py | 3 --- src/telegram/ext/_utils/networkloop.py | 18 +++--------------- 3 files changed, 3 insertions(+), 19 deletions(-) diff --git a/src/telegram/ext/_application.py b/src/telegram/ext/_application.py index c0b064d2b30..51f739b3e27 100644 --- a/src/telegram/ext/_application.py +++ b/src/telegram/ext/_application.py @@ -1016,7 +1016,6 @@ async def _bootstrap_initialize(self, max_retries: int) -> None: description="Bootstrap Initialize Application", max_retries=max_retries, interval=1, - infinite_loop=False, ) def __run( diff --git a/src/telegram/ext/_updater.py b/src/telegram/ext/_updater.py index 212beffaf89..4fbf82128b4 100644 --- a/src/telegram/ext/_updater.py +++ b/src/telegram/ext/_updater.py @@ -382,7 +382,6 @@ def default_error_callback(exc: TelegramError) -> None: interval=poll_interval, stop_event=self.__polling_task_stop_event, max_retries=-1, - infinite_loop=True, ), name="Updater:start_polling:polling_task", ) @@ -711,7 +710,6 @@ async def bootstrap_set_webhook() -> None: interval=bootstrap_interval, stop_event=None, max_retries=max_retries, - infinite_loop=False, ) # Restore/set webhook settings, if needed. Again, we don't know ahead if a webhook is set, @@ -724,7 +722,6 @@ async def bootstrap_set_webhook() -> None: interval=bootstrap_interval, stop_event=None, max_retries=max_retries, - infinite_loop=False, ) async def stop(self) -> None: diff --git a/src/telegram/ext/_utils/networkloop.py b/src/telegram/ext/_utils/networkloop.py index 132dc8c15e2..cfa346bd791 100644 --- a/src/telegram/ext/_utils/networkloop.py +++ b/src/telegram/ext/_utils/networkloop.py @@ -50,15 +50,14 @@ async def network_retry_loop( stop_event: Optional[asyncio.Event] = None, is_running: Optional[Callable[[], bool]] = None, max_retries: int, - infinite_loop: bool, ) -> None: """Perform a loop calling `action_cb`, retrying after network errors. - Stop condition for loop in case of `infinite_loop` is :obj:`True`: + Stop condition for loop in case of ``max_retries < 0``: * `is_running()` evaluates :obj:`False` * or `stop_event` is set. - Additional stop condition for loop in case of `infinite_loop` is :obj:`False`: + Additional stop condition for loop in case of `max_retries >= 0``: * a call to `action_cb` succeeds * or `max_retries` is reached. @@ -87,19 +86,8 @@ async def network_retry_loop( * 0: No retries. * > 0: Number of retries - Must be negative if `infinite_loop` is set to :obj:`True`. - infinite_loop (:obj:`bool`): If :obj:`True`, the loop will run indefinitely until - `is_running()` evaluates to :obj:`False` or `stop_event` is set. Otherwise, the loop - will stop after a successful call to `action_cb`, or when `is_running()` evaluates to - :obj:`False`, or `stop_event` is set, or `max_retries` is reached. - """ - if infinite_loop and max_retries >= 0: - raise ValueError( - "max_retries must be negative if infinite_loop is True. " - "Use -1 for infinite retries." - ) - + infinite_loop = max_retries < 0 log_prefix = f"Network Retry Loop ({description}):" effective_is_running = is_running or (lambda: True) From 44c1ee3998586b888d4046285f70565b78693499 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Wed, 30 Jul 2025 21:40:52 +0200 Subject: [PATCH 4/9] Debug print - failing tests don't run on windows --- src/telegram/ext/_utils/networkloop.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/telegram/ext/_utils/networkloop.py b/src/telegram/ext/_utils/networkloop.py index cfa346bd791..e8c42fbc7e8 100644 --- a/src/telegram/ext/_utils/networkloop.py +++ b/src/telegram/ext/_utils/networkloop.py @@ -84,10 +84,13 @@ async def network_retry_loop( * < 0: Retry indefinitely. * 0: No retries. - * > 0: Number of retries + * > 0: Number of retries. """ - infinite_loop = max_retries < 0 + try: + infinite_loop = max_retries < 0 + except TypeError as exc: + raise TypeError(f"max_retries: {max_retries!r}, {type(max_retries)}") from exc log_prefix = f"Network Retry Loop ({description}):" effective_is_running = is_running or (lambda: True) @@ -123,7 +126,7 @@ async def do_action() -> bool: await do_action() if not infinite_loop: _LOGGER.debug("%s Action succeeded. Stopping loop.", log_prefix) - return + break except RetryAfter as exc: slack_time = 0.5 _LOGGER.info( From de044b3eb31e42bdef1ab4a68e96300bba7e7248 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Wed, 30 Jul 2025 21:58:31 +0200 Subject: [PATCH 5/9] try fixing tests --- tests/ext/test_application.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/ext/test_application.py b/tests/ext/test_application.py index c99a1311d27..875da3035a7 100644 --- a/tests/ext/test_application.py +++ b/tests/ext/test_application.py @@ -1715,6 +1715,7 @@ def thread_target(): expected = { name: name for name in updater_signature.parameters if name != "error_callback" } + expected["max_retries"] = 42 thread = Thread(target=thread_target) thread.start() app.run_polling(close_loop=False, **expected) @@ -2021,6 +2022,7 @@ def thread_target(): assert self.received[name] == param.default expected = {name: name for name in updater_signature.parameters if name != "self"} + expected["max_retries"] = 42 thread = Thread(target=thread_target) thread.start() app.run_webhook(close_loop=False, **expected) From 2410bf8a2c96f65eccf7bd164631839498ac72b2 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Wed, 30 Jul 2025 22:08:40 +0200 Subject: [PATCH 6/9] try fixing tests --- tests/ext/test_application.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ext/test_application.py b/tests/ext/test_application.py index 875da3035a7..ed856e2154b 100644 --- a/tests/ext/test_application.py +++ b/tests/ext/test_application.py @@ -1715,7 +1715,7 @@ def thread_target(): expected = { name: name for name in updater_signature.parameters if name != "error_callback" } - expected["max_retries"] = 42 + expected["bootstrap_retries"] = 42 thread = Thread(target=thread_target) thread.start() app.run_polling(close_loop=False, **expected) @@ -2022,7 +2022,7 @@ def thread_target(): assert self.received[name] == param.default expected = {name: name for name in updater_signature.parameters if name != "self"} - expected["max_retries"] = 42 + expected["bootstrap_retries"] = 42 thread = Thread(target=thread_target) thread.start() app.run_webhook(close_loop=False, **expected) From f0df08a80b3e9db9753f0f7051b98bed2418947a Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Wed, 30 Jul 2025 22:27:07 +0200 Subject: [PATCH 7/9] Remove debug print --- src/telegram/ext/_utils/networkloop.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/telegram/ext/_utils/networkloop.py b/src/telegram/ext/_utils/networkloop.py index e8c42fbc7e8..3f847cd26e7 100644 --- a/src/telegram/ext/_utils/networkloop.py +++ b/src/telegram/ext/_utils/networkloop.py @@ -87,10 +87,7 @@ async def network_retry_loop( * > 0: Number of retries. """ - try: - infinite_loop = max_retries < 0 - except TypeError as exc: - raise TypeError(f"max_retries: {max_retries!r}, {type(max_retries)}") from exc + infinite_loop = max_retries < 0 log_prefix = f"Network Retry Loop ({description}):" effective_is_running = is_running or (lambda: True) From a1a2099786f2f2c3f10f3152b18b3bd8e1bc3292 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Thu, 31 Jul 2025 19:31:03 +0200 Subject: [PATCH 8/9] Review --- src/telegram/ext/_utils/networkloop.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/telegram/ext/_utils/networkloop.py b/src/telegram/ext/_utils/networkloop.py index f0822f82ba0..b1c65610757 100644 --- a/src/telegram/ext/_utils/networkloop.py +++ b/src/telegram/ext/_utils/networkloop.py @@ -92,11 +92,10 @@ async def network_retry_loop( log_prefix = f"Network Retry Loop ({description}):" effective_is_running = is_running or (lambda: True) - async def do_action() -> bool: - """Return value indicating whether the action was successful.""" + async def do_action() -> None: if not stop_event: await action_cb() - return True + return action_cb_task = asyncio.create_task(action_cb()) stop_task = asyncio.create_task(stop_event.wait()) @@ -109,12 +108,10 @@ async def do_action() -> bool: if stop_task in done: _LOGGER.debug("%s Cancelled", log_prefix) - return False # Calling `result()` on `action_cb_task` will raise an exception if the task failed. # this is important to propagate the error to the caller. action_cb_task.result() - return True _LOGGER.debug("%s Starting", log_prefix) cur_interval = interval From 02902bfd2a893a67e44557e1fd312ee4700cac8a Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Thu, 31 Jul 2025 19:42:49 +0200 Subject: [PATCH 9/9] fix --- src/telegram/ext/_utils/networkloop.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/telegram/ext/_utils/networkloop.py b/src/telegram/ext/_utils/networkloop.py index b1c65610757..e59f12229db 100644 --- a/src/telegram/ext/_utils/networkloop.py +++ b/src/telegram/ext/_utils/networkloop.py @@ -108,6 +108,7 @@ async def do_action() -> None: if stop_task in done: _LOGGER.debug("%s Cancelled", log_prefix) + return # Calling `result()` on `action_cb_task` will raise an exception if the task failed. # this is important to propagate the error to the caller.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: