-
-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Add warning for creation a session outside of coroutine #1468
Conversation
Current coverage is 98.83% (diff: 100%)@@ master #1468 diff @@
==========================================
Files 30 30
Lines 6955 6961 +6
Methods 0 0
Messages 0 0
Branches 1152 1154 +2
==========================================
+ Hits 6874 6880 +6
Misses 40 40
Partials 41 41
|
Can this change possibly be reverted or maybe thought out a little more? I have HTTP Clients in my library where I create a session object in class HTTP:
def __init__(self):
self.session = aiohttp.ClientSession(...) and then close it at some later point in time when necessary for clean-up. With this new warning my users are now getting a noisy warning and reporting bugs to me about it when there is nothing actually wrong. At best the current way to fix this is to recommend my users to shut off warnings via the filters (or do it myself programmatically) but I don't really think that's a good idea. |
How do you create your |
I create an HTTP instance inside a different class. I don't really know how to explain it without detailing the entire library's architecture. The session is maintained by an I should note that sessions are properly cleaned up. So all this warning does is bother my users and cause annoying bug reports. I already lock the aiohttp version to <1.1.0 but people without virtualenv will still update anyway and I've received reports from those people. As much as I wish I could just |
Well you could always make an Helper coroutine for that class like so: class HTTP:
def __init__(self, loop=None, *args, **kwargs):
self.loop = asyncio.get_event_loop() if loop is None else loop
self.session = self.loop.run_until_complete(self._create_session(*args, **kwargs))
async def _create_session(self, *args, **kwargs):
"""creates a ClientSession inside of a coroutine."""
session = aiohttp.ClientSession(*args, **kwargs)
return session It might need modified to fit your library needs so. At least it is an idea. Looking at the actual class that does it it could be simple just do the following changes to the above: class HTTPClient:
def __init__(self, connector=None, *, loop=None):
self.loop = asyncio.get_event_loop() if loop is None else loop
self.connector = connector
self.session = self.loop.run_until_complete(self._create_session())
# other library specific stuff.
@asyncio.coroutine
def _create_session(self):
"""creates a ClientSession inside of a coroutine."""
session = aiohttp.ClientSession(connector=self.connector, loop=self.loop)
return session
# recreate function change:
def recreate(self):
self.session = self.loop.run_until_complete(self._create_session()) This way you get the following:
I do not like the fact that there is no |
...wow! Nice work, AraHaan.
Kind regards,
*Paul Reiber*http://about.me/reiber
…On Fri, Dec 23, 2016 at 2:30 PM, AraHaan ***@***.***> wrote:
Well you could always make an Helper coroutine for that class like so:
class HTTP:
def __init__(self):
self.session = asyncio.create_task(self._create_session(), loop=loop)
async def _create_session(self, *args, **kwargs):
"""creates an ClientSession inside of a coroutine."""
session = await aiohttp.ClientSession(*args, **kwargs)
return session
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#1468 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AADlQBjXwXB-oOwHjLBAH2wKKjIneJtWks5rLEufgaJpZM4LI7Ww>
.
|
@AraHaan So what you're suggesting is that I have to create an To actually make use of your session, I would have to either call Honestly this is overall a really poor workaround. I also can't comprehend what the 'recreate' part is all about. |
The recreate part is to basically recreate the client session just like your recreate method does without having to reinitialize the class which can cause in most cases a Resource or Memory leak because the old instance is not getting destroyed. Also edited it above to instead use |
@Rapptz just so you know on your HTTP client class on the recreate method of yours you did not delete the old client session instance before reinitializing it after it has been closed thereby it can gradually create a memory leak. you might need to consider deleting it before creating the new session instance on that method. |
Well, a warning might be omitted if loop is passed to |
hmm oddly they are passing in a loop that they use when they call their HTTP client but still the warning shows for some reason. Or at least I think they are passing the loop to their http class. Yep I have verified by looking inside Rapptz's client.py the warning still persists and inside their self.loop = asyncio.get_event_loop() if loop is None else loop
self.http = HTTPClient(connector, loop=self.loop) and then HTTPClient lazy inits ClientSession like so: def __init__(self, connector=None, *, loop=None):
self.loop = asyncio.get_event_loop() if loop is None else loop
self.connector = connector
self.session = aiohttp.ClientSession(connector=connector, loop=self.loop) |
Sorry, I was not clean.
The main intention for adding the warning is prevention silly error-prone code like the following:
This code is just not testable. |
@asvetlov removing the warning if an explicit loop is given is good enough for me (and it's what people should be doing anyway). |
I was thinking of PRing the changes myself since this issue is becoming worse and worse on my end with the support questions. However I stumbled across a roadblock while doing so. The logic for detecting the loop can be initialised in two different branches and it's not really clear if something like this should raise a warning (and if so, how): s = aiohttp.ClientSession(connector=aiohttp.TCPConnector()) There's technically no explicit loop passed, so I wasn't sure how to PR the changes with this bikeshed in my mind. I was thinking of just a menial check of s = aiohttp.ClientSession(connector=aiohttp.TCPConnector(loop=some_loop)) And I'm not sure if that counts as "explicit loop passing" under the criteria of shutting off the warnings. Any help would be appreciated. |
Care to explain how is it any more dangerous than creating any other "asyncio object" outside of the coroutine, like |
When you create session or TCPConnector objects, they capture current loop.
If for some reason you set new loop, those objects would stop work.
Sure, you can make it work, but you need to be more careful. asyncio.Lock,Queue,Semaphore suffer from same problem
… On Feb 22, 2017, at 11:30 AM, WGH ***@***.***> wrote:
Care to explain how is it any more dangerous than creating any other "asyncio object" outside of the coroutine, like asyncio.Lock, asyncio.Queue, asyncio.Whatever or anything from third-party libraries?
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub <#1468 (comment)>, or mute the thread <https://github.com/notifications/unsubscribe-auth/AAkjzl7owWk2tGIjU1H0gtF-3temUj-Cks5rfIzDgaJpZM4LI7Ww>.
|
Isn't the best solution to simply reproduce what class HTTP:
def __init__(self):
pass # replace by any classic __init__ method
async def __aenter__(self, *your_args, **your_kwargs):
self.session = await aiohttp.ClientSession().__aenter__()
return self
async def __aexit__(self, *args, **kwargs):
await self.session.__aexit__(*args, **kwargs) Then you can just do: async with HTTP() as http:
http.get("http://api.mathjs.org/v4/", params={'expr' : '4^2'}) I don't know if there is a clean way to "force" users to use class HTTP:
def __init__(self):
self.session = None
async def __aenter__(self, *your_args, **your_kwargs):
self.session = await aiohttp.ClientSession().__aenter__()
return self
async def __aexit__(self, *args, **kwargs):
await self.session.__aexit__(*args, **kwargs)
# Check if there is an active session, if not use a single-request session:
async def get(self, *args, **kwargs):
if self.session is None:
async with aiohttp.ClientSession() as session:
print("One shot session.")
return await self._get_with_session(session, *args, **kwargs)
else:
print("Long lasting session.")
return await self._get_with_session(self.session, *args, **kwargs)
async def _get_with_session(self, session, url, params):
async with session.get(url, params=params) as response:
return await response.text() That way, both the following piece of code would work: http = HTTP()
await http.get("http://api.mathjs.org/v4/", params={'expr' : '3^2'}))
await http.get("http://api.mathjs.org/v4/", params={'expr' : '4^2'}) async with HTTP() as http:
await http.get("http://api.mathjs.org/v4/", params={'expr' : '3^2'})
await http.get("http://api.mathjs.org/v4/", params={'expr' : '4^2'}) I feel like this is a good solution as it requires no coding effort and is probably easy to maintain as you just delegate everything to the session object. EDIT If you want to add the active session automatically if it exists or feed a new one if it doesn't for several services ( import asyncio, aiohttp
import functools
from abc import ABCMeta, abstractmethod
def attach_session(method):
@functools.wraps(method)
async def method_with_session(self, *args, **kwargs):
if self.session is None:
async with aiohttp.ClientSession() as session:
print("{} using a one shot session.".format(method.__name__))
return await method(self, *args, session=session, **kwargs)
else:
print("{} using the long lasting session {}.".format(method.__name__, self.session))
return await method(self, *args, session=self.session, **kwargs)
return method_with_session
class AbstractSessionContainer(metaclass=ABCMeta):
@abstractmethod
def __init__(self):
self.session = None
async def __aenter__(self):
self.session = await aiohttp.ClientSession().__aenter__()
return self
async def __aexit__(self, *args, **kwargs):
await self.session.__aexit__(*args, **kwargs)
class HTTP(AbstractSessionContainer):
def __init__(self):
pass # If you don't have an init method I think you wont be able to create an HTTP instance
@attach_session
async def get(self, url, params, session=None):
async with session.get(url, params=params) as response:
return await response.text()
@attach_session
async def post(self, url, json_data, session=None):
async with session.post(url, json=json_data) as response:
return await response.text() If anyone sees any improvement (even a minor one like namings) to this I would be happy to get some feedback (as I wrote these for myself too :p). |
You can use |
Do you suggest to add methods in the If so then here is the modified code (in the mean time I also modified to code to handle parameters on the context manager creation): import asyncio, aiohttp
import functools
from abc import ABCMeta, abstractmethod
def attach_session(method):
@functools.wraps(method)
async def method_with_session(self, *args, **kwargs):
if self.session is None:
async with aiohttp.ClientSession() as session:
print("{} using a one shot session.".format(method.__name__))
return await method(self, *args, session=session, **kwargs)
else:
print("{} using the long lasting session {}.".format(method.__name__, self.session))
return await method(self, *args, session=self.session, **kwargs)
return method_with_session
class AbstractSessionContainer(metaclass=ABCMeta):
@abstractmethod
def __init__(self, *args, **kwargs):
self.session = None
self.args = args
self.kwargs = kwargs
async def __aenter__(self):
self.session = await aiohttp.ClientSession(*self.args, **self.kwargs).__aenter__()
return self
async def __aexit__(self, *args, **kwargs):
await self.session.__aexit__(*args, **kwargs)
async def start_session(self, *args, **kwargs):
self.session = aiohttp.ClientSession(*args, **kwargs)
async def close_session(self):
if (self.session is not None) and (not self.session.closed):
await self.session.close()
self.session = None
class HTTP(AbstractSessionContainer):
def __init__(self):
super().__init__(raise_for_status=True) # add any 'aiohttp.ClientSession' parameters here
@attach_session
async def get(self, url, params, session=None):
async with session.get(url, params=params) as response:
return await response.text()
@attach_session
async def post(self, url, json_data, session=None):
async with session.post(url, json=json_data) as response:
return await response.text() This allow to have 3 different ways of working with http = HTTP()
await http.get("http://api.mathjs.org/v4/", params={'expr' : '3^2'})
await http.get("http://api.mathjs.org/v4/", params={'expr' : '4^2'}) This doesn't change either, a single session is used for both requests: async with HTTP() as http:
await http.get("http://api.mathjs.org/v4/", params={'expr' : '3^2'})
await http.get("http://api.mathjs.org/v4/", params={'expr' : '4^2'}) You can also mimic the http = HTTP()
await http.start_session()
await http.get("http://api.mathjs.org/v4/", params={'expr' : '3^2'})
await http.get("http://api.mathjs.org/v4/", params={'expr' : '4^2'})
await http.close_session() |
@cglacet the answer depends on requirements. |
If an aiohttp.ClientSession is created outside a coroutine without a running event loop, a warning is shown: /Users/walkman/Nextcloud/Projects/simple-podcast-dl/podcast_dl/cli.py:210: UserWarning: Creating a client session outside of coroutine is a very dangerous idea http = aiohttp.ClientSession() Creating a client session outside of coroutine client_session: <aiohttp.client.ClientSession object at 0x102cb5ef0> We can avoid it by explicitly passing an event loop. For details, see: aio-libs/aiohttp#1468
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a [new issue] for related bugs. |
For preventing many stupid mistakes