-
Notifications
You must be signed in to change notification settings - Fork 38
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
Fix pool concurrency #46
Conversation
Previous design of the pool had various flaws: - Created more connections than maxsize (although because of maxsize they were discarded). - There were race conditions in the control of max size and when creating connection. Now the size is controlled by the queue + _in_use set - Although asyncio.Queue was being used, no blocking feature was being used. Now clients will block when trying to acquire a connection
Again, although I've added tests, I've also used this script to combine with import uuid
import logging
import asyncio
import aiomcache
from aiohttp import web
logger = logging.getLogger(__name__)
class CacheManager:
def __init__(self):
self.cache = aiomcache.Client("127.0.0.1", 11211, pool_size=4)
async def get(self, key):
return await asyncio.wait_for(self.cache.get(key), 0.1)
async def set(self, key, value):
return await asyncio.wait_for(self.cache.set(key, value), 0.1)
async def handler_get(req):
try:
data = await req.app['cache'].get(b'testkey')
assert req.app['cache'].cache._pool.size() <= 4
if data:
return web.Response(text=data.decode())
data = str(uuid.uuid4()).encode()
await req.app['cache'].set(b'testkey', data)
return web.Response(text=str(data))
except asyncio.TimeoutError:
data = str(uuid.uuid4()).encode()
await req.app['cache'].set(b'testkey', data)
return web.Response(status=404)
if __name__ == '__main__':
app = web.Application()
app['cache'] = CacheManager()
app.router.add_route('GET', '/', handler_get)
web.run_app(app)
@pfreixes @achedeuzot it would be nice if you could test this. |
@@ -16,9 +16,8 @@ def __init__(self, host, port, *, minsize, maxsize, loop=None): | |||
self._minsize = minsize | |||
self._maxsize = maxsize | |||
self._loop = loop | |||
self._pool = asyncio.Queue(maxsize, loop=loop) | |||
self._pool = asyncio.Queue(loop=loop) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no need for the maxsize, control is done outside the data structure. Having a maxsize here is misleading and can also hide effects like creating more connections than needed (because they get queued or discarded)
could you try "-k -n 10000"? |
conn = _conn | ||
|
||
if conn is None: | ||
_conn = yield from self._pool.get() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clients will now block here until there is a connection available. If the retrieved connection has an error, they will close it and try to create a new one
if self.size() < self._maxsize: | ||
reader, writer = yield from asyncio.open_connection( | ||
self._host, self._port, loop=self._loop) | ||
if self.size() < self._maxsize: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid race condition when yielding from open_connection
. When we get a new connection, maybe other clients also created their connections so we have to check again and close it if necessary
else: | ||
reader.feed_eof() | ||
writer.close() | ||
return None | ||
else: | ||
return None | ||
|
||
def size(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Size of the pool is now coupled with the underlying data structures used to store connections
@fafhrd91 I've updated the script (forgot to add the
With master
Even worse, after the load test, the server doesn't respond to ANY request, which was the original issue reported by @achedeuzot |
Codecov Report
@@ Coverage Diff @@
## master #46 +/- ##
=========================================
- Coverage 91.72% 91.5% -0.23%
=========================================
Files 5 5
Lines 266 259 -7
Branches 39 38 -1
=========================================
- Hits 244 237 -7
Misses 11 11
Partials 11 11
Continue to review full report at Codecov.
|
@fafhrd91 would you mind reviewing it? I think its a real improvement |
if _conn is None: | ||
break | ||
yield from self._pool.put(_conn) | ||
self._pool.put_nowait(_conn) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here, If Im not wrong, the code has still chances to oveflow the pool size required by teh user. How much closer are the values btw minsize
and maxsize
these chances will increase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I'm missing something but I don't see it happening. put_nowait
is sync. It calls _put
that directly inserts into the internal deque:
def _put(self, item):
self._queue.append(item)
This means that after self._pool.put_nowait
, the object is inserted without breaking the coroutine execution.
Also, I added this tests that should cover this case (note it checks every time that the pool size is never bigger than maxsize): https://github.com/aio-libs/aiomcache/pull/46/files#diff-44251e44a09d028bd96e8275ea1a0ec9R92
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeps, my mistake I can see that the _create_new_connection
already takes care of that kind of situations.
@@ -39,29 +37,20 @@ def acquire(self): | |||
|
|||
:return: ``tuple`` (reader, writer) | |||
""" | |||
while self._size < self._minsize: | |||
while self.size() == 0 or self.size() < self._minsize: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not just evaluate self.size() < self._minsize
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because _minsize
can be 0 and then pool would end up being blocked because no connections exist
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oks got it, I would prefer to do both things without mixing them to get more readability - Ive read it three or four times. But not a big deal for me
LGTM good work !!! |
LGTM! |
Previous design of the pool had various flaws:
Fixes #43
EDIT:
I have to fix the case when minsize is 0, clients get blocked foreverFixed in ff4dbc1