Skip to content

Commit e1b3e2f

Browse files
Yaser-Amiriasvetlov
authored andcommitted
Add memcached storage (#224)
* Add memcached storage. * Change .travis file to add memcached service and install aiomcache.
1 parent 74cc25c commit e1b3e2f

File tree

8 files changed

+335
-0
lines changed

8 files changed

+335
-0
lines changed

.travis.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ install:
1717
- pip install pytest-aiohttp
1818
- pip install pytest-cov
1919
- pip install codecov
20+
- pip install aiomcache
2021
- python setup.py develop
2122

2223
script:
@@ -37,6 +38,8 @@ env:
3738
- PYTHONASYNCIODEBUG=
3839
services:
3940
- redis-server
41+
- memcached
42+
4043
sudo: false
4144

4245
deploy:

aiohttp_session/memcached_storage.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import asyncio
2+
import json
3+
import uuid
4+
5+
from . import AbstractStorage, Session
6+
7+
8+
class MemcachedStorage(AbstractStorage):
9+
"""Memcached storage"""
10+
11+
def __init__(self, memcached_conn, *, cookie_name="AIOHTTP_SESSION",
12+
domain=None, max_age=None, path='/',
13+
secure=None, httponly=True,
14+
encoder=json.dumps, decoder=json.loads,
15+
key_factory=lambda: uuid.uuid4().hex):
16+
super().__init__(cookie_name=cookie_name, domain=domain,
17+
max_age=max_age, path=path, secure=secure,
18+
httponly=httponly)
19+
self._encoder = encoder
20+
self._decoder = decoder
21+
self._key_factory = key_factory
22+
self.conn = memcached_conn
23+
24+
@asyncio.coroutine
25+
def load_session(self, request):
26+
cookie = self.load_cookie(request)
27+
if cookie is None:
28+
return Session(None, data=None, new=True, max_age=self.max_age)
29+
else:
30+
key = str(cookie)
31+
stored_key = (self.cookie_name + '_' + key).encode('utf-8')
32+
data = yield from self.conn.get(stored_key)
33+
if data is None:
34+
return Session(None, data=None,
35+
new=True, max_age=self.max_age)
36+
data = data.decode('utf-8')
37+
try:
38+
data = self._decoder(data)
39+
except ValueError:
40+
data = None
41+
return Session(key, data=data, new=False, max_age=self.max_age)
42+
43+
@asyncio.coroutine
44+
def save_session(self, request, response, session):
45+
key = session.identity
46+
if key is None:
47+
key = self._key_factory()
48+
self.save_cookie(response, key,
49+
max_age=session.max_age)
50+
else:
51+
if session.empty:
52+
self.save_cookie(response, '',
53+
max_age=session.max_age)
54+
else:
55+
key = str(key)
56+
self.save_cookie(response, key,
57+
max_age=session.max_age)
58+
59+
data = self._encoder(self._get_session_data(session))
60+
max_age = session.max_age
61+
expire = max_age if max_age is not None else 0
62+
stored_key = (self.cookie_name + '_' + key).encode('utf-8')
63+
yield from self.conn.set(
64+
stored_key, data.encode('utf-8'),
65+
exptime=expire)

demo/memcached_storage.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import time
2+
from aiohttp import web
3+
import asyncio
4+
import aiomcache
5+
from aiohttp_session import setup, get_session
6+
7+
from aiohttp_session.memcached_storage import MemcachedStorage
8+
9+
10+
async def handler(request):
11+
session = await get_session(request)
12+
last_visit = session['last_visit'] if 'last_visit' in session else None
13+
session['last_visit'] = time.time()
14+
text = 'Last visited: {}'.format(last_visit)
15+
return web.Response(text=text)
16+
17+
18+
async def make_app():
19+
app = web.Application()
20+
mc = aiomcache.Client("127.0.0.1", 11211, loop=loop)
21+
setup(app, MemcachedStorage(mc))
22+
app.router.add_get('/', handler)
23+
return app
24+
25+
loop = asyncio.get_event_loop()
26+
app = loop.run_until_complete(make_app())
27+
web.run_app(app)

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,5 +332,6 @@
332332
intersphinx_mapping = {'https://docs.python.org/3': None,
333333
'https://aiohttp.readthedocs.io/en/stable': None,
334334
'https://aioredis.readthedocs.io/en/latest': None,
335+
'https://github.com/aio-libs/aiomcache', None,
335336
'http://cryptography.io/en/latest': None,
336337
'https://pynacl.readthedocs.io/en/latest': None}

docs/glossary.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414

1515
https://aioredis.readthedocs.io/
1616

17+
aiomcache
18+
19+
:term:`asyncio` compatible Memcached client library
20+
21+
https://github.com/aio-libs/aiomcache
22+
1723
asyncio
1824

1925
The library for writing single-threaded concurrent code using

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ aiohttp==2.3.1
1212
multidict==3.3.0
1313
chardet==3.0.4
1414
yarl==0.13.0
15+
aiomcache==0.5.2

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def read(f):
1818
install_requires = ['aiohttp>=2.3.0']
1919
extras_require = {
2020
'aioredis': ['aioredis>=0.1.4'],
21+
'aiomcache': ['aiomcache>=0.5.2'],
2122
'pycrypto': ['cryptography'],
2223
'secure': ['cryptography'],
2324
'pynacl': ['pynacl'],

tests/test_memcached_storage.py

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import asyncio
2+
import json
3+
import uuid
4+
import aiomcache
5+
import time
6+
import pytest
7+
8+
from aiohttp import web
9+
from aiohttp_session import Session, session_middleware, get_session
10+
from aiohttp_session.memcached_storage import MemcachedStorage
11+
12+
13+
@pytest.yield_fixture
14+
def memcached(loop):
15+
conn = aiomcache.Client("127.0.0.1", 11211, loop=loop)
16+
yield conn
17+
conn.close()
18+
19+
20+
def create_app(loop, handler, memcached, max_age=None,
21+
key_factory=lambda: uuid.uuid4().hex):
22+
middleware = session_middleware(
23+
MemcachedStorage(memcached, max_age=max_age, key_factory=key_factory))
24+
app = web.Application(middlewares=[middleware], loop=loop)
25+
app.router.add_route('GET', '/', handler)
26+
return app
27+
28+
29+
@asyncio.coroutine
30+
def make_cookie(client, memcached, data):
31+
session_data = {
32+
'session': data,
33+
'created': int(time.time())
34+
}
35+
value = json.dumps(session_data)
36+
key = uuid.uuid4().hex
37+
storage_key = ('AIOHTTP_SESSION_' + key).encode('utf-8')
38+
yield from memcached.set(storage_key, bytes(value, 'utf-8'))
39+
client.session.cookie_jar.update_cookies({'AIOHTTP_SESSION': key})
40+
41+
42+
@asyncio.coroutine
43+
def make_cookie_with_bad_value(client, memcached):
44+
key = uuid.uuid4().hex
45+
storage_key = ('AIOHTTP_SESSION_' + key).encode('utf-8')
46+
yield from memcached.set(storage_key, b'')
47+
client.session.cookie_jar.update_cookies({'AIOHTTP_SESSION': key})
48+
49+
50+
@asyncio.coroutine
51+
def load_cookie(client, memcached):
52+
cookies = client.session.cookie_jar.filter_cookies(client.make_url('/'))
53+
key = cookies['AIOHTTP_SESSION']
54+
storage_key = ('AIOHTTP_SESSION_' + key.value).encode('utf-8')
55+
encoded = yield from memcached.get(storage_key)
56+
s = encoded.decode('utf-8')
57+
value = json.loads(s)
58+
return value
59+
60+
61+
@asyncio.coroutine
62+
def test_create_new_sesssion(test_client, memcached):
63+
64+
@asyncio.coroutine
65+
def handler(request):
66+
session = yield from get_session(request)
67+
assert isinstance(session, Session)
68+
assert session.new
69+
assert not session._changed
70+
assert {} == session
71+
return web.Response(body=b'OK')
72+
73+
client = yield from test_client(create_app, handler, memcached)
74+
resp = yield from client.get('/')
75+
assert resp.status == 200
76+
77+
78+
@asyncio.coroutine
79+
def test_load_existing_sesssion(test_client, memcached):
80+
81+
@asyncio.coroutine
82+
def handler(request):
83+
session = yield from get_session(request)
84+
assert isinstance(session, Session)
85+
assert not session.new
86+
assert not session._changed
87+
assert {'a': 1, 'b': 12} == session
88+
return web.Response(body=b'OK')
89+
90+
client = yield from test_client(create_app, handler, memcached)
91+
yield from make_cookie(client, memcached, {'a': 1, 'b': 12})
92+
resp = yield from client.get('/')
93+
assert resp.status == 200
94+
95+
96+
@asyncio.coroutine
97+
def test_load_bad_sesssion(test_client, memcached):
98+
@asyncio.coroutine
99+
def handler(request):
100+
session = yield from get_session(request)
101+
assert isinstance(session, Session)
102+
assert not session.new
103+
assert not session._changed
104+
assert {} == session
105+
return web.Response(body=b'OK')
106+
107+
client = yield from test_client(create_app, handler, memcached)
108+
yield from make_cookie_with_bad_value(client, memcached)
109+
resp = yield from client.get('/')
110+
assert resp.status == 200
111+
112+
113+
@asyncio.coroutine
114+
def test_change_sesssion(test_client, memcached):
115+
116+
@asyncio.coroutine
117+
def handler(request):
118+
session = yield from get_session(request)
119+
session['c'] = 3
120+
return web.Response(body=b'OK')
121+
122+
client = yield from test_client(create_app, handler, memcached)
123+
yield from make_cookie(client, memcached, {'a': 1, 'b': 2})
124+
resp = yield from client.get('/')
125+
assert resp.status == 200
126+
127+
value = yield from load_cookie(client, memcached)
128+
assert 'session' in value
129+
assert 'a' in value['session']
130+
assert 'b' in value['session']
131+
assert 'c' in value['session']
132+
assert 'created' in value
133+
assert value['session']['a'] == 1
134+
assert value['session']['b'] == 2
135+
assert value['session']['c'] == 3
136+
morsel = resp.cookies['AIOHTTP_SESSION']
137+
assert morsel['httponly']
138+
assert '/' == morsel['path']
139+
140+
141+
@asyncio.coroutine
142+
def test_clear_cookie_on_sesssion_invalidation(test_client, memcached):
143+
144+
@asyncio.coroutine
145+
def handler(request):
146+
session = yield from get_session(request)
147+
session.invalidate()
148+
return web.Response(body=b'OK')
149+
150+
client = yield from test_client(create_app, handler, memcached)
151+
yield from make_cookie(client, memcached, {'a': 1, 'b': 2})
152+
resp = yield from client.get('/')
153+
assert resp.status == 200
154+
155+
value = yield from load_cookie(client, memcached)
156+
assert {} == value
157+
morsel = resp.cookies['AIOHTTP_SESSION']
158+
assert morsel['path'] == '/'
159+
assert morsel['expires'] == "Thu, 01 Jan 1970 00:00:00 GMT"
160+
assert morsel['max-age'] == "0"
161+
162+
163+
@asyncio.coroutine
164+
def test_create_cookie_in_handler(test_client, memcached):
165+
166+
@asyncio.coroutine
167+
def handler(request):
168+
session = yield from get_session(request)
169+
session['a'] = 1
170+
session['b'] = 2
171+
return web.Response(body=b'OK', headers={'HOST': 'example.com'})
172+
173+
client = yield from test_client(create_app, handler, memcached)
174+
resp = yield from client.get('/')
175+
assert resp.status == 200
176+
177+
value = yield from load_cookie(client, memcached)
178+
assert 'session' in value
179+
assert 'a' in value['session']
180+
assert 'b' in value['session']
181+
assert 'created' in value
182+
assert value['session']['a'] == 1
183+
assert value['session']['b'] == 2
184+
morsel = resp.cookies['AIOHTTP_SESSION']
185+
assert morsel['httponly']
186+
assert morsel['path'] == '/'
187+
storage_key = ('AIOHTTP_SESSION_' + morsel.value).encode('utf-8')
188+
exists = yield from memcached.get(storage_key)
189+
assert exists
190+
191+
192+
@asyncio.coroutine
193+
def test_create_new_sesssion_if_key_doesnt_exists_in_memcached(
194+
test_client, memcached):
195+
196+
@asyncio.coroutine
197+
def handler(request):
198+
session = yield from get_session(request)
199+
assert session.new
200+
return web.Response(body=b'OK')
201+
202+
client = yield from test_client(create_app, handler, memcached)
203+
client.session.cookie_jar.update_cookies(
204+
{'AIOHTTP_SESSION': 'invalid_key'})
205+
resp = yield from client.get('/')
206+
assert resp.status == 200
207+
208+
209+
@asyncio.coroutine
210+
def test_create_storate_with_custom_key_factory(test_client, memcached):
211+
212+
@asyncio.coroutine
213+
def handler(request):
214+
session = yield from get_session(request)
215+
session['key'] = 'value'
216+
assert session.new
217+
return web.Response(body=b'OK')
218+
219+
def key_factory():
220+
return 'test-key'
221+
222+
client = yield from test_client(create_app, handler, memcached, 8,
223+
key_factory)
224+
resp = yield from client.get('/')
225+
assert resp.status == 200
226+
227+
assert resp.cookies['AIOHTTP_SESSION'].value == 'test-key'
228+
229+
value = yield from load_cookie(client, memcached)
230+
assert 'key' in value['session']
231+
assert value['session']['key'] == 'value'

0 commit comments

Comments
 (0)