Skip to content

Commit 345f694

Browse files
committed
Fix race condition.
1 parent 7d3ebb6 commit 345f694

File tree

1 file changed

+64
-0
lines changed

1 file changed

+64
-0
lines changed

roundup/backends/back_mysql.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,70 @@ def _subselect(self, classname, multilink_table):
572572
s = ','.join([x[0] for x in self.db.sql_fetchall()])
573573
return '_%s.id not in (%s)'%(classname, s)
574574

575+
def create_inner(self, **propvalues):
576+
try:
577+
return rdbms_common.Class.create_inner(self, **propvalues)
578+
except MySQLdb.IntegrityError, e:
579+
self._handle_integrity_error(e, propvalues)
580+
581+
def set_inner(self, nodeid, **propvalues):
582+
try:
583+
return rdbms_common.Class.set_inner(self, nodeid,
584+
**propvalues)
585+
except MySQLdb.IntegrityError, e:
586+
self._handle_integrity_error(e, propvalues)
587+
588+
def _handle_integrity_error(self, e, propvalues):
589+
''' Handle a MySQL IntegrityError.
590+
591+
If the error is recognized, then it may be converted into an
592+
alternative exception. Otherwise, it is raised unchanged from
593+
this function.'''
594+
595+
# There are checks in create_inner/set_inner to see if a node
596+
# is being created with the same key as an existing node.
597+
# But, there is a race condition -- we may pass those checks,
598+
# only to find out that a parallel session has created the
599+
# node by by the time we actually issue the SQL command to
600+
# create the node. Fortunately, MySQL gives us a unique error
601+
# code for this situation, so we can detect it here and handle
602+
# it appropriately.
603+
#
604+
# The details of the race condition are as follows, where
605+
# "X" is a classname, and the term "thread" is meant to
606+
# refer generically to both threads and processes:
607+
#
608+
# Thread A Thread B
609+
# -------- --------
610+
# read table for X
611+
# create new X object
612+
# commit
613+
# create new X object
614+
#
615+
# In Thread B, the check in create_inner does not notice that
616+
# the new X object is a duplicate of that committed in Thread
617+
# A because MySQL's default "consistent nonlocking read"
618+
# behavior means that Thread B sees a snapshot of the database
619+
# at the point at which its transaction began -- which was
620+
# before Thread A created the object. However, the attempt
621+
# to *write* to the table for X, creating a duplicate entry,
622+
# triggers an error at the point of the write.
623+
#
624+
# If both A and B's transaction begins with creating a new X
625+
# object, then this bug cannot occur because creating the
626+
# object requires getting a new ID, and newid() locks the id
627+
# table until the transaction is committed or rolledback. So,
628+
# B will block until A's commit is complete, and will not
629+
# actually get its snapshot until A's transaction completes.
630+
# But, if the transaction has begun prior to calling newid,
631+
# then the snapshot has already been established.
632+
if e[0] == ER.DUP_ENTRY:
633+
key = propvalues[self.key]
634+
raise ValueError, 'node with key "%s" exists' % key
635+
# We don't know what this exception is; reraise it.
636+
raise
637+
638+
575639
class Class(MysqlClass, rdbms_common.Class):
576640
pass
577641
class IssueClass(MysqlClass, rdbms_common.IssueClass):

0 commit comments

Comments
 (0)