Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timeouts checked once a second (?) #4850

Closed
haizaar opened this issue Jul 4, 2020 · 9 comments · Fixed by #5091
Closed

Timeouts checked once a second (?) #4850

haizaar opened this issue Jul 4, 2020 · 9 comments · Fixed by #5091
Labels

Comments

@haizaar
Copy link

haizaar commented Jul 4, 2020

🐞 Describe the bug

aiohttp.ClientTimeout.total is annotated to accept floats, yet my observations are that timeout condition is checked once a second. Is it a bug or feature? E.g. requests library does not have this limitation and easily works with 0.01 timeouts in my tests. I realize that sub-second timeouts are quite synthetic, but very useful for tests.

💡 To Reproduce
aiohttp 3.6.2 on Ubuntu 18.04 amd64 with CPython 3.8.3.

Consider the below code:

import asyncio
import socket
import sys
import time

import aiohttp
from aiohttp import web


async def hello(request):
    await asyncio.sleep(10)
    return web.Response(text="Hello, world")


async def run_client(port: int, timeout: float):
    async with aiohttp.ClientSession() as session:
        while True:
            start = time.monotonic()
            try:
                await session.get(f"http://localhost:{port}/",
                                  timeout=aiohttp.ClientTimeout(total=timeout))
            except asyncio.TimeoutError:
                pass
            took = time.monotonic() - start
            print(f"expected={timeout}, took={took:.3f}, skew={took-timeout:.3f}")


async def amain(timeout: float):
    app = web.Application()
    app.add_routes([web.get('/', hello)])
    runner = web.AppRunner(app)
    await runner.setup()

    sock = socket.socket(socket.AF_INET, type=socket.SOCK_STREAM)
    sock.bind(("localhost", 0))
    _, port = sock.getsockname()

    site = web.SockSite(runner=runner, sock=sock)
    await site.start()

    await run_client(port, timeout)


if __name__ == "__main__":
    asyncio.run(amain(float(sys.argv[1])))

No matter what the timeout value is, the actual timeout is always in multiples of 1 second as you can see in the took value below:

$ python /tmp/timeout-bug.py 0.1
expected=0.1, took=0.726, skew=0.626
expected=0.1, took=1.000, skew=0.900
expected=0.1, took=1.000, skew=0.900
python /tmp/timeout-bug.py 0.8
expected=0.8, took=1.414, skew=0.614
expected=0.8, took=0.999, skew=0.199
expected=0.8, took=1.000, skew=0.200
expected=0.8, took=1.001, skew=0.201
python /tmp/timeout-bug.py 1.1
expected=1.1, took=1.473, skew=0.373
expected=1.1, took=2.000, skew=0.900
expected=1.1, took=2.000, skew=0.900
expected=1.1, took=2.000, skew=0.900
python /tmp/timeout-bug.py 2.1
expected=2.1, took=2.158, skew=0.058
expected=2.1, took=2.999, skew=0.899
expected=2.1, took=3.001, skew=0.901
expected=2.1, took=2.998, skew=0.898

💡 Expected behavior
Is this intentional or a bug?


📋 **Your version of the Python**
<!-- Attach your version of the Python. -->
```console
$ python --version
Python 3.8.3
...

📋 Your version of the aiohttp/yarl/multidict distributions

$ python -m pip show aiohttp
Name: aiohttp
Version: 3.6.2
Summary: Async http client/server framework (asyncio)
Home-page: https://github.com/aio-libs/aiohttp
Author: Nikolay Kim
Author-email: [email protected]
License: Apache 2
Location: /home/.../lib/python3.8/site-packages
Requires: multidict, chardet, async-timeout, attrs, yarl
Required-by: 
$ python -m pip show multidict
Name: aiohttp
Version: 3.6.2
Summary: Async http client/server framework (asyncio)
Home-page: https://github.com/aio-libs/aiohttp
Author: Nikolay Kim
Author-email: [email protected]
License: Apache 2
Location: /home/.../lib/python3.8/site-packages
Requires: multidict, chardet, async-timeout, attrs, yarl
Required-by: 
$ python -m pip show multidict
Name: multidict
Version: 4.7.6
Summary: multidict implementation
Home-page: https://github.com/aio-libs/multidict
Author: Andrew Svetlov
Author-email: [email protected]
License: Apache 2
Location: /home/.../lib/python3.8/site-packages
Requires: 
Required-by: yarl, aiohttp
$ python -m pip show yarl
Name: yarl
Version: 1.4.2
Summary: Yet another URL library
Home-page: https://github.com/aio-libs/yarl/
Author: Andrew Svetlov
Author-email: [email protected]
License: Apache 2
Location: /home/.../lib/python3.8/site-packages
Requires: idna, multidict
Required-by: aiohttp

📋 Additional context

@asvetlov
Copy link
Member

asvetlov commented Jul 7, 2020

Thanks for the question!
The reason was the reducing amount of event loop wakeups when working with massive (thousands and millions) number of concurrent requests.
In the real world, tiny timeouts are not used AFAIK.

Rather rounding to milliseconds (or not rounding at all) I prefer configuring the precession by aiohttp.ClientTimeout. We can provide a float (e.g. 0.001 vs. 1.0) or a callable that accepts loop.time() and timeout and returns absolute timestamp when the timeout expires. By default, the implementation is:

def timeout_at(now: float, timeout: float) -> float:
    return ceil(now + timeout)

What do you think?

@haizaar
Copy link
Author

haizaar commented Jul 8, 2020

Hi Andrew,

I totally agree that very short timeouts are not used in real world scenarios but will reduce my test cycle from 15 seconds to 1 :) in my http-noah project.

If what you are referring to is as in #4851 then it looks very good to me - nothing will practically change for real-world timeouts and shorter ones will now work.

@luckydenis
Copy link

luckydenis commented Jul 8, 2020

Hi!
When rounding, the call processing time increases. This doesn't seem to be what we want. I looked at work after the function call and didn't notice a strong need for rounding.

Test stand

from random import uniform
from math import ceil, trunc


timeout = 0.1

def use_round():
    timer = uniform(1, 300)
    round(timer + timeout, 3)
    
    
def use_ceil():
    timer = uniform(1, 300)
    ceil(timer + timeout)
    

def use_castom():
    timer = uniform(1, 300)
    (ceil(timer + timeout) * 100) / 100

    
def use_not_round():
    timer = uniform(1, 300)
    timer + timeout
    
    
%timeit -n 10000 -r 100 use_not_round()
%timeit -n 10000 -r 100 use_ceil()
%timeit -n 10000 -r 100 use_castom()
%timeit -n 10000 -r 100 use_round()

stdin:

371 ns ± 97.9 ns per loop (mean ± std. dev. of 100 runs, 10000 loops each)
461 ns ± 53.9 ns per loop (mean ± std. dev. of 100 runs, 10000 loops each)
542 ns ± 95.2 ns per loop (mean ± std. dev. of 100 runs, 10000 loops each)
879 ns ± 146 ns per loop (mean ± std. dev. of 100 runs, 10000 loops each)

@haizaar
Copy link
Author

haizaar commented Jul 8, 2020

The code in #4851 does not suggest any rounds as far as I can tell. Am I missing anything?

Regardless, why do you think <0.5 microsecond is significant in real-world Python HTTP clients?

@luckydenis
Copy link

It seems to me that rounding is not required here, especially since we are losing the speed of calling the function. A little here, a little somewhere else, the overall performance is getting worse.

@asvetlov
Copy link
Member

asvetlov commented Jul 15, 2020

The question is not about the time of cell() vs round() performance but the number of the event loop wakeups per minute.
With many concurrent clients the loop can wakeup too often on timeouts, every look iteration is not for free.

The loop is overwhelmed by loop.call_later() with small delays, these delays point to different absolute times.
When cell-ing, the number of delays is the same but they are happening at predictable timestamps (many delays every second). It requires a single loop wakeup that handles multiple callbacks at once. This is the real optimization source, not the cell performance.

@haizaar
Copy link
Author

haizaar commented Jul 15, 2020

Thanks Andrew,
So to sum it up we are talking about today situation where tiny timeouts do not slow the server but are not truly honoured either vs the suggested change where large amounts of requests all using tiny timeouts can hinder loop effectiveness, right?

If so, I'm rather in favor of applying the suggested change. For normal workload nothing will change; for test workloads with tiny timeouts there would be no impact since the scale is typically small; for extremer corner case of large workload with tiny timeouts now developer at least will have a choice to prefer tiny timeouts and bite the impact or switch back to large timeouts in his app.

Maybe a slight improvement to the current proposal would be to still round/ceil loop.call_later() to the whole second but only for timeout values larger than, say, 2-3 seconds. This way absolutely nothing will change for the real workloads and aiohttp can still process multiple callbacks in a single wakeup in those cases.

WDYT?

@asvetlov
Copy link
Member

asvetlov commented Oct 17, 2020

Sorry for the pause.
Rounding for timeouts larger than 5 seconds only is the perfect solution (I prefer 5 over 2-3).
Disabling the rounding at all is not satisfactory because it degrades both server and client performance under high load (many concurrent requests are sent or received).

@haizaar
Copy link
Author

haizaar commented Oct 18, 2020

Happy to hear. @luckydenis do you want to proceed with your PR as per Andre's last comments?

netbsd-srcmastr referenced this issue in NetBSD/pkgsrc Oct 24, 2020
This fixes py-yarl in pkgsrc being too new for py-aiohttp.


3.7.0 (2020-10-24)
==================

Features
--------

- Response headers are now prepared prior to running ``on_response_prepare`` hooks, directly before headers are sent to the client.
  `#1958 <https://github.com/aio-libs/aiohttp/issues/1958>`_
- Add a ``quote_cookie`` option to ``CookieJar``, a way to skip quotation wrapping of cookies containing special characters.
  `#2571 <https://github.com/aio-libs/aiohttp/issues/2571>`_
- Call ``AccessLogger.log`` with the current exception available from ``sys.exc_info()``.
  `#3557 <https://github.com/aio-libs/aiohttp/issues/3557>`_
- `web.UrlDispatcher.add_routes` and `web.Application.add_routes` return a list
  of registered `AbstractRoute` instances. `AbstractRouteDef.register` (and all
  subclasses) return a list of registered resources registered resource.
  `#3866 <https://github.com/aio-libs/aiohttp/issues/3866>`_
- Added properties of default ClientSession params to ClientSession class so it is available for introspection
  `#3882 <https://github.com/aio-libs/aiohttp/issues/3882>`_
- Don't cancel web handler on peer disconnection, raise `OSError` on reading/writing instead.
  `#4080 <https://github.com/aio-libs/aiohttp/issues/4080>`_
- Implement BaseRequest.get_extra_info() to access a protocol transports' extra info.
  `#4189 <https://github.com/aio-libs/aiohttp/issues/4189>`_
- Added `ClientSession.timeout` property.
  `#4191 <https://github.com/aio-libs/aiohttp/issues/4191>`_
- allow use of SameSite in cookies.
  `#4224 <https://github.com/aio-libs/aiohttp/issues/4224>`_
- Use ``loop.sendfile()`` instead of custom implementation if available.
  `#4269 <https://github.com/aio-libs/aiohttp/issues/4269>`_
- Apply SO_REUSEADDR to test server's socket.
  `#4393 <https://github.com/aio-libs/aiohttp/issues/4393>`_
- Use .raw_host instead of slower .host in client API
  `#4402 <https://github.com/aio-libs/aiohttp/issues/4402>`_
- Allow configuring the buffer size of input stream by passing ``read_bufsize`` argument.
  `#4453 <https://github.com/aio-libs/aiohttp/issues/4453>`_
- Pass tests on Python 3.8 for Windows.
  `#4513 <https://github.com/aio-libs/aiohttp/issues/4513>`_
- Add `method` and `url` attributes to `TraceRequestChunkSentParams` and `TraceResponseChunkReceivedParams`.
  `#4674 <https://github.com/aio-libs/aiohttp/issues/4674>`_
- Add ClientResponse.ok property for checking status code under 400.
  `#4711 <https://github.com/aio-libs/aiohttp/issues/4711>`_
- Don't ceil timeouts that are smaller than 5 seconds.
  `#4850 <https://github.com/aio-libs/aiohttp/issues/4850>`_
- TCPSite now listens by default on all interfaces instead of just IPv4 when `None` is passed in as the host.
  `#4894 <https://github.com/aio-libs/aiohttp/issues/4894>`_
- Bump ``http_parser`` to 2.9.4
  `#5070 <https://github.com/aio-libs/aiohttp/issues/5070>`_


Bugfixes
--------

- Fix keepalive connections not being closed in time
  `#3296 <https://github.com/aio-libs/aiohttp/issues/3296>`_
- Fix failed websocket handshake leaving connection hanging.
  `#3380 <https://github.com/aio-libs/aiohttp/issues/3380>`_
- Fix tasks cancellation order on exit. The run_app task needs to be cancelled first for cleanup hooks to run with all tasks intact.
  `#3805 <https://github.com/aio-libs/aiohttp/issues/3805>`_
- Don't start heartbeat until _writer is set
  `#4062 <https://github.com/aio-libs/aiohttp/issues/4062>`_
- Fix handling of multipart file uploads without a content type.
  `#4089 <https://github.com/aio-libs/aiohttp/issues/4089>`_
- Preserve view handler function attributes across middlewares
  `#4174 <https://github.com/aio-libs/aiohttp/issues/4174>`_
- Fix the string representation of ``ServerDisconnectedError``.
  `#4175 <https://github.com/aio-libs/aiohttp/issues/4175>`_
- Raising RuntimeError when trying to get encoding from not read body
  `#4214 <https://github.com/aio-libs/aiohttp/issues/4214>`_
- Remove warning messages from noop.
  `#4282 <https://github.com/aio-libs/aiohttp/issues/4282>`_
- Raise ClientPayloadError if FormData re-processed.
  `#4345 <https://github.com/aio-libs/aiohttp/issues/4345>`_
- Fix a warning about unfinished task in ``web_protocol.py``
  `#4408 <https://github.com/aio-libs/aiohttp/issues/4408>`_
- Fixed 'deflate' compression. According to RFC 2616 now.
  `#4506 <https://github.com/aio-libs/aiohttp/issues/4506>`_
- Fixed OverflowError on platforms with 32-bit time_t
  `#4515 <https://github.com/aio-libs/aiohttp/issues/4515>`_
- Fixed request.body_exists returns wrong value for methods without body.
  `#4528 <https://github.com/aio-libs/aiohttp/issues/4528>`_
- Fix connecting to link-local IPv6 addresses.
  `#4554 <https://github.com/aio-libs/aiohttp/issues/4554>`_
- Fix a problem with connection waiters that are never awaited.
  `#4562 <https://github.com/aio-libs/aiohttp/issues/4562>`_
- Always make sure transport is not closing before reuse a connection.

  Reuse a protocol based on keepalive in headers is unreliable.
  For example, uWSGI will not support keepalive even it serves a
  HTTP 1.1 request, except explicitly configure uWSGI with a
  ``--http-keepalive`` option.

  Servers designed like uWSGI could cause aiohttp intermittently
  raise a ConnectionResetException when the protocol poll runs
  out and some protocol is reused.
  `#4587 <https://github.com/aio-libs/aiohttp/issues/4587>`_
- Handle the last CRLF correctly even if it is received via separate TCP segment.
  `#4630 <https://github.com/aio-libs/aiohttp/issues/4630>`_
- Fix the register_resource function to validate route name before splitting it so that route name can include python keywords.
  `#4691 <https://github.com/aio-libs/aiohttp/issues/4691>`_
- Improve typing annotations for ``web.Request``, ``aiohttp.ClientResponse`` and
  ``multipart`` module.
  `#4736 <https://github.com/aio-libs/aiohttp/issues/4736>`_
- Fix resolver task is not awaited when connector is cancelled
  `#4795 <https://github.com/aio-libs/aiohttp/issues/4795>`_
- Fix a bug "Aiohttp doesn't return any error on invalid request methods"
  `#4798 <https://github.com/aio-libs/aiohttp/issues/4798>`_
- Fix HEAD requests for static content.
  `#4809 <https://github.com/aio-libs/aiohttp/issues/4809>`_
- Fix incorrect size calculation for memoryview
  `#4890 <https://github.com/aio-libs/aiohttp/issues/4890>`_
- Add HTTPMove to _all__.
  `#4897 <https://github.com/aio-libs/aiohttp/issues/4897>`_
- Fixed the type annotations in the ``tracing`` module.
  `#4912 <https://github.com/aio-libs/aiohttp/issues/4912>`_
- Fix typing for multipart ``__aiter__``.
  `#4931 <https://github.com/aio-libs/aiohttp/issues/4931>`_
- Fix for race condition on connections in BaseConnector that leads to exceeding the connection limit.
  `#4936 <https://github.com/aio-libs/aiohttp/issues/4936>`_
- Add forced UTF-8 encoding for ``application/rdap+json`` responses.
  `#4938 <https://github.com/aio-libs/aiohttp/issues/4938>`_
- Fix inconsistency between Python and C http request parsers in parsing pct-encoded URL.
  `#4972 <https://github.com/aio-libs/aiohttp/issues/4972>`_
- Fix connection closing issue in HEAD request.
  `#5012 <https://github.com/aio-libs/aiohttp/issues/5012>`_
- Fix type hint on BaseRunner.addresses (from ``List[str]`` to ``List[Any]``)
  `#5086 <https://github.com/aio-libs/aiohttp/issues/5086>`_
- Make `web.run_app()` more responsive to Ctrl+C on Windows for Python < 3.8. It slightly
  increases CPU load as a side effect.
  `#5098 <https://github.com/aio-libs/aiohttp/issues/5098>`_


Improved Documentation
----------------------

- Fix example code in client quick-start
  `#3376 <https://github.com/aio-libs/aiohttp/issues/3376>`_
- Updated the docs so there is no contradiction in ``ttl_dns_cache`` default value
  `#3512 <https://github.com/aio-libs/aiohttp/issues/3512>`_
- Add 'Deploy with SSL' to docs.
  `#4201 <https://github.com/aio-libs/aiohttp/issues/4201>`_
- Change typing of the secure argument on StreamResponse.set_cookie from ``Optional[str]`` to ``Optional[bool]``
  `#4204 <https://github.com/aio-libs/aiohttp/issues/4204>`_
- Changes ``ttl_dns_cache`` type from int to Optional[int].
  `#4270 <https://github.com/aio-libs/aiohttp/issues/4270>`_
- Simplify README hello word example and add a documentation page for people coming from requests.
  `#4272 <https://github.com/aio-libs/aiohttp/issues/4272>`_
- Improve some code examples in the documentation involving websockets and starting a simple HTTP site with an AppRunner.
  `#4285 <https://github.com/aio-libs/aiohttp/issues/4285>`_
- Fix typo in code example in Multipart docs
  `#4312 <https://github.com/aio-libs/aiohttp/issues/4312>`_
- Fix code example in Multipart section.
  `#4314 <https://github.com/aio-libs/aiohttp/issues/4314>`_
- Update contributing guide so new contributors read the most recent version of that guide. Update command used to create test coverage reporting.
  `#4810 <https://github.com/aio-libs/aiohttp/issues/4810>`_
- Spelling: Change "canonize" to "canonicalize".
  `#4986 <https://github.com/aio-libs/aiohttp/issues/4986>`_
- Add ``aiohttp-sse-client`` library to third party usage list.
  `#5084 <https://github.com/aio-libs/aiohttp/issues/5084>`_


Misc
----

- `#2856 <https://github.com/aio-libs/aiohttp/issues/2856>`_, `#4218 <https://github.com/aio-libs/aiohttp/issues/4218>`_, `#4250 <https://github.com/aio-libs/aiohttp/issues/4250>`_
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants