Skip to content

Commit 06adcc2

Browse files
committed
New rdbms configuration option 'isolation_level'
See discussion in http://issues.roundup-tracker.org/issue2550806
1 parent b660a5c commit 06adcc2

File tree

5 files changed

+112
-0
lines changed

5 files changed

+112
-0
lines changed

CHANGES.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ Features:
3232
value as an ID when evaluating form values -- not as a key. Specifying
3333
try_id_parsing='no' for these Link/Multilink will skip the ID step,
3434
default is 'yes'.
35+
- New configuration option 'isolation_level' in rdbms section. Currently
36+
supported for Postgres and mysql, sets the transaction isolation level.
37+
Wrong history entries for concurrent database updates observed in
38+
issue2550806 can be prevented by setting this to 'repeatable read' if
39+
you want to pay the performance penalty. We test this behaviour in the
40+
regression tests for Postgres but not currently for mysql.
41+
See http://www.postgresql.org/docs/9.1/static/transaction-iso.html
42+
(Ralf Schlatterbeck)
3543

3644
Fixed:
3745

roundup/backends/back_mysql.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@
4040
from MySQLdb.constants import ER
4141
import logging
4242

43+
isolation_levels = \
44+
{ 'read uncommitted': 'READ UNCOMMITTED'
45+
, 'read committed': 'READ COMMITTED'
46+
, 'repeatable read': 'REPEATABLE READ'
47+
, 'serializable': 'SERIALIZABLE'
48+
}
49+
4350
def connection_dict(config, dbnamestr=None):
4451
d = rdbms_common.connection_dict(config, dbnamestr)
4552
if d.has_key('password'):
@@ -145,6 +152,8 @@ def sql_open_connection(self):
145152
raise DatabaseError, message
146153
cursor = conn.cursor()
147154
cursor.execute("SET AUTOCOMMIT=0")
155+
lvl = isolation_levels [self.config.RDBMS_ISOLATION_LEVEL]
156+
cursor.execute("SET SESSION TRANSACTION ISOLATION LEVEL %s" % lvl)
148157
cursor.execute("START TRANSACTION")
149158
return (conn, cursor)
150159

roundup/backends/back_postgresql.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,44 @@
88
__docformat__ = 'restructuredtext'
99

1010
import os, shutil, time
11+
ISOLATION_LEVEL_READ_UNCOMMITTED = None
12+
ISOLATION_LEVEL_READ_COMMITTED = None
13+
ISOLATION_LEVEL_REPEATABLE_READ = None
14+
ISOLATION_LEVEL_SERIALIZABLE = None
1115
try:
1216
import psycopg
1317
from psycopg import QuotedString
1418
from psycopg import ProgrammingError
19+
TransactionRollbackError = ProgrammingError
20+
try:
21+
from psycopg.extensions import ISOLATION_LEVEL_READ_UNCOMMITTED
22+
from psycopg.extensions import ISOLATION_LEVEL_READ_COMMITTED
23+
from psycopg.extensions import ISOLATION_LEVEL_REPEATABLE_READ
24+
from psycopg.extensions import ISOLATION_LEVEL_SERIALIZABLE
25+
except ImportError:
26+
pass
1527
except:
1628
from psycopg2 import psycopg1 as psycopg
1729
from psycopg2.extensions import QuotedString
30+
from psycopg2.extensions import ISOLATION_LEVEL_READ_UNCOMMITTED
31+
from psycopg2.extensions import ISOLATION_LEVEL_READ_COMMITTED
32+
from psycopg2.extensions import ISOLATION_LEVEL_REPEATABLE_READ
33+
from psycopg2.extensions import ISOLATION_LEVEL_SERIALIZABLE
1834
from psycopg2.psycopg1 import ProgrammingError
35+
from psycopg2.extensions import TransactionRollbackError
1936
import logging
2037

2138
from roundup import hyperdb, date
2239
from roundup.backends import rdbms_common
2340
from roundup.backends import sessions_rdbms
2441

42+
isolation_levels = \
43+
{ 'read uncommitted': ISOLATION_LEVEL_READ_COMMITTED
44+
, 'read committed': ISOLATION_LEVEL_READ_COMMITTED
45+
, 'repeatable read': ISOLATION_LEVEL_REPEATABLE_READ
46+
, 'serializable': ISOLATION_LEVEL_SERIALIZABLE
47+
}
48+
2549
def connection_dict(config, dbnamestr=None):
2650
''' read_default_group is MySQL-specific, ignore it '''
2751
d = rdbms_common.connection_dict(config, dbnamestr)
@@ -145,6 +169,9 @@ def sql_open_connection(self):
145169
raise hyperdb.DatabaseError(message)
146170

147171
cursor = conn.cursor()
172+
if ISOLATION_LEVEL_REPEATABLE_READ is not None:
173+
lvl = isolation_levels [self.config.RDBMS_ISOLATION_LEVEL]
174+
conn.set_isolation_level(lvl)
148175

149176
return (conn, cursor)
150177

roundup/configuration.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,20 @@ def str2value(self, value):
293293
else:
294294
raise OptionValueError(self, value, self.class_description)
295295

296+
class IsolationOption(Option):
297+
"""Database isolation levels"""
298+
299+
allowed = ['read uncommitted', 'read committed', 'repeatable read',
300+
'serializable']
301+
class_description = "Allowed values: %s" % ', '.join ("'%s'" % a
302+
for a in allowed)
303+
304+
def str2value(self, value):
305+
_val = value.lower()
306+
if _val in self.allowed:
307+
return _val
308+
raise OptionValueError(self, value, self.class_description)
309+
296310
class MailAddressOption(Option):
297311

298312
"""Email address
@@ -650,6 +664,10 @@ def str2value(self, value):
650664
" or use template0 as template.\n"
651665
"then set this option to the template name given in the\n"
652666
"error message."),
667+
(IsolationOption, 'isolation_level', 'read committed',
668+
"Database isolation level, currently supported for\n"
669+
"PostgreSQL and mysql. See, e.g.,\n"
670+
"http://www.postgresql.org/docs/9.1/static/transaction-iso.html"),
653671
), "Settings in this section are used"
654672
" by RDBMS backends only"
655673
),

test/test_postgresql.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
from db_test_base import DBTest, ROTest, config, SchemaTest, ClassicInitTest
2323
from db_test_base import ConcurrentDBTest, HTMLItemTest, FilterCacheTest
24+
from db_test_base import ClassicInitBase, setupTracker
2425

2526
from roundup.backends import get_backend, have_backend
2627

@@ -66,6 +67,54 @@ def tearDown(self):
6667
ConcurrentDBTest.tearDown(self)
6768
postgresqlOpener.tearDown(self)
6869

70+
class postgresqlJournalTest(postgresqlOpener, ClassicInitBase):
71+
backend = 'postgresql'
72+
def setUp(self):
73+
postgresqlOpener.setUp(self)
74+
ClassicInitBase.setUp(self)
75+
self.tracker = setupTracker(self.dirname, self.backend)
76+
db = self.tracker.open('admin')
77+
self.id = db.issue.create(title='initial value')
78+
db.commit()
79+
db.close()
80+
81+
def tearDown(self):
82+
self.db1.close()
83+
self.db2.close()
84+
ClassicInitBase.tearDown(self)
85+
postgresqlOpener.tearDown(self)
86+
87+
def _test_journal(self, expected_journal):
88+
id = self.id
89+
db1 = self.db1 = self.tracker.open('admin')
90+
db2 = self.db2 = self.tracker.open('admin')
91+
92+
t1 = db1.issue.get(id, 'title')
93+
t2 = db2.issue.get(id, 'title')
94+
95+
db1.issue.set (id, title='t1')
96+
db1.commit()
97+
db1.close()
98+
99+
db2.issue.set (id, title='t2')
100+
db2.commit()
101+
db2.close()
102+
self.db = self.tracker.open('admin')
103+
journal = self.db.getjournal('issue', id)
104+
for n, line in enumerate(journal):
105+
self.assertEqual(line[4], expected_journal[n])
106+
107+
def testConcurrentReadCommitted(self):
108+
expected_journal = [
109+
{}, {'title': 'initial value'}, {'title': 'initial value'}
110+
]
111+
self._test_journal(expected_journal)
112+
113+
def testConcurrentRepeatableRead(self):
114+
self.tracker.config.RDBMS_ISOLATION_LEVEL='repeatable read'
115+
exc = self.module.TransactionRollbackError
116+
self.assertRaises(exc, self._test_journal, [])
117+
69118
class postgresqlHTMLItemTest(postgresqlOpener, HTMLItemTest):
70119
backend = 'postgresql'
71120
def setUp(self):
@@ -132,6 +181,7 @@ def test_suite():
132181
suite.addTest(unittest.makeSuite(postgresqlClassicInitTest))
133182
suite.addTest(unittest.makeSuite(postgresqlSessionTest))
134183
suite.addTest(unittest.makeSuite(postgresqlConcurrencyTest))
184+
suite.addTest(unittest.makeSuite(postgresqlJournalTest))
135185
suite.addTest(unittest.makeSuite(postgresqlHTMLItemTest))
136186
suite.addTest(unittest.makeSuite(postgresqlFilterCacheTest))
137187
return suite

0 commit comments

Comments
 (0)