Fix a couple of inconsistencies in the public interface (#1102)

* Create `_NOT_A_REQUEST` when needed. Currently, modifications    
  in the raised exception would be "global".    
* `retries` parameters were actually attempts. This has been fixed    
  to actually be the amount of retries, so 0 now means don't retry.    
* Helper function to deal with retries instead of using a range with    
  different styles every time.
This commit is contained in:
Dmitry D. Chernov 2019-02-07 04:41:45 +10:00 committed by Lonami
parent c9e9b82eac
commit c8f16a4e89
5 changed files with 50 additions and 30 deletions

View File

@ -53,7 +53,7 @@ class _TakeoutClient:
wrapped = [] wrapped = []
for r in requests: for r in requests:
if not isinstance(r, TLRequest): if not isinstance(r, TLRequest):
raise _NOT_A_REQUEST raise _NOT_A_REQUEST()
await r.resolve(self, utils) await r.resolve(self, utils)
wrapped.append(functions.InvokeWithTakeoutRequest(takeout_id, r)) wrapped.append(functions.InvokeWithTakeoutRequest(takeout_id, r))

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,8 +6,9 @@ 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 = lambda: TypeError('You can only invoke requests, not types!')
class UserMethods(TelegramBaseClient): class UserMethods(TelegramBaseClient):
@ -15,7 +16,7 @@ class UserMethods(TelegramBaseClient):
requests = (request if utils.is_list_like(request) else (request,)) requests = (request if utils.is_list_like(request) else (request,))
for r in requests: for r in requests:
if not isinstance(r, TLRequest): if not isinstance(r, TLRequest):
raise _NOT_A_REQUEST raise _NOT_A_REQUEST()
await r.resolve(self, utils) await r.resolve(self, utils)
# Avoid making the request if it's already in a flood wait # Avoid making the request if it's already in a flood wait
@ -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):