mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 04:04:21 +02:00
Artwork fetches: TLS handshake within the deadline; cap stuck name lookups
The handshake runs after the watchdog can reach the TLS socket, with the remaining time as timeout, and the watchdog shuts the socket with the plain socket method. At most four lookups that outlived their deadline may run; more fail at once with a clear error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b9714fda45
commit
47266afe85
2 files changed
+51
-3
No files matched your search
@@ -196,6 +196,42 @@ class ArtworkTests(unittest.TestCase):
|
||||
self.assertLess(self.trickle(b'HTTP/1.1 200 OK\r\nContent-Length: 1000\r\n\r\n', 0.15), 0.4)
|
||||
self.assertLess(self.trickle(b'HTTP/1.1 200 OK\r\n', 0.15), 0.4) # headers never finish
|
||||
|
||||
def test_deadline_covers_a_stalled_tls_handshake(self):
|
||||
import socket
|
||||
from apk_sources import _images
|
||||
client, server = socket.socketpair()
|
||||
public = [(2, 1, 6, '', ('93.184.216.34', 443))]
|
||||
try:
|
||||
with patch.object(_images.socket, 'getaddrinfo', return_value=public), \
|
||||
patch.object(_images.socket, 'create_connection', return_value=client):
|
||||
start = time.monotonic()
|
||||
with self.assertRaisesRegex(_images.SourceError, 'too long'):
|
||||
_images.get('https://example.org/a.png', deadline=start + 0.25) # server never answers
|
||||
self.assertLess(time.monotonic() - start, 0.45)
|
||||
finally:
|
||||
server.close()
|
||||
|
||||
def test_timed_out_lookups_are_capped(self):
|
||||
import threading
|
||||
from apk_sources import _images
|
||||
gate = threading.Event()
|
||||
try:
|
||||
with patch.object(_images.socket, 'getaddrinfo', side_effect=lambda *a, **k: gate.wait(5) and []):
|
||||
errors = []
|
||||
for _ in range(6):
|
||||
try:
|
||||
_images.get('https://example.org/a.png', deadline=time.monotonic() + 0.05)
|
||||
except _images.SourceError as e:
|
||||
errors.append(str(e))
|
||||
self.assertEqual(sum('too long' in e for e in errors), 4)
|
||||
self.assertEqual(sum('Too many' in e for e in errors), 2)
|
||||
finally:
|
||||
gate.set()
|
||||
deadline = time.monotonic() + 5
|
||||
while time.monotonic() < deadline and not _images._resolvers.acquire(blocking=False):
|
||||
time.sleep(0.01)
|
||||
_images._resolvers.release() # the stuck lookups finished and gave their slots back
|
||||
|
||||
def test_deadline_covers_name_resolution(self):
|
||||
import threading
|
||||
from apk_sources import _images
|
||||
|
||||
@@ -69,8 +69,14 @@ def fetch(url, redirects=3, deadline=None, limit=MAX_IMAGE):
|
||||
return data, image_type(data)
|
||||
|
||||
|
||||
# Lookups that outlast their deadline keep running; cap them so they can't pile up.
|
||||
_resolvers = threading.BoundedSemaphore(4)
|
||||
|
||||
|
||||
def _resolve(host, port, timeout):
|
||||
# getaddrinfo has no timeout of its own; a thread keeps a slow resolver inside the budget.
|
||||
if not _resolvers.acquire(blocking=False):
|
||||
raise SourceError('Too many slow artwork name lookups are still running; try again shortly')
|
||||
found = {}
|
||||
|
||||
def run():
|
||||
@@ -78,6 +84,8 @@ def _resolve(host, port, timeout):
|
||||
found['addresses'] = socket.getaddrinfo(host, port, type=socket.SOCK_STREAM)
|
||||
except OSError as e:
|
||||
found['error'] = e
|
||||
finally:
|
||||
_resolvers.release()
|
||||
worker = threading.Thread(target=run, daemon=True)
|
||||
worker.start()
|
||||
worker.join(timeout)
|
||||
@@ -110,8 +118,8 @@ def get(url, headers=None, redirects=3, deadline=None, limit=MAX_IMAGE):
|
||||
live = [socket.create_connection((addresses[0][4][0], port), timeout=min(10, left()))]
|
||||
|
||||
def expire():
|
||||
try:
|
||||
live[0].shutdown(socket.SHUT_RDWR)
|
||||
try: # the plain socket method: it also ends a TLS handshake in progress
|
||||
socket.socket.shutdown(live[0], socket.SHUT_RDWR)
|
||||
except OSError:
|
||||
pass
|
||||
watchdog = threading.Timer(left(), expire)
|
||||
@@ -120,7 +128,11 @@ def get(url, headers=None, redirects=3, deadline=None, limit=MAX_IMAGE):
|
||||
conn = http.client.HTTPConnection(p.hostname, port, timeout=10)
|
||||
try:
|
||||
if p.scheme == 'https':
|
||||
live[0] = ssl.create_default_context().wrap_socket(live[0], server_hostname=p.hostname)
|
||||
# Handshake only once the watchdog can reach the TLS socket, within the remaining time.
|
||||
live[0] = ssl.create_default_context().wrap_socket(live[0], server_hostname=p.hostname,
|
||||
do_handshake_on_connect=False)
|
||||
live[0].settimeout(min(10, left()))
|
||||
live[0].do_handshake()
|
||||
conn.sock = live[0]
|
||||
path = p.path or '/'
|
||||
if p.query:
|
||||
|
||||
Reference in new issue
Block a user