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

tests - add test for ResourceWarning: unclosed transport <asyncio.sslproto._SSLProtocolTransport object … #5146

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ Marco Paolini
Mariano Anaya
Mariusz Masztalerczuk
Marko Kohtala
Markus Kötter
Martijn Pieters
Martin Melka
Martin Richard
Expand Down
140 changes: 140 additions & 0 deletions tests/test_ssl_transport_close.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import asyncio
import platform

Check notice

Code scanning / CodeQL

Unused import Note test

Import of 'platform' is not used.
import ssl
import sys

Check notice

Code scanning / CodeQL

Unused import Note test

Import of 'sys' is not used.
import warnings
from typing import Any, Tuple, cast

import pytest

from aiohttp import TCPConnector, web
from aiohttp.client import ServerDisconnectedError
from aiohttp.pytest_plugin import AiohttpClient, AiohttpServer
from aiohttp.test_utils import TestClient, TestServer


async def handle_root(request: web.Request) -> web.Response:
"""respond after server was restarted, forcing connection_lost"""
cq = request.app["cq"]
dq = request.app["dq"]

await dq.put(0)

await cq.get()
cq.task_done()

return web.Response(text="")


async def _prepare(
aiohttp_server: Any,
aiohttp_client: Any,
ssl_ctx: ssl.SSLContext,
client_ssl_ctx: ssl.SSLContext,
cq: "asyncio.Queue[int]",
dq: "asyncio.Queue[int]",
) -> Tuple[TestServer, TestClient]:
commonism marked this conversation as resolved.
Show resolved Hide resolved
app = web.Application()
app["cq"] = cq
app["dq"] = dq
app.router.add_get("/", handle_root)
server = await aiohttp_server(app, port=0, ssl=ssl_ctx)

session = await aiohttp_client(server, connector=TCPConnector(ssl=client_ssl_ctx))

# replace SockSite … it is different and blocks
await dq.put(0)
await _restart(server.runner, ssl_ctx, session.port, cq, dq)
await cq.get()

return server, session


async def _restart(
runner: web.BaseRunner,
ssl_ctx: ssl.SSLContext,
port: int,
cq: "asyncio.Queue[int]",
dq: "asyncio.Queue[int]",
) -> None:
"""restart service to force connection_lost"""
await dq.get()
dq.task_done()
site = next(iter(runner.sites))
await site.stop()
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 3.12 it hangs here - you do not get

DEBUG aiohttp.server:web_protocol.py:434 Ignored premature client disconnection

as you get in 3.11

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see anything suspicious in there, unless the asyncio server is not closing:
https://github.com/commonism/aiohttp/blob/ebe2a48c282f2484ee1766af5c4d52ff3431b43a/aiohttp/web_runner.py#L83

Everything else looks like it has a 60s timeout, so would fail before the 5 min timeout we're seeing.

Copy link
Member

@Dreamsorcerer Dreamsorcerer Aug 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it is the asyncio server not closing, then we should get a cpython bug filed for the regression.

await cq.put(0)
site = web.TCPSite(
runner, "127.0.0.1", port=port, ssl_context=ssl_ctx, shutdown_timeout=1.0
Dreamsorcerer marked this conversation as resolved.
Show resolved Hide resolved
)
await site.start()


def _ssl_resource_warnings(w: warnings.WarningMessage) -> bool:
unclosed_transport_msg = (
"unclosed transport <asyncio.sslproto._SSLProtocolTransport object at"
)
return (
w.category == ResourceWarning
and w.filename.endswith("sslproto.py")
and cast(Warning, w.message).args[0].startswith(unclosed_transport_msg)
)


async def _run(
aiohttp_server: AiohttpServer,
aiohttp_client: AiohttpClient,
recwarn: pytest.WarningsRecorder,
ssl_ctx: ssl.SSLContext,
client_ssl_ctx: ssl.SSLContext,
cq: "asyncio.Queue[int]",
dq: "asyncio.Queue[int]",
) -> None:
"""run for two processed client requests"""
server, session = await _prepare(
aiohttp_server, aiohttp_client, ssl_ctx, client_ssl_ctx, cq, dq
)
assert server.runner is not None and session.port is not None
for i in range(3):
try:
jobs: Any = []
jobs.append(session.get("/"))
jobs.append(_restart(server.runner, ssl_ctx, session.port, cq, dq))
await asyncio.gather(*jobs)
except ServerDisconnectedError:
# Restarting the service will cause the client connections to fail
# this is expected and not a failure.
pass
Fixed Show fixed Hide fixed
finally:
await asyncio.sleep(0.1)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's the 300s ClientTimeout

    real_timeout = ClientTimeout(total=300, connect=None, sock_read=None, sock_connect=None, ceil_threshold=5)

300.26s call tests/test_ssl_transport_close.py::test_unclosed_transport_asyncio_sslproto_SSLProtocolTransport[pyloop]

increasing the sleep to something larger to have to server restart in time?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not seeing how the sleep relates to it, but we can try it if you think that's the problem.


assert not len(
list(filter(_ssl_resource_warnings, recwarn))
), "unclosed transport"


@pytest.mark.xfail(
sys.version_info < (3, 11) and platform.python_implementation() != "PyPy",
reason="Working on 3.11+ and Pypy.",
)
Dreamsorcerer marked this conversation as resolved.
Show resolved Hide resolved
def test_unclosed_transport_asyncio_sslproto_SSLProtocolTransport(
loop: asyncio.AbstractEventLoop,
aiohttp_server: AiohttpServer,
aiohttp_client: AiohttpClient,
recwarn: pytest.WarningsRecorder,
ssl_ctx: ssl.SSLContext,
client_ssl_ctx: ssl.SSLContext,
) -> None:
cq: "asyncio.Queue[int]" = asyncio.Queue()
dq: "asyncio.Queue[int]" = asyncio.Queue()
loop.set_debug(True)
loop.run_until_complete(
_run(
aiohttp_server,
aiohttp_client,
recwarn,
ssl_ctx,
client_ssl_ctx,
cq,
dq,
)
)
Loading