Skip to content

Commit 91a1983

Browse files
authored
fix: resolve core runtime bugs in tester, scheduler and proxy parsing (#229)
- utils/proxy: fix ValueError on multi-colon proxy strings in the str branch of convert_proxy_or_proxies, which made /random return 500 (#218) - scheduler: guard None process handles in finally so ENABLE_*=false no longer raises AttributeError (#62) - tester: replace deprecated get_event_loop()/asyncio.wait() with a fresh loop + asyncio.gather to fix 'Passing coroutines is forbidden' on Python 3.10+ (#191, #201) - tester: reuse one aiohttp session with a bounded connector instead of one per proxy, reducing memory growth and fd exhaustion (#119, #155) - tester: catch ClientResponseError/ContentTypeError + return_exceptions so bad httpbin responses no longer raise 'Task exception was never retrieved' (#208)
1 parent dc32b04 commit 91a1983

3 files changed

Lines changed: 114 additions & 86 deletions

File tree

proxypool/processors/tester.py

Lines changed: 104 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from proxypool.storages.redis import RedisClient
66
from proxypool.setting import TEST_TIMEOUT, TEST_BATCH, TEST_URL, TEST_VALID_STATUS, TEST_ANONYMOUS, \
77
TEST_DONT_SET_MAX_SCORE
8-
from aiohttp import ClientProxyConnectionError, ServerDisconnectedError, ClientOSError, ClientHttpProxyError
8+
from aiohttp import ClientProxyConnectionError, ServerDisconnectedError, ClientOSError, ClientHttpProxyError, \
9+
ClientResponseError, ContentTypeError
910
from asyncio import TimeoutError
1011
from proxypool.testers import __all__ as testers_cls
1112

@@ -16,6 +17,8 @@
1617
ServerDisconnectedError,
1718
ClientOSError,
1819
ClientHttpProxyError,
20+
ClientResponseError,
21+
ContentTypeError,
1922
AssertionError
2023
)
2124

@@ -30,83 +33,102 @@ def __init__(self):
3033
init redis
3134
"""
3235
self.redis = RedisClient()
33-
self.loop = asyncio.get_event_loop()
3436
self.testers_cls = testers_cls
3537
self.testers = [tester_cls() for tester_cls in self.testers_cls]
3638

37-
async def test(self, proxy: Proxy):
39+
async def test(self, proxy: Proxy, session: aiohttp.ClientSession):
3840
"""
3941
test single proxy
4042
:param proxy: Proxy object
43+
:param session: shared aiohttp session
4144
:return:
4245
"""
43-
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
44-
try:
45-
logger.debug(f'testing {proxy.string()}')
46-
# if TEST_ANONYMOUS is True, make sure that
47-
# the proxy has the effect of hiding the real IP
48-
# logger.debug(f'TEST_ANONYMOUS {TEST_ANONYMOUS}')
49-
if TEST_ANONYMOUS:
50-
url = 'https://httpbin.org/ip'
51-
async with session.get(url, timeout=TEST_TIMEOUT) as response:
52-
resp_json = await response.json()
53-
origin_ip = resp_json['origin']
54-
# logger.debug(f'origin ip is {origin_ip}')
55-
async with session.get(url, proxy=f'http://{proxy.string()}', timeout=TEST_TIMEOUT) as response:
56-
resp_json = await response.json()
57-
anonymous_ip = resp_json['origin']
58-
logger.debug(f'anonymous ip is {anonymous_ip}')
59-
assert origin_ip != anonymous_ip
60-
assert proxy.host == anonymous_ip
61-
async with session.get(TEST_URL, proxy=f'http://{proxy.string()}', timeout=TEST_TIMEOUT,
62-
allow_redirects=False) as response:
63-
if response.status in TEST_VALID_STATUS:
64-
if TEST_DONT_SET_MAX_SCORE:
65-
logger.debug(
66-
f'proxy {proxy.string()} is valid, remain current score')
67-
else:
68-
self.redis.max(proxy)
69-
logger.debug(
70-
f'proxy {proxy.string()} is valid, set max score')
46+
try:
47+
logger.debug(f'testing {proxy.string()}')
48+
# if TEST_ANONYMOUS is True, make sure that
49+
# the proxy has the effect of hiding the real IP
50+
# logger.debug(f'TEST_ANONYMOUS {TEST_ANONYMOUS}')
51+
if TEST_ANONYMOUS:
52+
url = 'https://httpbin.org/ip'
53+
async with session.get(url, timeout=TEST_TIMEOUT) as response:
54+
resp_json = await response.json()
55+
origin_ip = resp_json['origin']
56+
# logger.debug(f'origin ip is {origin_ip}')
57+
async with session.get(url, proxy=f'http://{proxy.string()}', timeout=TEST_TIMEOUT) as response:
58+
resp_json = await response.json()
59+
anonymous_ip = resp_json['origin']
60+
logger.debug(f'anonymous ip is {anonymous_ip}')
61+
assert origin_ip != anonymous_ip
62+
assert proxy.host == anonymous_ip
63+
async with session.get(TEST_URL, proxy=f'http://{proxy.string()}', timeout=TEST_TIMEOUT,
64+
allow_redirects=False) as response:
65+
if response.status in TEST_VALID_STATUS:
66+
if TEST_DONT_SET_MAX_SCORE:
67+
logger.debug(
68+
f'proxy {proxy.string()} is valid, remain current score')
7169
else:
72-
self.redis.decrease(proxy)
70+
self.redis.max(proxy)
7371
logger.debug(
74-
f'proxy {proxy.string()} is invalid, decrease score')
75-
# if independent tester class found, create new set of storage and do the extra test
76-
for tester in self.testers:
77-
key = tester.key
78-
if self.redis.exists(proxy, key):
79-
test_url = tester.test_url
80-
headers = tester.headers()
81-
cookies = tester.cookies()
82-
async with session.get(test_url, proxy=f'http://{proxy.string()}',
83-
timeout=TEST_TIMEOUT,
84-
headers=headers,
85-
cookies=cookies,
86-
allow_redirects=False) as response:
87-
resp_text = await response.text()
88-
is_valid = await tester.parse(resp_text, test_url, proxy.string())
89-
if is_valid:
90-
if tester.test_dont_set_max_score:
91-
logger.info(
92-
f'key[{key}] proxy {proxy.string()} is valid, remain current score')
93-
else:
94-
self.redis.max(
95-
proxy, key, tester.proxy_score_max)
96-
logger.info(
97-
f'key[{key}] proxy {proxy.string()} is valid, set max score')
72+
f'proxy {proxy.string()} is valid, set max score')
73+
else:
74+
self.redis.decrease(proxy)
75+
logger.debug(
76+
f'proxy {proxy.string()} is invalid, decrease score')
77+
# if independent tester class found, create new set of storage and do the extra test
78+
for tester in self.testers:
79+
key = tester.key
80+
if self.redis.exists(proxy, key):
81+
test_url = tester.test_url
82+
headers = tester.headers()
83+
cookies = tester.cookies()
84+
async with session.get(test_url, proxy=f'http://{proxy.string()}',
85+
timeout=TEST_TIMEOUT,
86+
headers=headers,
87+
cookies=cookies,
88+
allow_redirects=False) as response:
89+
resp_text = await response.text()
90+
is_valid = await tester.parse(resp_text, test_url, proxy.string())
91+
if is_valid:
92+
if tester.test_dont_set_max_score:
93+
logger.info(
94+
f'key[{key}] proxy {proxy.string()} is valid, remain current score')
9895
else:
99-
self.redis.decrease(
100-
proxy, tester.key, tester.proxy_score_min)
96+
self.redis.max(
97+
proxy, key, tester.proxy_score_max)
10198
logger.info(
102-
f'key[{key}] proxy {proxy.string()} is invalid, decrease score')
99+
f'key[{key}] proxy {proxy.string()} is valid, set max score')
100+
else:
101+
self.redis.decrease(
102+
proxy, tester.key, tester.proxy_score_min)
103+
logger.info(
104+
f'key[{key}] proxy {proxy.string()} is invalid, decrease score')
103105

104-
except EXCEPTIONS:
105-
self.redis.decrease(proxy)
106-
[self.redis.decrease(proxy, tester.key, tester.proxy_score_min)
107-
for tester in self.testers]
106+
except EXCEPTIONS:
107+
self.redis.decrease(proxy)
108+
[self.redis.decrease(proxy, tester.key, tester.proxy_score_min)
109+
for tester in self.testers]
110+
logger.debug(
111+
f'proxy {proxy.string()} is invalid, decrease score')
112+
113+
async def run_tests(self):
114+
"""
115+
test all proxies in batches, reusing a single aiohttp session
116+
:return:
117+
"""
118+
count = self.redis.count()
119+
logger.debug(f'{count} proxies to test')
120+
cursor = 0
121+
connector = aiohttp.TCPConnector(ssl=False, limit=TEST_BATCH)
122+
async with aiohttp.ClientSession(connector=connector) as session:
123+
while True:
108124
logger.debug(
109-
f'proxy {proxy.string()} is invalid, decrease score')
125+
f'testing proxies use cursor {cursor}, count {TEST_BATCH}')
126+
cursor, proxies = self.redis.batch(cursor, count=TEST_BATCH)
127+
if proxies:
128+
tasks = [self.test(proxy, session) for proxy in proxies]
129+
await asyncio.gather(*tasks, return_exceptions=True)
130+
if not cursor:
131+
break
110132

111133
@logger.catch
112134
def run(self):
@@ -116,26 +138,29 @@ def run(self):
116138
"""
117139
# event loop of aiohttp
118140
logger.info('stating tester...')
119-
count = self.redis.count()
120-
logger.debug(f'{count} proxies to test')
121-
cursor = 0
122-
while True:
123-
logger.debug(
124-
f'testing proxies use cursor {cursor}, count {TEST_BATCH}')
125-
cursor, proxies = self.redis.batch(cursor, count=TEST_BATCH)
126-
if proxies:
127-
tasks = [self.loop.create_task(
128-
self.test(proxy)) for proxy in proxies]
129-
self.loop.run_until_complete(asyncio.wait(tasks))
130-
if not cursor:
131-
break
141+
loop = asyncio.new_event_loop()
142+
asyncio.set_event_loop(loop)
143+
try:
144+
loop.run_until_complete(self.run_tests())
145+
finally:
146+
loop.close()
132147

133148

134149
def run_tester():
135150
host = '96.113.165.182'
136151
port = '3128'
137-
tasks = [tester.test(Proxy(host=host, port=port))]
138-
tester.loop.run_until_complete(asyncio.wait(tasks))
152+
tester = Tester()
153+
154+
async def _test():
155+
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
156+
await tester.test(Proxy(host=host, port=port), session)
157+
158+
loop = asyncio.new_event_loop()
159+
asyncio.set_event_loop(loop)
160+
try:
161+
loop.run_until_complete(_test())
162+
finally:
163+
loop.close()
139164

140165

141166
if __name__ == '__main__':

proxypool/scheduler.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,15 @@ def run(self):
129129
tester_process and tester_process.join()
130130
getter_process and getter_process.join()
131131
server_process and server_process.join()
132-
logger.info(
133-
f'tester is {"alive" if tester_process.is_alive() else "dead"}')
134-
logger.info(
135-
f'getter is {"alive" if getter_process.is_alive() else "dead"}')
136-
logger.info(
137-
f'server is {"alive" if server_process.is_alive() else "dead"}')
132+
if tester_process:
133+
logger.info(
134+
f'tester is {"alive" if tester_process.is_alive() else "dead"}')
135+
if getter_process:
136+
logger.info(
137+
f'getter is {"alive" if getter_process.is_alive() else "dead"}')
138+
if server_process:
139+
logger.info(
140+
f'server is {"alive" if server_process.is_alive() else "dead"}')
138141
logger.info('proxy terminated')
139142

140143

proxypool/utils/proxy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def convert_proxy_or_proxies(data):
6363
if is_auth_proxy(data):
6464
host, port = extract_auth_proxy(data)
6565
else:
66-
host, port = data.split(':')
66+
host, port, *_ = data.split(':')
6767
return Proxy(host=host, port=int(port))
6868

6969

0 commit comments

Comments
 (0)