ドキュメントには、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)
改善できると思うことがいくつかあります。
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__
_を使用する場合は、なんらかの方法でコードを変更する必要はありません。
コードがこの警告メッセージをトリガーした後、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))