"Игорь Бигдан" ...
Всем привет, сто лет у вас не был :)
Делаю проект на Firebird 2.03, нашёл нечто похожее на баг. Если это
что-то новое, м.б. добавите в баг-лист?
Сначала разберёмся
Итаг. Задача: нужно резервировать столики в ночном клубе на
определённую дату. Если столик на эту дату уже занят - ругать
пользователя последними словами. Особенность - если столик на эту дату
ещё не занят, но как раз в данный момент резервируется другим
оператором - ругать пользователя другими словами. Короче нужно
показывать статус столиков на дату - "свободен", "занят",
"резервируется" и позволять резервировать только те, у которых статус
"свободен".
Проблема: Dirty Read у нас нет и прочитать состояние "столика" до
коммита нельзя.
Решение: основываясь на том, что индексы работают вне контекста
транзакции, делаю проверку записи на уникальность ещё до её коммита.
Делаем таблицу (все операции проводил в IBExpert):
create table BoothReservation (
ID integer not null,
pDate date,
rBooth integer);
alter table BoothReservation
add constraint PK_BoothReservation
primary key (ID);
генератор и триггер для ID само собой разумеющиеся.
Накладываем ограничение "уникальность" на пару полей pDate, rBooth:
create unique index UNQ_BoothReservation
on BoothReservation (pDate, rBooth);
Запись о резервировании пишется в BoothReservation. По смыслу, если
записи для уникальной пары (pDate, rBooth) в BoothReservation нет -
столик "свободен", если есть - "занят". И если записи нет, но
проверочная вставка уникальной пары не проходит - столик
"резервируется".
Речь идёт именно о "проверке", когда юзеру нужно отобразить календарь
столиков с состояниями. Не то что бы мне нравилась проверка через
insert, но ничего умнее я не придумал.
Это единственный правильный способ захвата ресурса в конкурентной среде.
Других не придумано
Проверяем. Открываем 2 транзакции. В транзакции No.1 добавляем 2 записи:
insert into BoothReservation (pDate, rBooth) values ('9-JAN-2008', 5);
insert into BoothReservation (pDate, rBooth) values ('9-JAN-2008', 2);
commit;
В транзакции No.2 добавляем 1 запись:
insert into BoothReservation (pDate, rBooth) values ('9-JAN-2008', 2);
Ошибка:
Invalid insert or update value(s): object columns are
constrained - no 2 table rows can have duplicate column values.
attempt to store duplicate value (visible to active transactions) in
unique index "UNQ_BOOTHRESERVATION".
Всё правильно. Откатываем транзакцию No.2.
Проверяем без коммита. В транзакции No.1:
insert into BoothReservation (pDate, rBooth) values ('9-JAN-2008', 1);
В транзакции No.2:
insert into BoothReservation (pDate, rBooth) values ('9-JAN-2008', 1);
Ошибка:
Unsuccessful execution caused by system error that does not preclude
successful execution of subsequent statements.
lock conflict on no wait transaction.
attempt to store duplicate value (visible to active transactions) in
unique index "UNQ_BOOTHRESERVATION".
Тоже всё правильно. НЕ откатываем транзакцию No.2. В транзакции No.1
удаляем свежедобавленную, но не закоммиченую запись:
delete from BoothReservation where ID = 4;
НЕ коммитим транзакцию No.1. В транзакции No.2 пытаемся повторить вставку
той же записи:
insert into BoothReservation (pDate, rBooth) values ('9-JAN-2008', 1);
Ошибка:
Unsuccessful execution caused by system error that does not preclude
successful execution of subsequent statements.
lock conflict on no wait transaction.
attempt to store duplicate value (visible to active transactions) in
unique index "UNQ_BOOTHRESERVATION".
А вот это уже неправильно. Запись удалена, но транзакция No.2 почему-то
до сих пор не вкурсе. По логике раз уж уникальный индекс
перестраивается при вставке до коммита, значит при удалении тоже
должен (если вообще в перестраивании индекса дело).
Это правильно. Запись не удалена, т.к. тр-ция не закоммичена. Перестраивание
индекса тут совершенно не при чём. Ты мог делать удаление под сейвпойнтом,
например, и откатить этот сейвпойнт - в результате как раз и будет нарушение
уникальности.
Пробуем дальше. Откатываем транзакцию No.2 и открываем её заново (я
понимаю, что это будет уже No.3, но для удобства обхожусь тем же
номером :) ). Опять пытаемся повторить вставку той же записи:
insert into BoothReservation (pDate, rBooth) values ('9-JAN-2008', 1);
Та же ошибка. То есть дело точно не в "устаревшей" транзакции No.2.
Конечно. У тебя есть версия записи с тем же ключём, созданная всё
ещё активной тр-цией No.1
Коммитим транзакцию No.1 (там висело незакоммиченое добавление и
удаление, если помните ещё :) ). В транзакции No.2 пытаемся повторить
вставку той же записи:
insert into BoothReservation (pDate, rBooth) values ('9-JAN-2008', 1);
Успешно.
Ещё бы :)
Однако такой расклад меня не устраивает - в приложении я не могу
коммитить транзакцию No.1 раньше времени. Грубо говоря пока пользователь
не нажал в диалоге кнопку Ok или Cancel - транзакция не завершена. И
Ща тебя тут научат уму-разуму
пользователь может выбирать в диалоге любые столик/дату, и в момент
выбора производится удаление предыдущего выбора из BoothReservation и
вставка нового. И всё это должно быть видно другим транзакциям без
коммита.
Подумай хорошенько об этом ещё раз.
Ладно, исходя из предположения, что "индексы при удалении не сразу
перестраиваются" делаем финт ушами - перед удалением записи в
транзакции No.1 проводим апдейт:
update BoothReservation set pDate = (CURRENT_DATE - 365*100 - ID)
where ID = 4;
delete from BoothReservation where ID = 4;
Т.е. тупо апдейтим поле, входящее в уникальный индекс каким-нибудь
левым значением - в данном случае делаем минус 100 лет, минус ID
(чтобы разные записи не пересекались).
Работает. Теперь вставка "удалённой" записи в транзакции No.2 проходит
без коммита транзакции No.1. Что и требовалось получить.
Не должно это работать. Проверю.
Но баг остаётся багом :)
Угу, только он не в FB
--
Хорсун Влад