map пула передаёт рабочему по одному элементу за раз. Сделайте этот элемент полной задачей, включая URL и выбранный прокси, чтобы рабочий не выводил свой маршрут из изменяющегося глобального списка.

Для конечного числа urls предоставьте непустой список проверенных URL прокси. Публичные кандидаты HTTP доступны в списке HTTP-прокси:

from concurrent.futures import ThreadPoolExecutor
from itertools import cycle
import requests

def fetch(job):
    url, proxy_url = job
    proxies = {"http": proxy_url, "https": proxy_url}
    with requests.get(url, proxies=proxies, timeout=(5, 15)) as response:
        response.raise_for_status()
        return url, response.text

if not proxy_urls:
    raise ValueError("No proxy endpoints were supplied")

jobs = zip(urls, cycle(proxy_urls))
with ThreadPoolExecutor(max_workers=5) as executor:
    for url, body in executor.map(fetch, jobs):
        handle_result(url, body)

urls, proxy_urls и handle_result предоставляет приложение. Координатор назначает конечные точки в повторяющемся порядке; каждый рабочий получает одну фиксированную пару. Перебор результатов также выявляет исключения рабочих.

Пять рабочих ограничивают активные задачи, а не активные задачи на прокси. Если в списке две конечные точки, несколько задач могут одновременно использовать одну и ту же конечную точку. При необходимости добавьте отдельное правило ёмкости для каждой конечной точки. Для одной активной задачи на URL конечной точки синхронизированная аренда очереди может резервировать и возвращать каждую запись.

Повторная попытка должна создавать новую ограниченную попытку для того же URL. Избегайте рекурсивных вызовов повторной попытки, которые удаляют записи из общего списка: другой рабочий может выбирать из него, а исчерпанный список всё равно требует явного результата.

Похожие статьи