Merge lp:~robru/bileto/no-more-sqlite-sessions into lp:bileto

Proposed by Robert Bruce Park
Status: Merged
Approved by: Robert Bruce Park
Approved revision: 221
Merged at revision: 203
Proposed branch: lp:~robru/bileto/no-more-sqlite-sessions
Merge into: lp:bileto
Diff against target: 274 lines (+77/-109)
4 files modified
Makefile (+1/-1)
tests/test_sessions.py (+32/-0)
tests/tests.py (+5/-1)
tickets/sessions.py (+39/-107)
To merge this branch: bzr merge lp:~robru/bileto/no-more-sqlite-sessions
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Approve
Robert Bruce Park (community) Approve
Review via email: mp+266637@code.launchpad.net

Commit message

Use JSON instead of Sqlite for storing sessions on disk.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:214
http://jenkins.qa.ubuntu.com/job/bileto-ci/13/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/bileto-ci/13/rebuild

review: Approve (continuous-integration)
Revision history for this message
Robert Bruce Park (robru) wrote :

Alright, this is looking good in staging, just needs test coverage.

215. By Robert Bruce Park

Note to self.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

FAILED: Continuous integration, rev:215
http://jenkins.qa.ubuntu.com/job/bileto-ci/14/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/bileto-ci/14/rebuild

review: Needs Fixing (continuous-integration)
216. By Robert Bruce Park

Whitespace fix.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:216
http://jenkins.qa.ubuntu.com/job/bileto-ci/15/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/bileto-ci/15/rebuild

review: Approve (continuous-integration)
217. By Robert Bruce Park

Drop CallableAttributeProxy and PersistedObjectProxy due to disuse.

218. By Robert Bruce Park

Initial session tests.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:218
http://jenkins.qa.ubuntu.com/job/bileto-ci/16/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/bileto-ci/16/rebuild

review: Approve (continuous-integration)
219. By Robert Bruce Park

Simplify session implementation.

220. By Robert Bruce Park

MOAR simple.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:220
http://jenkins.qa.ubuntu.com/job/bileto-ci/17/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/bileto-ci/17/rebuild

review: Approve (continuous-integration)
Revision history for this message
Robert Bruce Park (robru) wrote :

Looking really good in staging, no more IO error tracebacks as far as I can see.

review: Approve
221. By Robert Bruce Park

Fail under 98% test coverage.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:221
http://jenkins.qa.ubuntu.com/job/bileto-ci/18/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/bileto-ci/18/rebuild

review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2015-06-16 17:47:00 +0000
3+++ Makefile 2015-08-03 06:41:49 +0000
4@@ -2,7 +2,7 @@
5 find -name '*.py' | xargs pyflakes3
6 find -name '*.py' | xargs pep8
7 python3 -m coverage run -m nose2
8- python3 -m coverage report --include="t*/*" --show-missing --fail-under=92
9+ python3 -m coverage report --include="t*/*" --show-missing --fail-under=98
10
11 devel:
12 ./run.py
13
14=== added file 'tests/test_sessions.py'
15--- tests/test_sessions.py 1970-01-01 00:00:00 +0000
16+++ tests/test_sessions.py 2015-08-03 06:41:49 +0000
17@@ -0,0 +1,32 @@
18+from pickle import dumps
19+
20+from tests.tests import BiletoTestCase
21+
22+from tickets.app import app
23+from tickets.sessions import PickleSession
24+
25+
26+class SessionTestCase(BiletoTestCase):
27+ def test_save(self):
28+ sesh = PickleSession(app.session_interface.directory, 'foo')
29+ sesh['foo'] = 'bar'
30+ with open(sesh.path, 'rb') as pickled:
31+ self.assertEqual(pickled.read(), dumps(dict(foo='bar')))
32+
33+ def test_read(self):
34+ sesh = PickleSession(app.session_interface.directory, 'bar')
35+ with open(sesh.path, 'wb') as pickled:
36+ pickled.write(dumps(dict(baz='qux')))
37+ self.assertEqual(sesh['baz'], 'qux')
38+ self.assertEqual(len(sesh), 1)
39+
40+ def test_setters(self):
41+ # Initialize once just to get the path
42+ sesh = PickleSession(app.session_interface.directory, 'grill')
43+ with open(sesh.path, 'wb') as pickled:
44+ pickled.write(dumps(dict(baz='qux')))
45+ # Re-initialize with data in place.
46+ sesh = PickleSession(app.session_interface.directory, 'grill')
47+ sesh['foo'] = 'bar'
48+ with open(sesh.path, 'rb') as pickled:
49+ self.assertEqual(pickled.read(), dumps(dict(foo='bar', baz='qux')))
50
51=== modified file 'tests/tests.py'
52--- tests/tests.py 2015-06-02 21:26:29 +0000
53+++ tests/tests.py 2015-08-03 06:41:49 +0000
54@@ -2,6 +2,8 @@
55 import unittest
56 import tempfile
57
58+from shutil import rmtree
59+
60 from tickets.app import app, db
61
62
63@@ -10,6 +12,7 @@
64
65 def setUp(self):
66 """Create temp database."""
67+ os.makedirs(app.session_interface.directory, exist_ok=True)
68 self.db_fd, filename = tempfile.mkstemp()
69 app.config['DATABASE'] = filename
70 app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + filename
71@@ -18,6 +21,7 @@
72 db.create_all()
73
74 def tearDown(self):
75- """Close & delete temp database."""
76+ """Close & delete stuff."""
77 os.close(self.db_fd)
78 os.unlink(app.config['DATABASE'])
79+ rmtree(app.session_interface.directory, ignore_errors=True)
80
81=== modified file 'tickets/sessions.py'
82--- tickets/sessions.py 2015-06-18 20:09:08 +0000
83+++ tickets/sessions.py 2015-08-03 06:41:49 +0000
84@@ -1,151 +1,83 @@
85-# Author: Thiago Arruda
86-# License: Public Domain
87-# Source: http://flask.pocoo.org/snippets/86/
88-
89 import os
90-import sqlite3
91
92-from uuid import uuid4
93-from pickle import dumps, loads
94+from uuid import uuid1
95+from pickle import UnpicklingError, dumps, loads
96 from contextlib import suppress
97 from collections import MutableMapping
98 from flask.sessions import SessionInterface, SessionMixin
99
100
101-class SqliteSession(MutableMapping, SessionMixin):
102+class PickleSession(MutableMapping, SessionMixin):
103 """Server-side session implementation.
104
105- Uses sqlite to achieve a disk-backed session such that multiple
106+ Uses pickle to achieve a disk-backed session such that multiple
107 worker processes can access the same session data.
108 """
109
110- _create_sql = (
111- 'CREATE TABLE IF NOT EXISTS session '
112- '('
113- ' key TEXT PRIMARY KEY,'
114- ' val BLOB'
115- ')'
116- )
117- _get_sql = 'SELECT val FROM session WHERE key = ?'
118- _set_sql = 'REPLACE INTO session (key, val) VALUES (?, ?)'
119- _del_sql = 'DELETE FROM session WHERE key = ?'
120- _ite_sql = 'SELECT key FROM session'
121- _len_sql = 'SELECT COUNT(*) FROM session'
122-
123 def __init__(self, directory, sid, *args, **kwargs):
124 self.path = os.path.join(directory, sid)
125 self.directory = directory
126 self.sid = sid
127- self.modified = False
128- self.conn = None
129- if not os.path.exists(self.path):
130- with self._get_conn() as conn:
131- conn.execute(self._create_sql)
132- self.new = True
133+ self.read()
134
135 def __getitem__(self, key):
136- rv = None
137- with self._get_conn() as conn:
138- for row in conn.execute(self._get_sql, (key,)):
139- rv = loads(row[0])
140- break
141- if rv is None:
142- raise KeyError('Key not in this session')
143- return rv
144+ self.read()
145+ return self.data[key]
146
147 def __setitem__(self, key, value):
148- with self._get_conn() as conn:
149- conn.execute(self._set_sql, (key, dumps(value)))
150- self.modified = True
151+ self.data[key] = value
152+ self.save()
153
154 def __delitem__(self, key):
155- with self._get_conn() as conn:
156- conn.execute(self._del_sql, (key,))
157- self.modified = True
158+ del self.data[key]
159+ self.save()
160
161 def __iter__(self):
162- with self._get_conn() as conn:
163- for row in conn.execute(self._ite_sql):
164- yield str(row[0])
165+ return iter(self.data)
166
167 def __len__(self):
168+ return len(self.data)
169+
170+ def read(self):
171+ """Load pickle from (ram)disk."""
172 try:
173- with self._get_conn() as conn:
174- for row in conn.execute(self._len_sql):
175- return row[0]
176- except sqlite3.OperationalError:
177- return 0
178-
179- def _get_conn(self):
180- if not self.conn:
181- self.conn = sqlite3.connect(self.path)
182- return self.conn
183-
184- # These proxy classes are needed in order
185- # for this session implementation to work properly.
186- # That is because sometimes flask will chain method calls
187- # with session'setdefault' calls.
188- # Eg: session.setdefault('_flashes', []).append(1)
189- # With these proxies, the changes made by chained
190- # method calls will be persisted back to the sqlite
191- # database.
192- class CallableAttributeProxy(object):
193- def __init__(self, session, key, obj, attr):
194- self.session = session
195- self.key = key
196- self.obj = obj
197- self.attr = attr
198-
199- def __call__(self, *args, **kwargs):
200- rv = self.attr(*args, **kwargs)
201- self.session[self.key] = self.obj
202- return rv
203-
204- class PersistedObjectProxy(object):
205- def __init__(self, session, key, obj):
206- self.session = session
207- self.key = key
208- self.obj = obj
209-
210- def __getattr__(self, name):
211- attr = getattr(self.obj, name)
212- if callable(attr):
213- return SqliteSession.CallableAttributeProxy(
214- self.session, self.key, self.obj, attr)
215- return attr
216-
217- def setdefault(self, key, value):
218- if key not in self:
219- self[key] = value
220- self.modified = True
221- return SqliteSession.PersistedObjectProxy(
222- self, key, self[key])
223+ with open(self.path, 'rb') as blob:
224+ self.data = loads(blob.read())
225+ except (FileNotFoundError, ValueError, EOFError, UnpicklingError):
226+ self.data = {}
227+
228+ def save(self):
229+ """Dump pickle to (ram)disk atomically."""
230+ new_name = '{}.new'.format(self.path)
231+ with open(new_name, 'wb') as blob:
232+ blob.write(dumps(self.data))
233+ os.rename(new_name, self.path)
234+
235+ # Note: This shouldn't be a problem, but if you find poor interactions
236+ # between sessions and message "flashes" (a flask feature not used at this
237+ # time), you may need to resurrect the CallableAttributeProxy and
238+ # PersistedObjectProxy classes which I've dropped due to disuse.
239
240
241 class SqliteSessionInterface(SessionInterface):
242 """Basic SessionInterface which uses the SqliteSession."""
243
244 def __init__(self, directory):
245- directory = os.path.abspath(directory)
246- if not os.path.exists(directory):
247- os.mkdir(directory)
248- self.directory = directory
249+ self.directory = os.path.abspath(directory)
250+ os.makedirs(self.directory, exist_ok=True)
251
252 def open_session(self, app, request):
253- sid = request.cookies.get(app.session_cookie_name)
254- if not sid:
255- sid = str(uuid4())
256- rv = SqliteSession(self.directory, sid)
257- return rv
258+ sid = request.cookies.get(
259+ app.session_cookie_name) or '{}-{}'.format(uuid1(), os.getpid())
260+ return PickleSession(self.directory, sid)
261
262 def save_session(self, app, session, response):
263 domain = self.get_cookie_domain(app)
264 if not session:
265 with suppress(FileNotFoundError):
266 os.unlink(session.path)
267- if session.modified:
268- response.delete_cookie(
269- app.session_cookie_name, domain=domain)
270+ response.delete_cookie(
271+ app.session_cookie_name, domain=domain)
272 return
273 cookie_exp = self.get_expiration_time(app, session)
274 response.set_cookie(

Subscribers

People subscribed via source and target branches