web-dev-qa-db-ja.com

aiohttp ClientSessionプールを再利用する方法は?

ドキュメントには、ClientSessionを再利用すると書かれています。

リクエストごとにセッションを作成しないでください。ほとんどの場合、すべての要求をまとめて実行するアプリケーションごとのセッションが必要です。

セッションには内部に接続プールが含まれ、接続の再利用とキープアライブ(両方ともデフォルトでオン)により、全体的なパフォーマンスが向上する可能性があります。 1

しかし、これを行う方法についての説明はドキュメントにないようですか?関連する可能性のある例が1つありますが、他の場所でプールを再利用する方法は示されていません: http://aiohttp.readthedocs.io/en/stable/client.html#keep-alive-connection-pooling-および-cookie-sharing

このようなことはそれを行う正しい方法でしょうか?

@app.listener('before_server_start')
async def before_server_start(app, loop):
    app.pg_pool = await asyncpg.create_pool(**DB_CONFIG, loop=loop, max_size=100)
    app.http_session_pool = aiohttp.ClientSession()


@app.listener('after_server_stop')
async def after_server_stop(app, loop):
    app.http_session_pool.close()
    app.pg_pool.close()


@app.post("/api/register")
async def register(request):
    # json validation
    async with app.pg_pool.acquire() as pg:
        await pg.execute()  # create unactivated user in db
        async with app.http_session_pool as session:
            # TODO send activation email using SES API
            async with session.post('http://httpbin.org/post', data=b'data') as resp:
                print(resp.status)
                print(await resp.text())
        return HTTPResponse(status=204)
13
dvtan

改善できると思うことがいくつかあります。

1)

ClientSessionのインスタンスは1つのセッションオブジェクトです。このオンセッションには接続のプールが含まれていますが、それ自体は「session_pool」ではありません。 _http_session_pool_の名前を_http_session_に変更するか、_client_session_にすることをお勧めします。

2)

セッションのclose()メソッド コルーチンです 。あなたはそれを待つべきです:

_await app.client_session.close()
_

またはさらに良い(IMHO)、セッションを適切に開閉する方法を考える代わりに、___aenter___/___aexit___を待っている標準の非同期コンテキストマネージャーを使用します。

_@app.listener('before_server_start')
async def before_server_start(app, loop):
    # ...
    app.client_session = await aiohttp.ClientSession().__aenter__()


@app.listener('after_server_stop')
async def after_server_stop(app, loop):
    await app.client_session.__aexit__(None, None, None)
    # ...
_

3)

注意してください この情報

ただし、基になる接続が閉じられる前にイベントループが停止すると、_ResourceWarning: unclosed transport_警告が発行されます(警告が有効になっている場合)。

この状況を回避するには、イベントループを閉じる前に小さな遅延を追加して、開いている基になる接続をすべて閉じることができるようにする必要があります。

あなたの場合に必須かどうかはわかりませんが、ドキュメントのアドバイスとして_after_server_stop_内にawait asyncio.sleep(0)を追加しても問題はありません。

_@app.listener('after_server_stop')
async def after_server_stop(app, loop):
    # ...
    await asyncio.sleep(0)  # http://aiohttp.readthedocs.io/en/stable/client.html#graceful-shutdown
_

更新:

___aenter___/___aexit___を実装するクラスは 非同期コンテキストマネージャー として使用できます(_async with_ステートメントで使用できます)。内部ブロックを実行する前と実行した後に、いくつかのアクションを実行できます。これは通常のコンテキストマネージャーと非常に似ていますが、asyncioに関連しています。通常のコンテキストマネージャーの非同期と同じように、手動で_async with_/___aenter___を待機して直接(___aexit___なしで)使用できます。

たとえば、close()を使用する代わりに、___aenter___/___aexit___を手動で使用してセッションを作成/解放する方がよいと思うのはなぜですか? ___aenter___/___aexit___内で実際に何が起こるかを心配する必要はないからです。 aiohttpの将来のバージョンで、セッションの作成が変更され、たとえばopen()を待つ必要があると想像してください。 ___aenter___/___aexit___を使用する場合は、なんらかの方法でコードを変更する必要はありません。

6

コードがこの警告メッセージをトリガーした後、aiohttp ClientSessionインスタンスを再利用する方法についてGoogleで検索した後、この質問を見つけました:UserWarning:コルーチンの外部でクライアントセッションを作成することは非常に危険なアイデアです

このコードは関連していますが、上記の問題を解決できない場合があります。私はasyncioとaiohttpを初めて使用するため、これはベストプラクティスではない可能性があります。一見矛盾する情報をたくさん読んだ後、私が思いつくことができた最高のものです。

コンテキストを開くPython docsから取得したクラスResourceManagerを作成しました。

ResourceManagerインスタンスは、BaseScraper.set_sessionおよびBaseScraper.close_sessionラッパーメソッドを使用して、マジックメソッド__aenter__および__aexit__を介してaiohttpClientSessionインスタンスのオープンとクローズを処理します。

次のコードでClientSessionインスタンスを再利用できました。

BaseScraperクラスには、認証用のメソッドもあります。これは、lxmlサードパーティパッケージに依存します。

import asyncio
from time import time
from contextlib import contextmanager, AbstractContextManager, ExitStack

import aiohttp
import lxml.html


class ResourceManager(AbstractContextManager):
    # Code taken from Python docs: 29.6.2.4. of https://docs.python.org/3.6/library/contextlib.html

    def __init__(self, scraper, check_resource_ok=None):
        self.acquire_resource = scraper.acquire_resource
        self.release_resource = scraper.release_resource
        if check_resource_ok is None:

            def check_resource_ok(resource):
                return True

        self.check_resource_ok = check_resource_ok

    @contextmanager
    def _cleanup_on_error(self):
        with ExitStack() as stack:
            stack.Push(self)
            yield
            # The validation check passed and didn't raise an exception
            # Accordingly, we want to keep the resource, and pass it
            # back to our caller
            stack.pop_all()

    def __enter__(self):
        resource = self.acquire_resource()
        with self._cleanup_on_error():
            if not self.check_resource_ok(resource):
                msg = "Failed validation for {!r}"
                raise RuntimeError(msg.format(resource))
        return resource

    def __exit__(self, *exc_details):
        # We don't need to duplicate any of our resource release logic
        self.release_resource()


class BaseScraper:
    login_url = ""
    login_data = dict()  # dict of key, value pairs to fill the login form
    loop = asyncio.get_event_loop()

    def __init__(self, urls):
        self.urls = urls
        self.acquire_resource = self.set_session
        self.release_resource = self.close_session

    async def _set_session(self):
        self.session = await aiohttp.ClientSession().__aenter__()

    def set_session(self):
        set_session_attr = self.loop.create_task(self._set_session())
        self.loop.run_until_complete(set_session_attr)
        return self  # variable after "as" becomes instance of BaseScraper

    async def _close_session(self):
        await self.session.__aexit__(None, None, None)

    def close_session(self):
        close_session = self.loop.create_task(self._close_session())
        self.loop.run_until_complete(close_session)

    def __call__(self):
        fetch_urls = self.loop.create_task(self._fetch())
        return self.loop.run_until_complete(fetch_urls)

    async def _get(self, url):
        async with self.session.get(url) as response:
            result = await response.read()
        return url, result

    async def _fetch(self):
        tasks = (self.loop.create_task(self._get(url)) for url in self.urls)
        start = time()
        results = await asyncio.gather(*tasks)
        print(
            "time elapsed: {} seconds \nurls count: {}".format(
                time() - start, len(urls)
            )
        )
        return results

    @property
    def form(self):
        """Create and return form for authentication."""
        form = aiohttp.FormData(self.login_data)
        get_login_page = self.loop.create_task(self._get(self.login_url))
        url, login_page = self.loop.run_until_complete(get_login_page)

        login_html = lxml.html.fromstring(login_page)
        hidden_inputs = login_html.xpath(r'//form//input[@type="hidden"]')
        login_form = {x.attrib["name"]: x.attrib["value"] for x in hidden_inputs}
        for key, value in login_form.items():
            form.add_field(key, value)
        return form

    async def _login(self, form):
        async with self.session.post(self.login_url, data=form) as response:
            if response.status != 200:
                response.raise_for_status()
            print("logged into {}".format(url))
            await response.release()

    def login(self):
        post_login_form = self.loop.create_task(self._login(self.form))
        self.loop.run_until_complete(post_login_form)


if __name__ == "__main__":
    urls = ("http://example.com",) * 10
    base_scraper = BaseScraper(urls)
    with ResourceManager(base_scraper) as scraper:
        for url, html in scraper():
            print(url, len(html))
1
dmmfll