Skip to content

aiohttp client timeout does not work #3175

Closed
@lorien

Description

Long story short

I use ClientTimeout(total=0.1 to process request that takes 0.5 seconds and no exception is raised.

Expected behaviour

I expect to raise exception in 100% times

Actual behaviour

Sometimes it raise exception (concurrent.futures._base.TimeoutError), sometimes it works and ignores timeout option:

127.0.0.1 - - [06/Aug/2018 10:50:55] "GET / HTTP/1.1" 200 -
TRACE {'total': 511.8, 'is_redirect': False, 'dns_lookup_and_dial': 0, 'connect': 5.67, 'transfer': 506.13}

By looking at TRACE object it is clear that request takes more time than specified in ClientTimeout(total= option

Steps to reproduce

  • Save content of two files on your disk
  • Run python3 test.py

Script test.py to run:

import time
import asyncio
import aiohttp
from threading import Thread
from http.server import HTTPServer, BaseHTTPRequestHandler

from trace import request_tracer

PORT = 9876


class HomePageRequestHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        time.sleep(0.5)
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b'foo')


def server_thread():
    server = HTTPServer(('127.0.0.1', PORT), HomePageRequestHandler)
    server.serve_forever()


async def make_request():
    trace = {}
    timeout = aiohttp.ClientTimeout(total=0.1)
    url = 'http://127.0.0.1:%d/' % PORT
    async with aiohttp.ClientSession(trace_configs=[request_tracer(trace)]) as session:
        io_res = await session.request('get', url, timeout=timeout)
        body = await io_res.read()
        assert body == b'foo'
        print('TRACE', trace)


def main():
    th = Thread(target=server_thread)
    th.daemon = True
    th.start()
    time.sleep(0.2) # time to start server

    loop = asyncio.get_event_loop()
    loop.run_until_complete(make_request())


if __name__ == '__main__':
    main()

trace.py module:

import aiohttp


def request_tracer(results_collector):
    """
    Provides request tracing to aiohttp client sessions.
    :param results_collector: a dict to which the tracing results will be added.
    :return: an aiohttp.TraceConfig object.
    :example:
    >>> import asyncio
    >>> import aiohttp
    >>> from aiohttp_trace import request_tracer
    >>>
    >>>
    >>> async def func():
    >>>     trace = {}
    >>>     async with aiohttp.ClientSession(trace_configs=[request_tracer(trace)]) as client:
    >>>         async with client.get('https://github.com') as response:
    >>>             print(trace)
    >>>
    >>> asyncio.get_event_loop().run_until_complete(func())
    {'dns_lookup_and_dial': 43.3, 'connect': 334.29, 'transfer': 148.48, 'total': 526.08, 'is_redirect': False}
    """

    async def on_request_start(session, context, params):
        context.on_request_start = session.loop.time()
        context.is_redirect = False

    async def on_connection_create_start(session, context, params):
        since_start = session.loop.time() - context.on_request_start
        context.on_connection_create_start = since_start

    async def on_request_redirect(session, context, params):
        since_start = session.loop.time() - context.on_request_start
        context.on_request_redirect = since_start
        context.is_redirect = True

    async def on_dns_resolvehost_start(session, context, params):
        since_start = session.loop.time() - context.on_request_start
        context.on_dns_resolvehost_start = since_start

    async def on_dns_resolvehost_end(session, context, params):
        since_start = session.loop.time() - context.on_request_start
        context.on_dns_resolvehost_end = since_start

    async def on_connection_create_end(session, context, params):
        since_start = session.loop.time() - context.on_request_start
        context.on_connection_create_end = since_start
    
    async def on_request_chunk_sent(session, context, params):
        since_start = session.loop.time() - context.on_request_start
        context.on_request_chunk_sent = since_start

    async def on_request_end(session, context, params):
        total = session.loop.time() - context.on_request_start
        context.on_request_end = total

        try:
            dns_lookup_and_dial = context.on_dns_resolvehost_end - context.on_dns_resolvehost_start
        except AttributeError:
            dns_lookup_and_dial = 0

        connect = context.on_connection_create_end - dns_lookup_and_dial
        transfer = total - context.on_connection_create_end
        is_redirect = context.is_redirect

        results_collector['dns_lookup_and_dial'] = round(dns_lookup_and_dial * 1000, 2)
        results_collector['connect'] = round(connect * 1000, 2)
        results_collector['transfer'] = round(transfer * 1000, 2)
        results_collector['total'] = round(total * 1000, 2)
        results_collector['is_redirect'] = is_redirect

    trace_config = aiohttp.TraceConfig()

    trace_config.on_request_start.append(on_request_start)
    trace_config.on_request_redirect.append(on_request_redirect)
    trace_config.on_dns_resolvehost_start.append(on_dns_resolvehost_start)
    trace_config.on_dns_resolvehost_end.append(on_dns_resolvehost_end)
    trace_config.on_connection_create_start.append(on_connection_create_start)
    trace_config.on_connection_create_end.append(on_connection_create_end)
    trace_config.on_request_end.append(on_request_end)
    trace_config.on_request_chunk_sent.append(on_request_chunk_sent)

    return trace_config

Your environment

OS: Linux air 4.9.0-1-amd64 #1 SMP Debian 4.9.6-3 (2017-01-28) x86_64 GNU/Linux
python: 3.5.3
aiohttp: 3.3.2

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions