Skip to content

Commit b80dcac

Browse files
committed
Fix commits although a Reject exception is raised
Fix the problem that changes are committed to the database (due to commits to otk handling) even when a Reject exception occurs. The fix implements separate database connections for otk/session handling and normal database operation.
1 parent 79cdef3 commit b80dcac

19 files changed

+106
-92
lines changed

CHANGES.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,12 @@ Fixed:
493493
obsolete_history_roles in the main section that defines which roles
494494
may see removed properties. By default only role Admin is allowed to
495495
see these.
496+
- Fix issue2550955: Roundup commits although a Reject exception is raised
497+
Fix the problem that changes are committed to the database (due to
498+
commits to otk handling) even when a Reject exception occurs. The fix
499+
implements separate database connections for otk/session handling and
500+
normal database operation.
501+
496502

497503
2016-01-11: 1.5.1
498504

roundup/backends/back_anydbm.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ def __init__(self, config, journaltag=None):
191191
self.lockfile.write(str(os.getpid()))
192192
self.lockfile.flush()
193193

194+
self.Session = None
195+
self.Otk = None
196+
194197
def post_init(self):
195198
"""Called once the schema initialisation has finished.
196199
"""
@@ -204,10 +207,14 @@ def refresh_database(self):
204207
self.reindex()
205208

206209
def getSessionManager(self):
207-
return Sessions(self)
210+
if not self.Session:
211+
self.Session = Sessions(self)
212+
return self.Session
208213

209214
def getOTKManager(self):
210-
return OneTimeKeys(self)
215+
if not self.Otk:
216+
self.Otk = OneTimeKeys(self)
217+
return self.Otk
211218

212219
def reindex(self, classname=None, show_progress=False):
213220
if classname:
@@ -698,17 +705,11 @@ def pack(self, pack_before):
698705
#
699706
# Basic transaction support
700707
#
701-
def commit(self, fail_ok=False):
708+
def commit(self):
702709
""" Commit the current transactions.
703710
704711
Save all data changed since the database was opened or since the
705712
last commit() or rollback().
706-
707-
fail_ok indicates that the commit is allowed to fail. This is used
708-
in the web interface when committing cleaning of the session
709-
database. We don't care if there's a concurrency issue there.
710-
711-
The only backend this seems to affect is postgres.
712713
"""
713714
logging.getLogger('roundup.hyperdb.backend').info('commit %s transactions'%(
714715
len(self.transactions)))

roundup/backends/back_mysql.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,7 @@ def create_class(self, spec):
563563
vals = (spec.classname, 1)
564564
self.sql(sql, vals)
565565

566-
def sql_commit(self, fail_ok=False):
566+
def sql_commit(self):
567567
''' Actually commit to the database.
568568
'''
569569
self.log_info('commit')

roundup/backends/back_postgresql.py

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def db_create(config):
5252
logging.getLogger('roundup.hyperdb').info(command)
5353
db_command(config, command)
5454

55-
def db_nuke(config, fail_ok=0):
55+
def db_nuke(config):
5656
"""Clear all database contents and drop database itself"""
5757
command = 'DROP DATABASE "%s"'% config.RDBMS_NAME
5858
logging.getLogger('roundup.hyperdb').info(command)
@@ -151,9 +151,6 @@ class Database(rdbms_common.Database):
151151
# used by some code to switch styles of query
152152
implements_intersect = 1
153153

154-
def getSessionManager(self):
155-
return Sessions(self)
156-
157154
def sql_open_connection(self):
158155
db = connection_dict(self.config, 'database')
159156
logging.getLogger('roundup.hyperdb').info(
@@ -187,6 +184,9 @@ def open_connection(self):
187184
self.sql("CREATE TABLE dual (dummy integer)")
188185
self.sql("insert into dual values (1)")
189186
self.create_version_2_tables()
187+
# Need to commit here, otherwise otk/session will not find
188+
# the necessary tables (in a parallel connection!)
189+
self.commit()
190190

191191
def create_version_2_tables(self):
192192
# OTK store
@@ -244,25 +244,6 @@ def add_actor_column(self):
244244
def __repr__(self):
245245
return '<roundpsycopgsql 0x%x>' % id(self)
246246

247-
def sql_commit(self, fail_ok=False):
248-
''' Actually commit to the database.
249-
'''
250-
logging.getLogger('roundup.hyperdb').info('commit')
251-
252-
try:
253-
self.conn.commit()
254-
except ProgrammingError as message:
255-
# we've been instructed that this commit is allowed to fail
256-
if fail_ok and str(message).endswith('could not serialize '
257-
'access due to concurrent update'):
258-
logging.getLogger('roundup.hyperdb').info(
259-
'commit FAILED, but fail_ok')
260-
else:
261-
raise
262-
263-
# open a new cursor for subsequent work
264-
self.cursor = self.conn.cursor()
265-
266247
def sql_stringquote(self, value):
267248
''' psycopg.QuotedString returns a "buffer" object with the
268249
single-quotes around it... '''

roundup/backends/back_sqlite.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
from roundup import hyperdb, date, password
1414
from roundup.backends import rdbms_common
15+
from roundup.backends.sessions_dbm import Sessions, OneTimeKeys
16+
1517
sqlite_version = None
1618
try:
1719
import sqlite3 as sqlite
@@ -94,6 +96,19 @@ class Database(rdbms_common.Database):
9496
hyperdb.Multilink : lambda x: x, # used in journal marshalling
9597
}
9698

99+
# We're using DBM for managing session info and one-time keys:
100+
# For SQL database storage of this info we would need two concurrent
101+
# connections to the same database which SQLite doesn't support
102+
def getSessionManager(self):
103+
if not self.Session:
104+
self.Session = Sessions(self)
105+
return self.Session
106+
107+
def getOTKManager(self):
108+
if not self.Otk:
109+
self.Otk = OneTimeKeys(self)
110+
return self.Otk
111+
97112
def sqlite_busy_handler(self, data, table, count):
98113
"""invoked whenever SQLite tries to access a database that is locked"""
99114
now = time.time()
@@ -349,7 +364,7 @@ def sql_rollback(self):
349364
def __repr__(self):
350365
return '<roundlite 0x%x>'%id(self)
351366

352-
def sql_commit(self, fail_ok=False):
367+
def sql_commit(self):
353368
""" Actually commit to the database.
354369
355370
Ignore errors if there's nothing to commit.

roundup/backends/rdbms_common.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,10 @@ def __init__(self, config, journaltag=None):
189189
# database lock
190190
self.lockfile = None
191191

192+
# Uppercase to not collide with Class names
193+
self.Session = None
194+
self.Otk = None
195+
192196
# open a connection to the database, creating the "conn" attribute
193197
self.open_connection()
194198

@@ -199,10 +203,14 @@ def clearCache(self):
199203
roundupdb.Database.clearCache(self)
200204

201205
def getSessionManager(self):
202-
return Sessions(self)
206+
if not self.Session:
207+
self.Session = Sessions(self)
208+
return self.Session
203209

204210
def getOTKManager(self):
205-
return OneTimeKeys(self)
211+
if not self.Otk:
212+
self.Otk = OneTimeKeys(self)
213+
return self.Otk
206214

207215
def open_connection(self):
208216
""" Open a connection to the database, creating it if necessary.
@@ -1411,7 +1419,7 @@ def pack(self, pack_before):
14111419
"action<>'create'"%(classname, self.arg)
14121420
self.sql(sql, (date_stamp,))
14131421

1414-
def sql_commit(self, fail_ok=False):
1422+
def sql_commit(self):
14151423
""" Actually commit to the database.
14161424
"""
14171425
logging.getLogger('roundup.hyperdb.backend').info('commit')
@@ -1421,20 +1429,21 @@ def sql_commit(self, fail_ok=False):
14211429
# open a new cursor for subsequent work
14221430
self.cursor = self.conn.cursor()
14231431

1424-
def commit(self, fail_ok=False):
1432+
def commit(self):
14251433
""" Commit the current transactions.
14261434
14271435
Save all data changed since the database was opened or since the
14281436
last commit() or rollback().
1429-
1430-
fail_ok indicates that the commit is allowed to fail. This is used
1431-
in the web interface when committing cleaning of the session
1432-
database. We don't care if there's a concurrency issue there.
1433-
1434-
The only backend this seems to affect is postgres.
14351437
"""
14361438
# commit the database
1437-
self.sql_commit(fail_ok)
1439+
self.sql_commit()
1440+
1441+
# session and otk are committed with the db but not the other
1442+
# way round
1443+
if self.Session:
1444+
self.Session.commit()
1445+
if self.Otk:
1446+
self.Otk.commit()
14381447

14391448
# now, do all the other transaction stuff
14401449
for method, args in self.transactions:
@@ -1483,6 +1492,12 @@ def close(self):
14831492
"""
14841493
self.indexer.close()
14851494
self.sql_close()
1495+
if self.Session:
1496+
self.Session.close()
1497+
self.Session = None
1498+
if self.Otk:
1499+
self.Otk.close()
1500+
self.Otk = None
14861501

14871502
#
14881503
# The base Class class

roundup/backends/sessions_rdbms.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
class. It's now also used for One Time Key handling too.
66
"""
77
__docformat__ = 'restructuredtext'
8-
import os, time
8+
import os, time, logging
99
from cgi import escape
1010

1111
class BasicDatabase:
@@ -16,7 +16,7 @@ class BasicDatabase:
1616
name = None
1717
def __init__(self, db):
1818
self.db = db
19-
self.cursor = self.db.cursor
19+
self.conn, self.cursor = self.db.sql_open_connection()
2020

2121
def clear(self):
2222
self.cursor.execute('delete from %ss'%self.name)
@@ -113,6 +113,15 @@ def clean(self):
113113
self.cursor.execute('delete from %ss where %s_time < %s'%(self.name,
114114
self.name, self.db.arg), (old, ))
115115

116+
def commit(self):
117+
logger = logging.getLogger('roundup.hyperdb.backend')
118+
logger.info('commit %s' % self.name)
119+
self.conn.commit()
120+
self.cursor = self.conn.cursor()
121+
122+
def close(self):
123+
self.conn.close()
124+
116125
class Sessions(BasicDatabase):
117126
name = 'session'
118127

roundup/cgi/actions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -911,7 +911,7 @@ def handle(self):
911911
cl.set(uid, password=password.Password(newpw, config=self.db.config))
912912
# clear the props from the otk database
913913
otks.destroy(otk)
914-
self.db.commit()
914+
otks.commit()
915915
except (ValueError, KeyError) as message:
916916
self.client.add_error_message(str(message))
917917
return
@@ -965,7 +965,7 @@ def handle(self):
965965
while otks.exists(otk):
966966
otk = ''.join([random.choice(chars) for x in range(32)])
967967
otks.set(otk, uid=uid, uaddress=address)
968-
self.db.commit()
968+
otks.commit()
969969

970970
# send the email
971971
tracker_name = self.db.config.TRACKER_NAME

roundup/cgi/client.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ def destroy(self):
182182
self.client.add_cookie(self.cookie_name, None)
183183
self._data = {}
184184
self.session_db.destroy(self._sid)
185-
self.client.db.commit()
185+
self.session_db.commit()
186186

187187
def get(self, name, default=None):
188188
return self._data.get(name, default)
@@ -200,7 +200,7 @@ def set(self, **kwargs):
200200
self.client.session = self._sid
201201
else:
202202
self.session_db.set(self._sid, **self._data)
203-
self.client.db.commit()
203+
self.session_db.commit()
204204

205205
def update(self, set_cookie=False, expire=None):
206206
""" update timestamp in db to avoid expiration
@@ -212,7 +212,7 @@ def update(self, set_cookie=False, expire=None):
212212
lifetime is longer
213213
"""
214214
self.session_db.updateTimestamp(self._sid)
215-
self.client.db.commit()
215+
self.session_db.commit()
216216

217217
if set_cookie:
218218
self.client.add_cookie(self.cookie_name, self._sid, expire=expire)
@@ -697,14 +697,15 @@ def clean_up(self):
697697

698698
# XXX: hack - use OTK table to store last_clean time information
699699
# 'last_clean' string is used instead of otk key
700-
last_clean = self.db.getOTKManager().get('last_clean', 'last_use', 0)
700+
otks = self.db.getOTKManager()
701+
last_clean = otks.get('last_clean', 'last_use', 0)
701702
if now - last_clean < hour:
702703
return
703704

704705
self.session_api.clean_up()
705-
self.db.getOTKManager().clean()
706-
self.db.getOTKManager().set('last_clean', last_use=now)
707-
self.db.commit(fail_ok=True)
706+
otks.clean()
707+
otks.set('last_clean', last_use=now)
708+
otks.commit()
708709

709710
def determine_charset(self):
710711
"""Look for client charset in the form parameters or browser cookie.
@@ -982,7 +983,7 @@ def handle_csrf(self, xmlrpc=False):
982983
self._("csrf key used with wrong method from: %s"),
983984
referer)
984985
otks.destroy(key)
985-
self.db.commit()
986+
otks.commit()
986987
# do return here. Keys have been obsoleted.
987988
# we didn't do a expire cycle of session keys,
988989
# but that's ok.
@@ -1105,7 +1106,7 @@ def handle_csrf(self, xmlrpc=False):
11051106

11061107
if xmlrpc:
11071108
# Save removal of expired keys from database.
1108-
self.db.commit()
1109+
otks.commit()
11091110
# Return from here since we have done housekeeping
11101111
# and don't use csrf tokens for xmlrpc.
11111112
return True
@@ -1125,7 +1126,7 @@ def handle_csrf(self, xmlrpc=False):
11251126
otks.destroy(key)
11261127

11271128
# commit the deletion/expiration of all keys
1128-
self.db.commit()
1129+
otks.commit()
11291130

11301131
enforce=config['WEB_CSRF_ENFORCE_TOKEN']
11311132
if key is None: # we do not have an @csrf token
@@ -1172,7 +1173,7 @@ def handle_csrf(self, xmlrpc=False):
11721173
self.form["@action"].value == "Login":
11731174
if header_pass > 0:
11741175
otks.destroy(key)
1175-
self.db.commit()
1176+
otks.commit()
11761177
return True
11771178
else:
11781179
self.add_error_message("Reload window before logging in.")

roundup/cgi/templating.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def anti_csrf_nonce(self, client, lifetime=None):
104104
otks.set(key, uid=client.db.getuid(),
105105
sid=client.session_api._sid,
106106
__timestamp=ts )
107-
client.db.commit()
107+
otks.commit()
108108
return key
109109

110110
### templating

0 commit comments

Comments
 (0)