Почему никому нет дела до этой ошибки MySQLdb? это ошибка? - PullRequest
10 голосов
/ 03 июня 2009

TL; DR: я предоставил патч для найденной ошибки и получил 0 отзывов об этом. Мне интересно, если это ошибка. Это не напыщенная речь. Пожалуйста, прочитайте это, и если это может повлиять на вас, проверьте исправление.

Я нашел и сообщил об этой ошибке MySQLdb несколько недель назад (редактирование: 6 недель назад), отправил патч, разместил его на нескольких форумах ORM, отправил по почте автору MySQLdb, отправил по почте некоторых людей, говорящих об обработке взаимоблокировок, отправил ORM Авторы, и я все еще жду каких-либо отзывов.

Эта ошибка доставила мне много горя, и единственное объяснение, которое я могу найти в обратной связи, это то, что либо никто не использует «SELECT ... FOR UPDATE» в python с mysql, либо это не ошибка.

В основном проблема заключается в том, что взаимные блокировки и исключения «ожидания ожидания блокировки» НЕ вызываются при выдаче «SELECT ... FOR UPDATE» с использованием курсора MySQLdb. Вместо этого оператор молча завершается ошибкой и возвращает пустой набор результатов, который любое приложение будет интерпретировать, как если бы не было совпадений ни одной строки.

Я протестировал версию SVN, и это все еще влияет. Протестировано на стандартных установках Ubuntu Intrepid, Jaunty и Debian Lenny, и они тоже затронуты. Это влияет на текущую версию, установленную easy_install (1.2.3c1).

Это также влияет на SQLAlchemy и SQLObject, и, вероятно, также затрагивается любой ORM, использующий курсоры MySQLdb.

Этот скрипт может воспроизвести тупик, который вызовет ошибку (просто измените пользователя / пароль в get_conn, он создаст необходимые таблицы):

import time
import threading
import traceback
import logging
import MySQLdb

def get_conn():
    return MySQLdb.connect(host='localhost', db='TESTS',
                           user='tito', passwd='testing123')

class DeadlockTestThread(threading.Thread):
    def __init__(self, order):
        super(DeadlockTestThread, self).__init__()
        self.first_select_done = threading.Event()
        self.do_the_second_one = threading.Event()
        self.order = order

    def log(self, msg):
        logging.info('%s: %s' % (self.getName(), msg))

    def run(self):
        db = get_conn()
        c = db.cursor()
        c.execute('BEGIN;')
        query = 'SELECT * FROM locktest%i FOR UPDATE;'
        try:
            try:
                c.execute(query  % self.order[0])
                self.first_select_done.set()

                self.do_the_second_one.wait()
                c.execute(query  % self.order[1])
                self.log('2nd SELECT OK, we got %i rows' % len(c.fetchall()))

                c.execute('SHOW WARNINGS;')
                self.log('SHOW WARNINGS: %s' % str(c.fetchall()))
            except:
                self.log('Failed! Rolling back')
                c.execute('ROLLBACK;')
                raise
            else:
                c.execute('COMMIT;')
        finally:
            c.close()
            db.close()


def init():
    db = get_conn()

    # Create the tables.
    c = db.cursor()
    c.execute('DROP TABLE IF EXISTS locktest1;')
    c.execute('DROP TABLE IF EXISTS locktest2;')
    c.execute('''CREATE TABLE locktest1 (
                    a int(11), PRIMARY KEY(a)
                  ) ENGINE=innodb;''')
    c.execute('''CREATE TABLE locktest2 (
                    a int(11), PRIMARY KEY(a)
                  ) ENGINE=innodb;''')
    c.close()

    # Insert some data.
    c = db.cursor()
    c.execute('BEGIN;')
    c.execute('INSERT INTO locktest1 VALUES (123456);')
    c.execute('INSERT INTO locktest2 VALUES (123456);')
    c.execute('COMMIT;')
    c.close()

    db.close()

if __name__ == '__main__':
    logging.basicConfig(level=logging.INFO)

    init()

    t1 = DeadlockTestThread(order=[1, 2])
    t2 = DeadlockTestThread(order=[2, 1])

    t1.start()
    t2.start()

    # Wait till both threads did the 1st select.
    t1.first_select_done.wait()
    t2.first_select_done.wait()

    # Let thread 1 continue, it will get wait for the lock 
    # at this point.
    t1.do_the_second_one.set()

    # Just make sure thread 1 is waiting for the lock.
    time.sleep(0.1)

    # This will trigger the deadlock and thread-2 will
    # fail silently, getting 0 rows.
    t2.do_the_second_one.set()

    t1.join()
    t2.join()

Результат выполнения этого на непатентованном MySQLdb такой:

$ python bug_mysqldb_deadlock.py
INFO:root:Thread-2: 2nd SELECT OK, we got 0 rows
INFO:root:Thread-2: SHOW WARNINGS: (('Error', 1213L, 'Deadlock found when trying to get lock; try restarting transaction'),)
INFO:root:Thread-1: 2nd SELECT OK, we got 1 rows
INFO:root:Thread-1: SHOW WARNINGS: ()

Вы можете видеть, что Thread-2 получил 0 строк из таблицы, которая, как мы знаем, имеет 1, и только при выполнении оператора "SHOW WARNINGS" вы можете увидеть, что произошло. Если вы отметите «SHOW ENGINE INNODB STATUS», вы увидите эту строку в журнале «*** ROLL BACK TRANSACTION (2)», все, что происходит после неудачного выбора в Thread-2, относится к транзакции с половиной отката.

После применения патча (проверьте тикет на него, URL ниже), это результат запуска скрипта:

$ python bug_mysqldb_deadlock.py
INFO:root:Thread-2: Failed! Rolling back
Exception in thread Thread-2:
Traceback (most recent call last):
  File "/usr/lib/python2.4/threading.py", line 442, in __bootstrap
    self.run()
  File "bug_mysqldb_deadlock.py", line 33, in run
    c.execute(query  % self.order[1])
  File "/home/koba/Desarollo/InetPub/IBSRL/VirtualEnv-1.0-p2.4/lib/python2.4/site-packages/MySQL_python-1.2.2-py2.4-linux-x86_64.egg/MySQLdb/cursors.py", line 178, in execute
    self.errorhandler(self, exc, value)
  File "/home/koba/Desarollo/InetPub/IBSRL/VirtualEnv-1.0-p2.4/lib/python2.4/site-packages/MySQL_python-1.2.2-py2.4-linux-x86_64.egg/MySQLdb/connections.py", line 35, in defaulterrorhandler
    raise errorclass, errorvalue
OperationalError: (1213, 'Deadlock found when trying to get lock; try restarting transaction')

INFO:root:Thread-1: 2nd SELECT OK, we got 1 rows
INFO:root:Thread-1: SHOW WARNINGS: ()

В этом случае возникает исключение для Thread-2, и оно откатывается должным образом.

Итак, что вы думаете? Это ошибка? никого не волнует или я просто сумасшедший?

Это билет, который я открыл на SF: http://sourceforge.net/tracker/index.php?func=detail&aid=2776267&group_id=22307&atid=374932

1 Ответ

7 голосов
/ 03 июня 2009

Почему никому нет до этого дела? Ошибка MySQLdb?

ошибки могут занять некоторое время, чтобы расставить приоритеты, исследовать, проверить проблему, найти исправление, протестировать исправление, убедиться, что исправление исправления не нарушает ничего другого. Я бы посоветовал вам развернуть обходное решение, поскольку это исправление может занять некоторое время.

...