@@ -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+
575639class Class (MysqlClass , rdbms_common .Class ):
576640 pass
577641class IssueClass (MysqlClass , rdbms_common .IssueClass ):
0 commit comments