orvind ([info]orvind) wrote,
@ 2009-06-24 16:35:00
Previous Entry  Add to memories!  Tell a Friend  Next Entry
История 2 Как редактор испортил базу

Однажды наш дизайнер Миша ([info]mrfrostman ) пожаловался, что данные, получающиеся при экспорте из редактора не соответствуют действительности. Например, магазинов в редакторе 12, а для установки на карту доступно 16.

В нашей базе есть отдельная табличка с "описателями" ресурсов. Через нее ресурс локается на редактирование и табличка эта отдельная, чтобы взятие/снятие локов не портило список коммитов в envers'е (это такая надстройка над hibernate, которая позволяет отслеживать историю изменений). Единственное, из-за чего (чисто теоретически) действительно могло бы не совпадать количество объектов в редакторе и в экспорченных файлах - это если бы вдруг описатель ресурса удалился, а сам ресурс не пометился бы удаленным. Но это невозможно, так как удаление описателя и пометка ресурса как удаленного производятся внутри одной транзакции, так что если даже по пути выскочил exception, транзакция не даст испортить базу. Мой мозг долго отказывался верить в то, что в этой теории где-то есть ошибка, так что я довольно долго копал в неправильном направлении, проверяя экспортер.

В конце концов я полез в базу.. И обнаружил там, что дескрипторы-таки уничтожены. Я написал скрипт, восстанавливающий дескрипторы и решил еще раз удалить магазин... и опа! Обнаружилось нечто интересное. При попытке удалить магазин вылетает
java.sql.SQLException: Can't call commit when autocommit=true
Мать-перемать! С чего вдруг autocommit=true ?! Как это может быть, если я специально ставил InnoDB, специально добавлял в hibernate опцию autocommit=false и какого-то еще одного способа отключить автокоммит просто нет и быть не может. Вводим текст исключения в гугл и видим, что энтерпрайз-программисты из hibernate'а замутили еще один мегаалгоритм. Учтите, hibernate используется в очень серьезных сайтах. Не удивлюсь, если его используют банки, интернет-магазины и т.д.

Итак, мой код:

entityManager.getTransaction().begin();
entityManager.remove(descriptor);
entityManager.merge(resource);
entityManager.getTransaction().commit();

Как этот алгоритм блестяще кастрируется hibernate'ом? А вот смотрите.

1. Сначала открывается транзакция - это первая строчка.
2. Мы хотим удалить объект. Hibernate смотрит, что какой-то ресурс удаляется или изменяется, а данные в кэше могут слегка отличаться от данных в базе. Он знает, что транзакция началась и нужно следить, чтобы ничего не сломалось. Для этого нужно привести участвующие в транзакции объекты в то состояние, каким оно является в базе на момент начала транзакции. Дескрипторы наших ресурсов лежат в отдельной таблице и ссылок на них нигде нет, они используются только для отметок, что редактирование началось/закончилось. Поэтому вторая строчка тупо генерит "DELETE FROM resourcedescriptor WHERE id = ?".
3. А вот изменяющийся магазин уже используется во многих местах. Каждый моб может иметь на него ссылку, да и он сам уже мог измениться в базе. Поэтому hibernate должен послать набор селектов, чтобы обновить магазин до нового состояния. А теперь следите за руками! В истории номер 1 мы запретили для селектов делать OUTER JOIN'ы. Поэтому селектов нужно сделать много. Внутри одной транзакции. Не так чтобы очень много, но больше, чем разрешает установленный в hibernate'е по умолчанию лимит селектов на одну транзакцию. Что делать? И тут наши энтерпрайз-гении придумывают блестящее решение. Они.. внимание.. барабанная дробь.. Я говорил, что они настоящие гении? Приготовились?.. Так вот, они включают автокоммит! И типо волки сыты - пользователю никакой эксепшен не вылетел, и сцуко овцы целы - ограничение на количество селектов не превысили, так как каждый селект при включенном автокоммите идет отдельной транзакцией. А теперь возвращаем автокоммит обратно в false, типа так и было. И посылаем апдейты в базу.
4. Ну а теперь пора коммитить транзакцию. Но что за нах? Дескриптор-то уже убит в базе и убит он автокоммитом. Какая-то непонятная ситуация. Выбрасываем эксепшен и роллбэкаем транзакцию. Вуаля!

Самое досадное в этой истории то, что и тут мегасистему можно "настроить". Есть магический флажок relaxAutoCommit=true, о смысле названия которого мне лучше не думать. Я попытался найти в гугле описание того, что же именно флажок делает, но первые 3 страницы мне упорно говорят: "Он фиксит баг с вылетающим эксепшеном Can't call commit when autocommit=true". Типа магия такая.

Я понимаю, конечно, что вопрос риторический, но разве можно писать серьезный коммерческий код ТАК?



(6 comments) - (Post a new comment)


[info]vamp1r0
2009-06-24 04:26 pm UTC (link)
Можно конечно :) Я помню когда писали систем для учёта билетов, часто региональные клиенты просили странного, поскольку клиент должен быть сыт, там частенько заплатки ставились в самых неожиданных местах, как раз на такие случаи, когда мол база не тянет, селекты там джойны нежелательные. А потом оно со временем расползалось вообще во все версии, а не только для одного-двух клиентов. Очень знакомая в общем ситуация. Оно ж _обычно_ - работает :)

(Reply to this)

Ыыыы
[info]inandout_oflove
2009-06-24 07:33 pm UTC (link)
я в шоке
за такой энтерпрайз надо руки от головы отрывать, а поделку выкидывать нах

кстати - а с чем вообще связан лимит селектов на одну транзакцию? я энтерпрайзом не страдал, но про такое ограничение слышу впервые

(Reply to this) (Thread)

Re: Ыыыы
[info]orvind
2009-06-24 08:28 pm UTC (link)
Я не до конца уверен, что мегасистема работает именно так. Знаю только, что:
1. Баг возникал только с магазинами и некоторыми другими объектами, которые имеют много ассоциаций OneToMany (только когда много селектов, пока селектов мало все работает)
2. Когда я искал решения на различных форумах, там часто заходила речь о том, что проблема возникает при большом количестве запросов к базе и часто связана с изменением количества запросов внутри batch'а (например, http://blog.upcom.eu/tag/relaxautocommit/). Там же часто говорится, что проблема в JBoss, Hibernate и т.д. У нас из всех этих систем используется только Hibernate.
3. В hibernate'е есть константы на все случаи жизни: hibernate.jdbc.batch_size, куча всяких max_queries и т.д. И критическое количество селектов (я попробовал удалить несколько разных ресурсов и поигрался с FetchType.LAZY/EAGER) примерно в районе этих констант.

То есть мое знание о причинах ошибки настолько же недостоверно, насколько ненадежно решение в виде relaxAutoCommit=true. Но мой ЖЖ на звание печатного издания и не претендует. И если кто-то расскажет, как оно на самом деле - от моего поста будет даже больше пользы :)

(Reply to this) (Parent)


[info]_winnie
2009-06-24 07:35 pm UTC (link)
Аааа! Наша информационно-кибернетическая цивилизация будет уничтожена первым же дятлом!

(Reply to this) (Thread)


[info]inandout_oflove
2009-06-25 08:55 am UTC (link)
Голосую за жучка-короеда

(Reply to this) (Parent)


[info]pavel_kaplin
2009-07-08 08:25 am UTC (link)
Жесть :-)

(Reply to this)


(6 comments) - (Post a new comment)

Create an Account
Forgot your login or password?
Login w/ OpenID
English • Español • Deutsch • Русский…