Client: Fix counter-intuitive support for retrying

To retry connection or a request N times the corresponding parameter had to be equal to N+1, since the initial attempt was also taken into account.
This commit is contained in:
Dmitry D. Chernov 2019-02-07 03:56:07 +10:00
parent c9e9b82eac
commit 11896bb0e5
4 changed files with 47 additions and 27 deletions

View File

@ -71,23 +71,23 @@ class TelegramBaseClient(abc.ABC):
invoked requests, and you should use ``asyncio.wait`` or invoked requests, and you should use ``asyncio.wait`` or
``asyncio.wait_for`` for that. ``asyncio.wait_for`` for that.
request_retries (`int`, optional): request_retries (`int` | `None`, optional):
How many times a request should be retried. Request are retried How many times a request should be retried. Request are retried
when Telegram is having internal issues (due to either when Telegram is having internal issues (due to either
``errors.ServerError`` or ``errors.RpcCallFailError``), ``errors.ServerError`` or ``errors.RpcCallFailError``),
when there is a ``errors.FloodWaitError`` less than when there is a ``errors.FloodWaitError`` less than
`flood_sleep_threshold`, or when there's a migrate error. `flood_sleep_threshold`, or when there's a migrate error.
May set to a false-y value (``0`` or ``None``) for infinite May take a negative or ``None`` value for infinite retries, but
retries, but this is not recommended, since some requests can this is not recommended, since some requests can always trigger
always trigger a call fail (such as searching for messages). a call fail (such as searching for messages).
connection_retries (`int`, optional): connection_retries (`int` | `None`, optional):
How many times the reconnection should retry, either on the How many times the reconnection should retry, either on the
initial connection or when Telegram disconnects us. May be initial connection or when Telegram disconnects us. May be
set to a false-y value (``0`` or ``None``) for infinite set to a negative or ``None`` value for infinite retries, but
retries, but this is not recommended, since the program can this is not recommended, since the program can get stuck in an
get stuck in an infinite loop. infinite loop.
retry_delay (`int` | `float`, optional): retry_delay (`int` | `float`, optional):
The delay in seconds to sleep between automatic reconnections. The delay in seconds to sleep between automatic reconnections.
@ -236,8 +236,8 @@ class TelegramBaseClient(abc.ABC):
self.api_id = int(api_id) self.api_id = int(api_id)
self.api_hash = api_hash self.api_hash = api_hash
self._request_retries = request_retries or sys.maxsize self._request_retries = request_retries
self._connection_retries = connection_retries or sys.maxsize self._connection_retries = connection_retries
self._retry_delay = retry_delay or 0 self._retry_delay = retry_delay or 0
self._proxy = proxy self._proxy = proxy
self._timeout = timeout self._timeout = timeout

View File

@ -6,6 +6,7 @@ from .telegrambaseclient import TelegramBaseClient
from .. import errors, utils from .. import errors, utils
from ..errors import MultiError, RPCError from ..errors import MultiError, RPCError
from ..tl import TLObject, TLRequest, types, functions from ..tl import TLObject, TLRequest, types, functions
from ..helpers import retry_range
_NOT_A_REQUEST = TypeError('You can only invoke requests, not types!') _NOT_A_REQUEST = TypeError('You can only invoke requests, not types!')
@ -34,7 +35,7 @@ class UserMethods(TelegramBaseClient):
request_index = 0 request_index = 0
self._last_request = time.time() self._last_request = time.time()
for _ in range(self._request_retries): for attempt in retry_range(self._request_retries):
try: try:
future = self._sender.send(request, ordered=ordered) future = self._sender.send(request, ordered=ordered)
if isinstance(future, list): if isinstance(future, list):
@ -86,7 +87,8 @@ class UserMethods(TelegramBaseClient):
raise raise
await self._switch_dc(e.new_dc) await self._switch_dc(e.new_dc)
raise ValueError('Number of retries reached 0') raise ValueError('Request was unsuccessful {} time(s)'
.format(attempt))
# region Public methods # region Public methods

View File

@ -72,6 +72,21 @@ def strip_text(text, entities):
return text return text
def retry_range(retries):
"""
Generates an integer sequence starting from 1. If `retries` is
negative or ``None`` then sequence is infinite, otherwise it will
end at `retries + 1`.
"""
yield 1
if retries is None:
retries = -1
attempt = 0
while attempt != retries:
attempt += 1
yield 1 + attempt
# endregion # endregion
# region Cryptographic related utils # region Cryptographic related utils

View File

@ -22,6 +22,7 @@ from ..tl.types import (
MsgsStateInfo, MsgsAllInfo, MsgResendReq, upload MsgsStateInfo, MsgsAllInfo, MsgResendReq, upload
) )
from ..crypto import AuthKey from ..crypto import AuthKey
from ..helpers import retry_range
def _cancellable(func): def _cancellable(func):
@ -211,26 +212,27 @@ class MTProtoSender:
receive loops. receive loops.
""" """
self._log.info('Connecting to %s...', self._connection) self._log.info('Connecting to %s...', self._connection)
for retry in range(1, self._retries + 1): for attempt in retry_range(self._retries):
try: try:
self._log.debug('Connection attempt {}...'.format(retry)) self._log.debug('Connection attempt {}...'.format(attempt))
await self._connection.connect(timeout=self._connect_timeout) await self._connection.connect(timeout=self._connect_timeout)
except (ConnectionError, asyncio.TimeoutError) as e: except (ConnectionError, asyncio.TimeoutError) as e:
self._log.warning('Attempt {} at connecting failed: {}: {}' self._log.warning('Attempt {} at connecting failed: {}: {}'
.format(retry, type(e).__name__, e)) .format(attempt, type(e).__name__, e))
await asyncio.sleep(self._delay) await asyncio.sleep(self._delay)
else: else:
break break
else: else:
raise ConnectionError('Connection to Telegram failed {} times' raise ConnectionError('Connection to Telegram failed {} time(s)'
.format(self._retries)) .format(attempt))
self._log.debug('Connection success!') self._log.debug('Connection success!')
if not self.auth_key: if not self.auth_key:
plain = MTProtoPlainSender(self._connection, loggers=self._loggers) plain = MTProtoPlainSender(self._connection, loggers=self._loggers)
for retry in range(1, self._retries + 1): for attempt in retry_range(self._retries):
try: try:
self._log.debug('New auth_key attempt {}...'.format(retry)) self._log.debug('New auth_key attempt {}...'
.format(attempt))
self.auth_key.key, self._state.time_offset =\ self.auth_key.key, self._state.time_offset =\
await authenticator.do_authentication(plain) await authenticator.do_authentication(plain)
@ -244,11 +246,11 @@ class MTProtoSender:
break break
except (SecurityError, AssertionError) as e: except (SecurityError, AssertionError) as e:
self._log.warning('Attempt {} at new auth_key failed: {}' self._log.warning('Attempt {} at new auth_key failed: {}'
.format(retry, e)) .format(attempt, e))
await asyncio.sleep(self._delay) await asyncio.sleep(self._delay)
else: else:
e = ConnectionError('auth_key generation failed {} times' e = ConnectionError('auth_key generation failed {} time(s)'
.format(self._retries)) .format(attempt))
self._disconnect(error=e) self._disconnect(error=e)
raise e raise e
@ -321,17 +323,17 @@ class MTProtoSender:
self._state.reset() self._state.reset()
retries = self._retries if self._auto_reconnect else 0 retries = self._retries if self._auto_reconnect else 0
for retry in range(1, retries + 1): for attempt in retry_range(retries):
try: try:
await self._connect() await self._connect()
except (ConnectionError, asyncio.TimeoutError) as e: except (ConnectionError, asyncio.TimeoutError) as e:
self._log.info('Failed reconnection retry %d/%d with %s', self._log.info('Failed reconnection attempt %d with %s',
retry, retries, e.__class__.__name__) attempt, e.__class__.__name__)
await asyncio.sleep(self._delay) await asyncio.sleep(self._delay)
except Exception: except Exception:
self._log.exception('Unexpected exception reconnecting on ' self._log.exception('Unexpected exception reconnecting on '
'retry %d/%d', retry, retries) 'attempt %d', attempt)
await asyncio.sleep(self._delay) await asyncio.sleep(self._delay)
else: else:
@ -343,7 +345,8 @@ class MTProtoSender:
break break
else: else:
self._log.error('Failed to reconnect automatically.') self._log.error('Automatic reconnection failed {} time(s)'
.format(attempt))
self._disconnect(error=ConnectionError()) self._disconnect(error=ConnectionError())
def _start_reconnect(self): def _start_reconnect(self):