Что именно не так с моими асинхронными запросами?

Ссылка скопирована
1 ответ

По вводным: у меня есть бот который взаимодействует с api.
В коде что я представил:
По вводным: 1. Класс Requester - моя попытка переделать запросы на асинхронные с ограничением 20 в сек.
Сейчас ситуация такая: 2. Функция которая ждет от api список id и повторяет запрос несколько раз если сервис возвращает не полный список в csv.
По вводным: теперь я вижу какое то странное поведение - запрос может отправиться сразу, а может зависнуть на несколько минут. Если запрос ожидает 10 id все нормально работает, а если должно прийти 100 id - то я могу ждать целую минуту (до переделок нормально работало)

Сейчас ситуация такая: что я сделал не так? Как будто запрос при 100 id заклинивает. Может потому что при 10 сервис сразу грузит, а при 100 может понужнобиться вторая попытка запроса.

class Requester: def __init__(self, max_per_second: int = 20): self.limiter = AsyncLimiter(max_per_second, 1.0) self.client = httpx.AsyncClient(timeout=30.0) async def get(self, url, **kwargs): async with self.limiter: response = await self.client.get(url, **kwargs) print(response.text) return response.text async def post(self, url, **kwargs): async with self.limiter: response = await self.client.post(url, **kwargs) print(response.text) return response.text async def close(self): await self.client.aclose()
async def get_call_ids(self, phonelist_id, country, need_len): url = (f"https://zvonok.com/manager/cabapi_external/api/v1/phonelists/call_ids_by_phonelist/" f"?public_key={s.d[f'zvonok_api_{country}']}" f"&campaign_id={s.d[f'zvonok_id_{country}']}" f"&phonelist_id={phonelist_id}") logger.info(f"Получение call_ids. {phonelist_id} {country} {url}") tryes = 15 while tryes: try: print('отправка получения ид') resp = await self.requester.get(url) print('ид получены') if resp: f = StringIO(resp) data = [] reader = csv.DictReader(f) for row in reader: data.append([row['phoneval'], row['ats_call_id']]) if len(data) < need_len: if tryes == 1: logger.info(f"Возврат меньшего количества номеров {len(data)}") return data logger.info(f"В ответе не все номера - {len(data)} из {need_len}") await sleep(3) raise Exception except Exception as e: traceback.print_exc() tryes -= 1 return False
Нужно решить такую задачу?

Опишите проблему, и специалист поможет с настройкой, исправлением ошибки или доработкой сайта. Подберём понятный план работ без лишней переписки.

Заказать помощь
Лучший ответ
1
Mobile-редакция Ответ

По описанию проблема похожа на смешение rate limit, повторных запросов и ожидания ответа API без нормальных timeout/backoff. Если при 10 id все быстро, а при 100 id запрос может “зависнуть”, то вероятно API обрабатывает большие выборки дольше, возвращает неполный CSV, а ваш код держит семафор/лимитер или сессию так, что следующие попытки ждут не там, где вы ожидаете.

Для aiohttp/httpx важно иметь одну ClientSession на пачку запросов, явный timeout, ограничение параллелизма и понятный retry. AsyncLimiter ограничивает частоту входа в блок, но не ограничивает количество одновременно висящих запросов, если API отвечает долго. Поэтому часто нужен еще asyncio.Semaphore.

import asyncio
import aiohttp
from aiolimiter import AsyncLimiter
 
class Requester:
    def __init__(self, max_per_second=20, concurrency=10):
        self.limiter = AsyncLimiter(max_per_second, 1)
        self.sem = asyncio.Semaphore(concurrency)
        self.timeout = aiohttp.ClientTimeout(total=60, connect=10)
 
    async def get(self, session, url, params=None):
        async with self.sem:
            async with self.limiter:
                async with session.get(url, params=params, timeout=self.timeout) as resp:
                    resp.raise_for_status()
                    return await resp.text()
 
    async def run_many(self, jobs):
        async with aiohttp.ClientSession(timeout=self.timeout) as session:
            return await asyncio.gather(*[
                self.get(session, url, params) for url, params in jobs
            ])

import asyncio import aiohttp from aiolimiter import AsyncLimiter class Requester: def __init__(self, max_per_second=20, concurrency=10): self.limiter = AsyncLimiter(max_per_second, 1) self.sem = asyncio.Semaphore(concurrency) self.timeout = aiohttp.ClientTimeout(total=60, connect=10) async def get(self, session, url, params=None): async with self.sem: async with self.limiter: async with session.get(url, params=params, timeout=self.timeout) as resp: resp.raise_for_status() return await resp.text() async def run_many(self, jobs): async with aiohttp.ClientSession(timeout=self.timeout) as session: return await asyncio.gather(*[ self.get(session, url, params) for url, params in jobs ])

Если API для 100 id сначала готовит результат, а потом возвращает неполный список, лучше не делать слепые повторы без паузы. Добавьте backoff и логируйте размер ответа, статус, время выполнения и номер попытки.

for attempt in range(1, 5):
    started = time.monotonic()
    text = await requester.get(session, url, params)
    print('attempt', attempt, 'seconds', time.monotonic() - started, 'bytes', len(text))
    if is_complete(text):
        return text
    await asyncio.sleep(min(2 ** attempt, 20))

for attempt in range(1, 5): started = time.monotonic() text = await requester.get(session, url, params) print('attempt', attempt, 'seconds', time.monotonic() - started, 'bytes', len(text)) if is_complete(text): return text await asyncio.sleep(min(2 ** attempt, 20))

Еще проверьте, нет ли ошибки “создаю новую сессию на каждый запрос”. Это резко ухудшает скорость. И не парсите тяжелый CSV синхронно внутри event loop, если он большой: вынесите CPU-тяжелую обработку в отдельный поток или делайте после получения всех ответов.

Вывод: разделите rate limit и concurrency, добавьте timeout, одну ClientSession, backoff и подробные логи. Тогда станет видно, где задержка: API долго готовит 100 id, ваш retry ждет неполный результат или код блокирует event loop обработкой CSV.

Другие ответы (0)

Пока нет других ответов. Будьте первым, кто поможет автору.

Ответить на вопрос

комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Вам также может быть интересно