Skip to content

Commit 4be5043

Browse files
committed
use normal handle for timeouts
1 parent a629d64 commit 4be5043

File tree

5 files changed

+102
-19
lines changed

5 files changed

+102
-19
lines changed

aiohttp/client.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from .client_ws import ClientWebSocketResponse
2020
from .cookiejar import CookieJar
2121
from .errors import WSServerHandshakeError
22-
from .helpers import TimeService
22+
from .helpers import TimeoutHandle, TimerContext, TimeService
2323

2424
__all__ = ('ClientSession', 'request', 'get', 'options', 'head',
2525
'delete', 'post', 'put', 'patch', 'ws_connect')
@@ -195,8 +195,10 @@ def _request(self, method, url, *,
195195

196196
# timeout is cumulative for all request operations
197197
# (request, redirects, responses, data consuming)
198-
timer = self._time_service.timeout(timeout)
198+
tm = TimeoutHandle(timeout)
199+
handle = tm.handle(self._loop)
199200

201+
timer = TimerContext(self._loop, tm)
200202
with timer:
201203
while True:
202204
url = URL(url).with_fragment(None)
@@ -273,6 +275,10 @@ def _request(self, method, url, *,
273275

274276
break
275277

278+
if resp.connection is not None:
279+
resp.connection.add_callback(handle.cancel)
280+
else:
281+
handle.cancel()
276282
resp._history = tuple(history)
277283
return resp
278284

aiohttp/client_reqrep.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import aiohttp
1515

1616
from . import hdrs, helpers, streams
17-
from .helpers import HeadersMixin, SimpleCookie, _TimeServiceTimeoutNoop
17+
from .helpers import HeadersMixin, SimpleCookie, TimerNoop
1818
from .log import client_logger
1919
from .multipart import MultipartWriter
2020
from .protocol import HttpMessage
@@ -86,7 +86,7 @@ def __init__(self, method, url, *,
8686
self.compress = compress
8787
self.loop = loop
8888
self.response_class = response_class or ClientResponse
89-
self._timer = timer if timer is not None else _TimeServiceTimeoutNoop()
89+
self._timer = timer if timer is not None else TimerNoop()
9090

9191
if loop.get_debug():
9292
self._source_traceback = traceback.extract_stack(sys._getframe(1))
@@ -526,7 +526,7 @@ def __init__(self, method, url, *,
526526
self._closed = False
527527
self._should_close = True # override by message.should_close later
528528
self._history = ()
529-
self._timer = timer if timer is not None else _TimeServiceTimeoutNoop()
529+
self._timer = timer if timer is not None else TimerNoop()
530530
self.cookies = SimpleCookie()
531531

532532
@property

aiohttp/connector.py

+20
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def __init__(self, connector, key, request, transport, protocol, loop):
4242
self._transport = transport
4343
self._protocol = protocol
4444
self._loop = loop
45+
self._callbacks = []
4546
self.reader = protocol.reader
4647
self.writer = protocol.writer
4748

@@ -68,25 +69,44 @@ def __del__(self, _warnings=warnings):
6869
context['source_traceback'] = self._source_traceback
6970
self._loop.call_exception_handler(context)
7071

72+
def add_callback(self, callback):
73+
if callback is not None:
74+
self._callbacks.append(callback)
75+
76+
def release_callbacks(self):
77+
callbacks, self._callbacks = self._callbacks[:], []
78+
79+
for cb in callbacks:
80+
try:
81+
cb()
82+
except:
83+
pass
84+
7185
@property
7286
def loop(self):
7387
return self._loop
7488

7589
def close(self):
90+
self.release_callbacks()
91+
7692
if self._transport is not None:
7793
self._connector._release(
7894
self._key, self._request, self._transport, self._protocol,
7995
should_close=True)
8096
self._transport = None
8197

8298
def release(self):
99+
self.release_callbacks()
100+
83101
if self._transport is not None:
84102
self._connector._release(
85103
self._key, self._request, self._transport, self._protocol,
86104
should_close=False)
87105
self._transport = None
88106

89107
def detach(self):
108+
self.release_callbacks()
109+
90110
if self._transport is not None:
91111
self._connector._release_acquired(self._key, self._transport)
92112
self._transport = None

aiohttp/helpers.py

+48-10
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import warnings
1616
from collections import MutableSequence, namedtuple
1717
from functools import total_ordering
18+
from math import ceil
1819
from pathlib import Path
1920
from time import gmtime
2021
from urllib.parse import urlencode
@@ -716,18 +717,18 @@ def timeout(self, timeout):
716717
717718
timeout - value in seconds or None to disable timeout logic
718719
"""
719-
if timeout:
720-
ctx = _TimeServiceTimeoutContext(self._loop)
720+
if timeout is not None and timeout > 0:
721+
ctx = TimerContext(self._loop)
721722
when = self._loop_time + timeout
722-
timer = TimerHandle(when, ctx.cancel, (), self._loop)
723+
timer = TimerHandle(when, ctx.timeout, (), self._loop)
723724
heapq.heappush(self._scheduled, timer)
724725
else:
725-
ctx = _TimeServiceTimeoutNoop()
726+
ctx = TimerNoop()
726727

727728
return ctx
728729

729730

730-
class _TimeServiceTimeoutNoop:
731+
class TimerNoop:
731732

732733
def __enter__(self):
733734
return self
@@ -736,14 +737,45 @@ def __exit__(self, exc_type, exc_val, exc_tb):
736737
return False
737738

738739

739-
class _TimeServiceTimeoutContext:
740+
class TimeoutHandle:
741+
""" Timeout handle """
742+
743+
def __init__(self, timeout):
744+
self._timeout = timeout
745+
self._callbacks = []
746+
747+
def register(self, callback, *args, **kwargs):
748+
self._callbacks.append((callback, args, kwargs))
749+
750+
def close(self):
751+
self._callbacks.clear()
752+
753+
def handle(self, loop):
754+
if self._timeout is not None and self._timeout > 0:
755+
at = ceil(loop.time() + self._timeout)
756+
return loop.call_at(at, self.__call__)
757+
758+
def __call__(self):
759+
for cb, args, kwargs in self._callbacks:
760+
try:
761+
cb(*args, **kwargs)
762+
except:
763+
pass
764+
765+
self._callbacks.clear()
766+
767+
768+
class TimerContext:
740769
""" Low resolution timeout context manager """
741770

742-
def __init__(self, loop):
771+
def __init__(self, loop, tm=None):
743772
self._loop = loop
744773
self._tasks = []
745774
self._cancelled = False
746775

776+
if tm is not None:
777+
tm.register(self.timeout)
778+
747779
def __enter__(self):
748780
task = asyncio.Task.current_task(loop=self._loop)
749781
if task is None:
@@ -759,17 +791,23 @@ def __enter__(self):
759791

760792
def __exit__(self, exc_type, exc_val, exc_tb):
761793
if self._tasks:
762-
self._tasks.pop()
794+
task = self._tasks.pop()
795+
else:
796+
task = None
763797

764798
if exc_type is asyncio.CancelledError and self._cancelled:
799+
for task in self._tasks:
800+
task.cancel()
765801
raise asyncio.TimeoutError from None
766802

767-
def cancel(self):
803+
if exc_type is None and self._cancelled and task is not None:
804+
task.cancel()
805+
806+
def timeout(self):
768807
if not self._cancelled:
769808
for task in self._tasks:
770809
task.cancel()
771810

772-
self._tasks = []
773811
self._cancelled = True
774812

775813

tests/test_client_functional.py

+23-4
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,11 @@ def handler(request):
533533

534534

535535
@asyncio.coroutine
536-
def test_timeout_on_reading_headers(loop, test_client):
536+
def test_timeout_on_reading_headers(loop, test_client, mocker):
537+
def ceil(val):
538+
return val
539+
540+
mocker.patch('aiohttp.helpers.ceil').side_effect = ceil
537541

538542
@asyncio.coroutine
539543
def handler(request):
@@ -551,7 +555,7 @@ def handler(request):
551555

552556

553557
@asyncio.coroutine
554-
def test_timeout_on_conn_reading_headers(loop, test_client):
558+
def test_timeout_on_conn_reading_headers(loop, test_client, mocker):
555559
# tests case where user did not set a connection timeout
556560

557561
@asyncio.coroutine
@@ -567,12 +571,17 @@ def handler(request):
567571
conn = aiohttp.TCPConnector(loop=loop)
568572
client = yield from test_client(app, connector=conn)
569573

574+
def ceil(val):
575+
return val
576+
577+
mocker.patch('aiohttp.helpers.ceil').side_effect = ceil
578+
570579
with pytest.raises(asyncio.TimeoutError):
571580
yield from client.get('/', timeout=0.01)
572581

573582

574583
@asyncio.coroutine
575-
def test_timeout_on_session_read_timeout(loop, test_client):
584+
def test_timeout_on_session_read_timeout(loop, test_client, mocker):
576585
@asyncio.coroutine
577586
def handler(request):
578587
resp = web.StreamResponse()
@@ -586,12 +595,22 @@ def handler(request):
586595
conn = aiohttp.TCPConnector(loop=loop)
587596
client = yield from test_client(app, connector=conn, read_timeout=0.01)
588597

598+
def ceil(val):
599+
return val
600+
601+
mocker.patch('aiohttp.helpers.ceil').side_effect = ceil
602+
589603
with pytest.raises(asyncio.TimeoutError):
590604
yield from client.get('/', timeout=None)
591605

592606

593607
@asyncio.coroutine
594-
def test_timeout_on_reading_data(loop, test_client):
608+
def test_timeout_on_reading_data(loop, test_client, mocker):
609+
610+
def ceil(val):
611+
return val
612+
613+
mocker.patch('aiohttp.helpers.ceil').side_effect = ceil
595614

596615
@asyncio.coroutine
597616
def handler(request):

0 commit comments

Comments
 (0)