Httpx 5 second delay

Open
#804 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
42/100
Issue type
Bug
Clarity
Needs clarification
Activity status
Quiet
Tech stack
python

Research direction

Start with the provided get_all_sites_health handler and perform_get_probe function, reproducing the concurrent requests against roughly 100 URLs with httpx and comparing them with aiohttp. Trace the DNS and connection behavior under this load; done means the cause of the repeated five-second delays and Docker DNS errors is identified and the affected behavior is covered by an appropriate regression check.

Written by the indexing model from the issue text.

Description

Originally opened by @dhensen on 2023-12-15 22:05:14 in encode/httpx

Hi all,

Httpx version: I'm still on httpx 24.1 because I had to downgrade because of https://github.com/encode/httpx/pull/2929

Expectation:

I expect my http calls not to get delayed by a constant 5 seconds.

Actual:

My http calls are delayed with a constant 5 seconds, plus I get DNS related logs from dockerd.

Long story

I noticed in my system logs weird docker dns issues:

Click to see logs
Dec 15 22:53:44 my-hostname dockerd[1601]: time="2023-12-15T22:53:44.285539223+01:00" level=error msg="[resolver] failed to query DNS server: 192.168.50.1:53, query: ;gameboy2023.my-site-ommitted.nl.\tIN\t AAAA" error="read udp 172.28.0.3:53713->192.168.50.1:53: i/o timeout"
Dec 15 22:53:44 my-hostname dockerd[1601]: time="2023-12-15T22:53:44.296076826+01:00" level=error msg="[resolver] failed to query DNS server: 1.1.1.1:53, query: ;ht.my-site-ommitted.nl.\tIN\t A" error="read udp 172.28.0.105:43077->1.1.1.1:53: i/o timeout"
Dec 15 22:53:44 my-hostname dockerd[1601]: time="2023-12-15T22:53:44.296120525+01:00" level=error msg="[resolver] failed to query DNS server: 1.1.1.1:53, query: ;ht.my-site-ommitted.nl.\tIN\t AAAA" error="read udp 172.28.0.105:60254->1.1.1.1:53: i/o timeout"
Dec 15 22:53:44 my-hostname dockerd[1601]: time="2023-12-15T22:53:44.305453453+01:00" level=error msg="[resolver] failed to query DNS server: 192.168.50.1:53, query: ;theultimatenerdconf.my-site-ommitted.nl.\tIN\t AAAA" error="read udp 172.28.0.3:49108->192.168.50.1:53: i/o timeout"
Dec 15 22:53:48 my-hostname dockerd[1601]: time="2023-12-15T22:53:48.364826590+01:00" level=error msg="[resolver] failed to query DNS server: 192.168.50.1:53, query: ;zongaatonder.my-site-ommitted.nl.\tIN\t AAAA" error="read udp 172.28.0.3:48923->192.168.50.1:53: i/o timeout"
Dec 15 22:53:48 my-hostname dockerd[1601]: time="2023-12-15T22:53:48.366090453+01:00" level=error msg="[resolver] failed to query DNS server: 192.168.50.1:53, query: ;top10movies.my-site-ommitted.nl.\tIN\t AAAA" error="read udp 172.28.0.3:56201->192.168.50.1:53: i/o timeout"
Dec 15 22:53:52 my-hostname dockerd[1601]: time="2023-12-15T22:53:52.496770882+01:00" level=error msg="[resolver] failed to query DNS server: 192.168.50.1:53, query: ;gameboy2023.my-site-ommitted.nl.\tIN\t AAAA" error="read udp 172.28.0.3:49707->192.168.50.1:53: i/o timeout"
Dec 15 22:53:52 my-hostname dockerd[1601]: time="2023-12-15T22:53:52.508179669+01:00" level=error msg="[resolver] failed to query DNS server: 192.168.50.1:53, query: ;ergotherapie2.my-site-ommitted.nl.\tIN\t AAAA" error="read udp 172.28.0.3:51178->192.168.50.1:53: i/o timeout"
Dec 15 22:53:56 my-hostname dockerd[1601]: time="2023-12-15T22:53:56.515096544+01:00" level=error msg="[resolver] failed to query DNS server: 192.168.50.1:53, query: ;theultimatenerdconf.my-site-ommitted.nl.\tIN\t AAAA" error="read udp 172.28.0.3:33374->192.168.50.1:53: i/o timeout"
Dec 15 22:53:56 my-hostname dockerd[1601]: time="2023-12-15T22:53:56.590845767+01:00" level=error msg="[resolver] failed to query DNS server: 192.168.50.1:53, query: ;zongaatonder.my-site-ommitted.nl.\tIN\t AAAA" error="read udp 172.28.0.3:60863->192.168.50.1:53: i/o timeout"

It always used to happen when I was doing health probes on about a 100 websites asynchronous as the same moment. I discovered this issue 2 weeks ago, but today got the time to dig into this. I suspected it was httpx, but I had no proof other than running into long timeouts for http calls to sites that were supposed to be fast.

I also ran while true; do dig "https://$(tr -dc 'a-zA-Z0-9' </dev/urandom | head -c 10).my-site-ommittedd.nl"; done on my host and inside docker on that same host to see if there is a docker related DNS issue. Running this large amount of DNS lookups do not result into the same system logs I saw from when my api was doing lookups using httpx. So httpx became my prime suspect.

I then changed up my implementation to retrieve all my site urls (there are a little over 100 in db) and make it so I can swap httpx for aiohttp (I'm sorry, I can image reading this hurts 💔 )

With aiohttp my problem disappears and I get normal response times. All 106 response times are below 1.2 seconds, with the lowest being 0.07.

With httpx about 64 out of 106 come with the 5.xx seconds (out of 106 calls only 1 fails, so no TimeoutException) AND system logs appear. Now I think those logs are trying to tell that the dns query wasn't read, hence the i/o timeout at the end of each log line.

So my bug is about what is httpx doing with regards to DNS? What could lead to the issues I'm describing.

This is an excerpt of my code (based on Sanic):

async def get_all_site_urls(conn):
    async with conn.cursor(aiomysql.DictCursor) as cur:
        await cur.execute("SELECT site_url FROM site_config")
        site_configs = await cur.fetchall()
        return [x["site_url"] for x in site_configs]


`@app`.get("/all_sites_health/<client_type>", name="get all sites health")
async def get_all_sites_health(request, client_type: str):
    site_urls = await get_all_site_urls(request.ctx.conn)
    # TODO set better timeouts on teh http clients, use defaults for now
    if client_type == "aiohttp":
        http_client_cls = aiohttp.ClientSession
        # timeout 5 seconds per conn/read/write/pool
    else:
        http_client_cls = httpx.AsyncClient
        # timeout 300 seconds by default

    async with http_client_cls() as http_client:
        tasks = []
        for url in site_urls:
            task = asyncio.create_task(perform_get_probe(http_client, url))
            tasks.append(task)
        results = await asyncio.gather(*tasks)
    return response.json({"res": results})


async def perform_get_probe(http_client, site_url: str):
    start = perf_counter()
    try:
        res = await http_client.get(site_url)
        try:
            healthy = res.status_code == 200  # httpx
        except AttributeError:
            healthy = res.status == 200  # aiohttp
    except (httpx.RequestError, aiohttp.ClientError):
        healthy = False

    duration = perf_counter() - start
    return site_url, healthy, duration
Dominant language
Python
Stars
1.6k
Forks
103
Avg merge
1d 19h
Merged PRs (30d)
25

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from pydantic/httpx2

All issues in pydantic/httpx2

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.