Точки сохранения (savepoints) и механизм целостности в СУБД Firebird

Дмитрий Еманов, (c) 2003-2005, v1.2

Автор выражает благодарность Николаю Самофатову за часть материала, а также Дмитрию Кузьменко, Владу Хорсуну и Дмитрию Коваленко за ценные замечания и дополнения.
 

Общие сведения

Точки сохранения (далее сокращенно ТС) – это внутренний механизм СУБД, привязывающий любые изменения в БД к конкретному моменту времени в рамках транзакции, и позволяющий при необходимости отменить все изменения, выполненные после установки данной ТС (так называемый откат до ТС).

По умолчанию сервер использует глобальную ТС (то есть уровня транзакции, ведь ТС не имеют смысла вне ее контекста) для осуществления отката транзакции. Данная ТС устанавливается автоматически при старте транзакции и является первой в ее контексте. Когда инициируется откат транзакции, то все изменения, выполненные в ее контексте, откатываются с помощью глобальной ТС транзакции, после чего данная транзакция подтверждается (!) в TIP (Transaction Inventory Page). Но если количество изменений, выполненных в контексте транзакции, становится слишком велико (порядка 104 – 106 записей), то хранение списков отката получается дорогостоящим и сервер удаляет глобальную ТС транзакции, переходя к стандартному механизму TIP для пометки транзакции как отмененной. Поэтому, если вы ожидаете, что в рамках транзакции будет выполнено большое количество изменений, то имеет смысл указать параметр транзакции isc_tpb_no_auto_undo, который отключает использование глобальной ТС для отката транзакции. Это в некоторых случаях позволит увеличить быстродействие сервера при массовых операциях.

Помимо использования ТС для отката транзакции, сервер также использует их для обработки исключений. Каждый DSQL и/или PSQL оператор обрамляется ТС-фреймом, позволяющим откатить именно этот оператор, не затрагивая предыдущие. Это гарантирует, что оператор либо выполнился успешно, либо все его изменения автоматически отменены и инициирована соответствующая ошибка. Для обработки исключений в PSQL, каждый BEGIN…END блок также обрамляется фреймом, позволяющим отменить все изменения, выполненные данным блоком. Более подробно об этом механизме описано ниже.
 

Как это устроено?

ТС представляет собой структуру данных, размещенную в динамической памяти сервера (в пуле транзакции) и имеющую уникальный числовой идентификатор. К каждой ТС привязан список действий, совершенных в ее контексте (так называемый undo log или журнал отмены). В пределах транзакции ТС образуют стек и, следовательно, их откат всегда возможен только последовательно. Фрагменты журнала отмены распределены между ТС, которые инкрементно хранят историю всех изменений, выполненных в контексте транзакции.

ТС, активная на момент изменения какой-либо записи, называется текущей. Информация об изменении записи помещается в журнал отмены текущей ТС. В случае инициации отката до ТС журнал отмены раскручивается в обратную сторону, реконструируя запись к виду, в каком она существовала на момент установки данной ТС. После реконструкции всех измененных записей ТС обычно удаляется из контекста транзакции. В случае отсутствия обработчиков исключений в контексте текущей ТС, данный процесс может повторяться, отменяя изменения вышестоящих ТС. Помимо операции отката до ТС существует еще и операция штатного удаления (освобождения) ТС. В случае удаления ТС ее журнал отмены объединяется с журналом отмены предыдущей в стеке ТС. С учетом вышесказанного можно говорить о вложенности ТС.

Очевидно, что штатное удаление подмножества ТС, расположенных "глубже" глобальной, приведет к переносу всех изменений с их контекста на контекст глобальной ТС транзакции. Как будет показано ниже, каждый клиентский DSQL-запрос и представляет собой такое подмножество ТС. Таким образом, совокупность всех изменений, успешно выполненных в контексте транзакции, хранится в ее журнале отмены, обеспечивая этим упомянутую выше возможность замены состояния dead транзакции в TIP на committed. При указании параметра isc_tpb_no_auto_undo при старте транзакции глобальная ТС не создается и в случае штатного удаления текущего стека ТС совокупный журнал отмены просто удаляется. Важно понимать, что данный параметр не отключает механизм ТС как таковой, это невозможно с точки зрения гарантии атомарности SQL-операторов. Он отключает только ведение журнала отмены транзакции как единого целого.
 

Системные точки сохранения и обработка исключений

Существует несколько событий, которые приводят к созданию сервером системных (то есть неуправляемых пользователем) ТС:
  1. Выполнение любого клиентского SQL-запроса. Как уже было сказано выше, это делается для обеспечения атомарности данного запроса, то есть при возниковении любого исключения во время выполнения запроса, изменения, внесенные им в БД, всегда будут отменены. По окончании выполнения запроса ТС автоматически удаляется.
  2. Выполнение BEGIN…END блока в PSQL (процедуре или триггере) в случае, если этот блок содержит обработчик ошибок (WHEN-блок), либо если любой из вышестоящих блоков содержит обработчик ошибок. В этом случае каждый оператор BEGIN устанавливает точку сохранения и соответствующий ему оператор END удаляет ее.
  3. Выполнение SQL-оператора в контексте BEGIN…END блока, непосредственно содержащего обработчик ошибок (WHEN-блок).
Пункты (2) и (3) позволяют обеспечить обработку ошибок в PSQL-блоках. Из них также можно сделать вывод, что при наличии в процедуре BEGIN...WHEN...END блока, как данный, так и все вложенные в него блоки, будут обрамлены ТС-фреймами. Тоже самое относится и к операторам, входящим в эти блоки. Отсутствие WHEN-блоков в процедуре автоматически означает отсутствие ТС внутри нее, приводя к безусловному откату всех операторов процедуры в случае ошибки, как будто бы это был единичный SQL-оператор.

Теперь вкратце рассмотрим механизм обработки ошибок в сервере. В случае возникновения исключения происходит автоматический откат до последней установленной ТС, вследствие чего отменяются все действия, выполненные ошибочным SQL-запросом. Далее, в случае PSQL-блока, анализируется наличие подходящего пользовательского WHEN-обработчика. При наличии такового управление передается ему и по выходу из него ТС блока освобождается. При отсутствии же подходящего обработчика, происходит откат до ТС блока. Далее процесс повторяется рекурсивно, до окончания вложенных обработчиков. Таким образом, при отсутствии в хранимой процедуре обработчика, способного перехватить данную ошибку, все действия процедуры будут последовательно отменены.

Из вышесказанного можно сделать несколько выводов. Во-первых, при отсутствии WHEN-обработчика блок является атомарным и в случае ошибки всегда будет отменен целиком. Во-вторых, при наличии подходящего WHEN-обработчика возникновение ошибки приводит к откату единственного оператора, после чего управление передается обработчику. При отсутствии же подходящего WHEN-обработчика блок будет отменен, но в два этапа – сначала отмена SQL-оператора и затем, после неудачного поиска обработчика, откат до ТС блока.

Однако, существует особенность в обработке PSQL-исключений, про которую часто забывают. Любой откат выполняется только до последнего вызова оператора SUSPEND, который как бы "разрывает" атомарность блока, в пределах которого он находится. И это вполне объяснимо, ведь достаточно странно требовать отката действий, результат выполнения которых уже был отправлен на клиентскую сторону.

Теперь отметим известную аномалию, которая не укладывается в описанную выше схему. Она состоит в том, что пункт 3 (см. выше) истинен только для SQL-операторов, да и то не для всех. То есть, например, оператор присваивания не будет обрамлен ТС-фреймом. Как результат, ошибка в выполнении присваивания приведет к нормальному откату до предыдущей ТС, которым в данном случае является… да, именно – ТС блока. То есть даже при наличии WHEN-обработчика ошибка может привести к откату всего блока перед передачей управления в обработчик. Вот полный список операторов, для которых создается ТС-фрейм: INSERT, UPDATE, DELETE, EXCEPTION, EXECUTE STATEMENT [INTO].
 
Примечание. Начиная с Firebird 2.0, данная аномалия устранена и все PSQL-операторы обрамляются ТС-фреймом в случае наличия WHEN-обработчика.
Для понимания этой аномалии следует знать, что в случае ошибки откат до последней ТС осуществляется безусловно. Эта означает следующее – при возникновении ошибки в операторах не из вышеприведенного списка (то есть не окруженных ТС-фреймом) на самом деле удаляется "чужая" ТС – уровня блока. Таким образом независимо от наличия WHEN-обработчика блок всегда будет отменен. Ситуация усугубляется еще и тем, что откат до ТС блока также осуществляется безусловно (без проверки идентификатора ТС). Таким образом, может возникнуть ситуация, когда целых две "лишних" ТС удалены. Пример подобного случая:
CREATE PROCEDURE PROC1
AS
  DECLARE VARIABLE X INT;
-- start savepoint #1
BEGIN
  -- start savepoint #anchor_2
  INSERT INTO TAB (COL) VALUES (1);
  -- end savepoint #anchor_2
  -- start savepoint #3
  BEGIN
    X = 1 / 0;
  WHEN EXCEPTION TEST DO
    -- start savepoint #anchor_4
    EXCEPTION;
    -- end savepoint #anchor_4
  END
  -- end savepoint #3
WHEN ANY DO
  EXIT;
-- end savepoint #1
END

В данном случае у нас присутствует ТС-фрейм вокруг оператора присваивания. Рассмотрим по шагам, что происходит при возникновении ошибки (деление на ноль). Сначала безусловно отменяется ближайшая ТС, которой является является ТС блока #3. Далее начинается поиск подходящего обработчика, которого в нашем примере нет. Тут происходит откат до ТС блока... который только что уже был удален. Так как откат производится безусловно, то удаляется предыдущая ТС #1. Соответственно, оператор INSERT будет отменен, хотя этого происходить не должно.
 
Примечание. Начиная с Firebird 2.0, откат до ТС блока всегда производится с проверкой идентификатора ТС, что устраняет подобные ошибочные ситуации.
Приведем еще несколько примеров. Для наглядности работу сервера с ТС в данных примерах отметим комментариями.

Процедура с WHEN-обработчиком и генерацией исключения в операторе присваивания:
CREATE PROCEDURE PROC2
AS
  DECLARE VARIABLE X INT;
-- start savepoint #1
BEGIN
  -- start savepoint #anchor_2
  INSERT INTO TAB (COL) VALUES (2);
  -- end savepoint #anchor_2
  X = 1 / 0;
WHEN ANY DO
  EXIT;
-- end savepoint #1
END

В данном случае INSERT будет отменен, так как ближайшей к ошибочному оператору ТС является #1 (ТС #anchor_2 была удалена непосредственно перед выполнением присваивания) и, следовательно, произойдет откат всего BEGIN...END блока перед входом в обработчик.

Та же процедура с явным вызовом исключения, заключенным в блок без WHEN-обработчика:
CREATE PROCEDURE PROC3
AS
  DECLARE VARIABLE X INT;
-- start savepoint #1
BEGIN
  -- start savepoint #anchor_2
  INSERT INTO TAB (COL) VALUES (3);
  -- end savepoint #anchor_2
  -- start savepoint #3
  BEGIN
    X = 1 / 0;
  -- end savepoint #3
  END
WHEN ANY DO
  EXIT;
-- end savepoint #1
END

При возникновении исключения первым делом удаляется ближайшая ТС #3. Далее производится откат атомарного блока и, вследствии вышеописанной ошибки сервера, удаляется ТС #1. Результат – INSERT снова отменен.

Та же процедура с явным вызовом исключения, заключенным в блок, содержащий WHEN-обработчик:
CREATE PROCEDURE PROC4
AS
  DECLARE VARIABLE X INT;
-- start savepoint #1
BEGIN
  -- start savepoint #anchor_2
  INSERT INTO TAB (COL) VALUES (4);
  -- end savepoint #anchor_2
  -- start savepoint #3
  BEGIN X = 1 / 0;
  WHEN ANY DO
    EXIT;
  -- end savepoint #3
END
WHEN ANY DO
  EXIT;
-- end savepoint #1
END

Сначала опять же будет удалена ТС #3. Однако, на этот раз у нас присутствует подходящий обработчик, которому и будет передано управление, после чего выполнение внешнего блока будет продолжено. В результате оператор INSERT остается нетронутым.

Процедура с WHEN-обработчиком и явным вызовом исключения:
CREATE PROCEDURE PROC5 AS
-- start savepoint #1
BEGIN
  -- start savepoint #anchor_2
  INSERT INTO TAB (COL) VALUES (5);
  -- end savepoint #anchor_2
  -- start savepoint #3
  EXCEPTION E;
  -- end savepoint #3
WHEN ANY DO
  EXIT;
-- end savepoint #1
END

В данном случае INSERT не будет отменен, ибо инициация исключения E приведет только к откату до ТС #3 и последующей передачи управления в обработчик. Чтобы в данном случае оператор INSERT все же был отменен, необходимо отказаться от использования обработчика исключений:
CREATE PROCEDURE PROC6
AS
BEGIN
  INSERT INTO TAB (COL) VALUES (6);
  EXCEPTION E;
END

Если рассматривать вышеописанные примеры с точки зрения правильности, то абсолютно корректно были выполнены только последние два примера (PROC5 и PROC6). Пример PROC4 выдает правильный результат, но только в данном конкретном случае – при наличии любого SQL-оператора в одном блоке перед присваиванием результат уже будет неправильным. Первые же три примера (PROC1 – PROC3) отрабатывают неправильно.
 
Примечание. Как уже было отмечено выше, в Firebird 2.0 все примеры выполняются корректно.
Напоследок рассмотрим соответствие обработки исключений SQL-стандарту. Последний определяет три вида обработчиков в PSQL – CONTINUE, EXIT и UNDO. В случае CONTINUE-обработчика сервер должен откатить ошибочный оператор, выполнить код обработчика, после чего продолжить выполнение блока с оператора, следующего за вызвавшим ошибку. EXIT-обработчик требует завершения выполнения блока сразу после выхода из кода обработчика. UNDO-обработчик требует отката всех действий блока до входа в обработчик. Текущие версии сервера не поддерживают явное указание типа обработчика и работают по принципу EXIT (однако, с возможностью UNDO-поведения вследствие описанной выше аномалии). Полагаю, что в будущем было бы желательно обеспечить возможность выбора между UNDO и EXIT поведением обработчика.
 

Пользовательские точки сохранения

Помимо внутренней реализации ТС уровня транзакции и уровня оператора/блока, последние версии серверов (InterBase 7.1, Firebird 1.5, Yaffil 1.1) обеспечивают еще и SQL-интерфейс к данному механизму.
 
Примечание. Синтаксис и семантика ТС декларированы в стандарте SQL-99 (раздел 4.37.1 спецификации).
Пользовательские ТС (иногда называемые также вложенными транзакциями – nested transactions) обеспечивают удобный метод обработки ошибок в бизнес-логике без необходимости отката транзакции целиком.
 
Примечание. Откат до ТС также иногда называется частичным откатом транзакции.
Декларирован новый SQL-оператор SAVEPOINT для идентификации точки в контексте транзакции, до которой впоследствии возможно произвести откат:
SAVEPOINT <name>;
<name> – это строковый идентификатор ТС. После создания ТС можно продолжить выполнение транзакции, подтвердить или откатить ее целиком, или откатить до данной ТС. Имена (идентификаторы) ТС должны быть уникальными в контексте транзакции. Если вы попытаетесь создать еще одну ТС с существующим именем, то первая из них будет удалена и указанное имя назначено второй.

Для отката до ТС используется следующий оператор:
ROLLBACK [WORK] TO [SAVEPOINT] <name>;
 
Примечание. Ключевое слово SAVEPOINT является обязательным в InterBase 7.1.
При выполнении данного оператора происходит следующее:
  • Откатываются все изменения, выполненные после установки данной ТС;
  • Удаляются все ТС, установленные после данной. Текущая ТС остается нетронутой, так что можно последовательно выполнять несколько откатов до одной ТС. Предыдущие ТС также остаются нетронутыми.
Примечание. Реализация отката до ТС в InterBase 7.1 удаляет указанную ТС.
  • Освобождаются все явные и неявные блокировки записей, захваченные после установки данной ТС. При этом другие транзакции, запросившие ранее доступ к записям, заблокированным данной транзакцией после установки данной ТС, продолжают ожидать завершения текущей транзакции. Прочие транзакции, еще не успевшие запросить доступ к этим записям, могут продолжать свое выполнение и получить доступ к ним.
Примечание. Данное поведение относится к Firebird 1.5 и может быть изменено в следующих версиях.
Так как каждая ТС занимает определенные системные ресурсы, а также засоряет пространство имен, то имеет смысл освободить (удалить) ТС тогда, когда в ней больше нет необходимости. Для этого предусмотрен следующий оператор:
RELEASE SAVEPOINT <name> [ONLY];

Данная команда удаляет указанную и все последующие ТС из контекста транзакции. Опция ONLY позволяет удалить только указанную ТС, сохранив последующие. Если ТС не была освобождена явно, то она будет автоматически удалена по завершению транзакции.
 
Примечание. Опция ONLY является нестандартным расширением и не поддерживается в InterBase 7.1.
Ниже приведен простейший пример работы ТС:
create table test (id int);
commit;
insert into test (id) values (1);
commit;
insert into test (id) values (2);
savepoint y;
delete from test;
select * from test; -- возвращает пустой набор
rollback to y;
select * from test; -- возвращает две записи
rollback;
select * from test; -- возвращает одну запись

Теперь приведем пример использования ТС в бизнес-логике. Допустим, в приложении есть операция массовой отработки документов в учете, причем в случае возникновения ошибки требуется показать ее на экран (или сохранить для последующего показа всех ошибок списком) и допустить продолжение данной массовой операции. Так как операция отработки документа не атомарна, использование штатных средств обработки исключений на клиенской стороне нам не подходит, ибо мы не можем продолжать транзакцию зная, что исключение откатило только половину операции. Такая задача может быть решена отработкой каждого документа в отдельной транзакции, последовательно. Но это влияет на внутренние ресурсы сервера (количество записей в TIP, инкремент счетчика транзакций), так что не является оптимальным вариантом. Кроме того, если есть необходимость зафиксировать набор документов в рамках процесса отработки (например, посредством единого режима изоляции транзакции или явной блокировки типа SELECT … WITH LOCK), то приходим к жесткой необходимости использовать именно одну транзакцию для всего пакета изменений. При использовании же ТС просто используется следующий алгоритм (в псевдокоде):
START TRANSACTION;
OPEN C FOR ( SELECT … );
FOR ( C ) DO
  LOOP
    TRY
      SAVEPOINT DOC;
      <…> // пакет команд отработки одного документа в учете
    EXCEPT
      ROLLBACK TO SAVEPOINT DOC;
      <…> // заносим ошибку в протокол или выводим на экран
    END
  END
CLOSE C;
COMMIT;

 
Примечание. Использование ТС в циклах имеет небольшое дополнительное удобство – нет необходимости каждый раз вызывать RELEASE, так как повторная установка ТС автоматически удаляет предыдущую ТС с таким же именем.
Другий пример – неоткатываемый аудит. Пусть, например, нам нужно каждое действие сопровождать записью в какой-либо журнал, причем так, чтобы в случае ошибки запись в журнале аудита оставалась (с соответствующей пометкой):
START TRANSACTION;
INSERT INTO AUDIT_LOG (ID, EVENT, STATUS) VALUES (:ID, :EVENT, 1);
SAVEPOINT OPER;
TRY  
  <…> // действия над БД
EXCEPT
  ROLLBACK TO SAVEPOINT OPER;
  UPDATE AUDIT_LOG SET STATUS = 0 WHERE ID = :ID;
END
COMMIT;
 

Точки сохранения в PSQL

Теперь обсудим весьма тонкий момент, а именно использование пользовательских ТС в процедурах и триггерах. На первый взгляд, это выглядит очень заманчивым и полезным, тем более что данная функциональность декларирована в InterBase 7.1. Но действительно ли все так красиво и безоблачно?

Во-первых, ТС не должны нарушать атомарность SQL-операторов. Это значит, что ни одна из команд не может быть отменена частично. Так как EXECUTE PROCEDURE является допустимым SQL-оператором, а операторы обновления могут приводить к исполнению триггеров, приходим к вопросу области видимости ТС. Совершенно очевидно, что для удовлетворения указанного требования атомарности, команды управления ТС в процедуре не должны иметь доступ к ТС транзакции (установленными через глобальный оператор SAVEPOINT), а также ТС процедуры должны быть локальными и иметь область действия, определяемой данной процедурой. То есть мы можем иметь ТС с именем S1 как в транзакции, так и в процедурах и триггерах, выполняющихся в контексте данной транзакции, причем они будут изолированы друг от друга. Отметим, что именно так это и реализовано в InterBase 7.1.

Во-вторых, возникает вопрос – а как пользовательские ТС в PSQL будут пересекаться с внутренними ТС, управляемыми сервером?

Рассмотрим простой пример использования ТС в PSQL, предлагаемый корпорацией Borland в документации к серверу InterBase 7.1:
CREATE PROCEDURE ADD_EMP_PROJ2 (
  EMP_NO SMALLINT,
  EMP_NAME VARCHAR(20),
  PROJ_ID CHAR(5) )
AS
BEGIN
  BEGIN
    SAVEPOINT EMP_PROJ_INSERT;
    INSERT INTO EMPLOYEE_PROJECT (EMP_NO, PROJ_ID) VALUES (:EMP_NO, :PROJ_ID);
  WHEN SQLCODE -530 DO
    BEGIN
      ROLLBACK TO SAVEPOINT EMP_PROJ_INSERT;
      EXCEPTION UNKNOWN_EMP_ID;
    END   
  END
END

Данный пример демонстрирует обработку исключительных ситуаций с использованием ТС. То есть при возникновении исключения с кодом –530 (нарушение ссылочной целостности по внешнему ключу) мы откатываем операцию вставки и инициируем пользовательское исключение. На самом деле, пример абсолютно бесполезный, ибо ТС здесь не нужна:
BEGIN
  INSERT INTO …
WHEN SQLCODE –530 DO
  EXCEPTION unknown_emp_id;
END

Этот код выполнит ту же самую функцию, ибо сервер сам откатит операцию INSERT при возникновении в ней исключения.

Рассмотрим чуть более сложный вариант:
FOR SELECT ID, … INTO :REC_ID, …
BEGIN
  SAVEPOINT S1;
  INSERT INTO TABLE1 …
  INSERT INTO TABLE2 …
  INSERT INTO TABLE3 …
  EXECUTE PROCEDURE …
  …
WHEN ANY DO
  BEGIN
    ROLLBACK TO SAVEPOINT S1;
    ERROR = REC_ID;
    SUSPEND;
  END
END

Здесь мы пытаемся обработать все документы, но не прекращая работу при сбое, а выдавая на выход процедуры все неудачные попытки. Стандартная логика сервера здесь отменит ошибочный оператор и передаст управление в обработчик, который уже откатит действия всего блока. Таким образом, с помощью пользовательских ТС мы легко превращаем EXIT-обработчик в UNDO в случае необходимости. Очевидно, этой же функциональности можно добиться и стандартными средствами:
FOR SELECT ID, … INTO :REC_ID, …
BEGIN
  BEGIN
    INSERT INTO TABLE1 …
    INSERT INTO TABLE2 …
    INSERT INTO TABLE3 …
    EXECUTE PROCEDURE …
    …
  END
WHEN ANY DO
  BEGIN
    ERROR = REC_ID;
    SUSPEND;
  END
END

Здесь все операторы внутри цикла будут отменены в случае исключения автоматически, ибо они находятся в атомарном блоке, посредством которого был выключен механизм ТС-фреймов для SQL-операторов. После чего сервер раскрутит цепочку вложенных блоков и передаст управление в обработчик.

Получается, что практически во всех случаях можно реализовать ту же семантику на штатных механизмах сервера, то есть на внутренних системных ТС вместо пользовательских, ценой несколько большей громоздкости исходного кода. Таким образом, ТС в PSQL есть не что иное, как более простая и понятная альтернатива явному управлению BEGIN…WHEN…END блоками.

Разумеется, существуют ситуации, когда применение пользовательских ТС оправдано. Например, может понадобиться откатить несколько операторов или итераций цикла в зависимости от какого-либо сложного условия. Подобной функциональности будет нелегко добиться стандартными средствами. Однако, понимаемость подобной логики оставляет желать лучшего и зачастую приводит к трудноуловимым ошибкам.

Но теперь вернемся от теории к практике и проверим наши рассуждения в InterBase 7.1. Результат откровенно удручает – ни один из примеров, приведенных выше, не работает (!!!), выдавая ошибку:
Statement failed, SQLCODE = -504
Savepoint <name> unknown.

Даже первый пример, который был взят из Release Notes (!). Но совсем примитивные примеры типа:
SAVEPOINT S1;
INSERT …
ROLLBACK TO SAVEPOINT S1;
работают корректно. Так в чем же дело? Если немного подумать, то причина очевидна. Вспомним два факта, изложенных выше:
  1. ТС образуют стек и могут быть отменены только последовательно
  2. Каждый блок PSQL-кода с обработчиком заключается во фрейм
Таким образом, приходим к выводу, что любой участок кода вида:
SAVEPOINT S1;

BEGIN

ROLLBACK TO SAVEPOINT S1;

WHEN
неработоспособен по определению, ибо для отката ТС S1 понадобится удалить и системную ТС, созданную сервером для обработки исключений в BEGIN…END блоке. А это было бы действием, приводящим к нарушению внутреннего журнала отмены и потенциальной порче БД. Слава богу, в Borland не дошли до такой кардинальной реализации и сервер пытается отменить непосредственно предыдущую (последнюю) ТС, если она имеет данное имя. Так как системные ТС безымянны, то в этом случае такая попытка обречена на провал, что мы и наблюдаем в виде упомянутого сообщения об ошибке. Отсюда можно сделать вывод, что механизм работы с ТС в PSQL ограничен единым уровнем вложенности в случае блока с WHEN-обработчиком.

Но, как оказалось, самое интересное еще впереди. Очень занимательна реакция сервера на ошибку, инициированную оператором ROLLBACK TO SAVEPOINT или RELEASE SAVEPOINT. Продемонстрируем это на примитивном примере:
BEGIN
  INSERT INTO TABLE1 …
  ROLLBACK TO SAVEPOINT S1;
  INSERT INTO TABLE2 …
END

Здесь эмулирована ошибка ненахождения нужной ТС в пределах даже одного блока кода. Как и ожидалось, выполнение процедуры возвращает ту же ошибку. Но!!! Исполнение процедуры на этом не заканчивается, а вместо этого выполняется второй INSERT (легко проверяется его заменой на оператор вида EXCEPTION E_TEST). Спрашивается, зачем? Но это не все. Оказывается, что данную ошибку не удается обработать в процедуре, то есть следующий код:
INSERT INTO TABLE1 …
BEGIN
  ROLLBACK TO SAVEPOINT S1;
  WHEN ANY DO
    EXCEPTION E_TEST;
END
не выбросит исключения E_TEST, как можно бы было ожидать. Но и это еще не все. Несмотря на то, что выполняется код, находящийся после ROLLBACK TO SAVEPOINT, это в результате абсолютно ни к чему не приводит. То есть в случае возникновения в процедуре указанной ошибки, все изменения, вызванные данной процедурой, будут безусловно (!) отменены. Независимо от того, какой код выполнялся до или после этой команды. Разъяснение этого феномена оставим инженерам Borland’а.

Резюме: существуют особенности логики ТС, которые затрудняют реализацию их поддержки в PSQL в полном объеме. Анализ поведения InterBase 7.1 это полностью подтверждает. Основной причиной этого является наличие системных ТС, взаимодействие с которыми со стороны пользовательских ограничено из-за требований целостности данных. Именно по этим соображениям данная функциональность не доступна в Firebird и Yaffil.
 
Примечание. К слову, эти же причины не дают возможности использовать commit/rollback retaining в PSQL, ибо при этом будет разрушен ТС-фрейм процедуры.
 

Точки сохранения в распределенных транзакциях

InterBase 7.1 также декларирует возможность работы с ТС в распределенных транзакциях. Для этого введены три новые функции API:
ISC_STATUS isc_start_transaction(ISC_STATUS* status,
isc_tr_handle* trans, char* name);
ISC_STATUS isc_release_transaction(ISC_STATUS* status,
isc_tr_handle* trans, char* name);
ISC_STATUS isc_rollback_transaction(ISC_STATUS* status,
isc_tr_handle* trans, char* name, short option);

Как видно из прототипа, эти функции не получают дескриптор соединения (database handle), то есть соответствующие SQL-команды посылаются на все БД, задействованные транзакцией. Выглядит абсолютно логичным, ибо, формально говоря, ТС – это часть транзакции, а не соединения. Но здесь есть один нюанс. Рассмотрим следующий фрагмент программы (обработка ошибок опущена):
/* Подключаемся к БД */
isc_attach_database(status, 0, database1, &db1, 0, NULL);
isc_attach_database(status, 0, database2, &db2, 0, NULL);

/* Стартуем распределенную транзакцию */
isc_start_transaction(status, &trans, 2, &db1, 0, NULL, &db2, 0, NULL);

/* Создаем ТС */
isc_start_savepoint(status, &trans, "A");

/* Выполняем операции над БД */
isc_dsql_execute_immediate(status, &db1, &trans, 0, "DELETE FROM TABLE1", 1, NULL);
isc_dsql_execute_immediate(status, &db2, &trans, 0, "DELETE FROM TABLE2", 1, NULL);

/* Удаляем ТС явно, через дескриптор второго соединения */
isc_dsql_execute_immediate(status, &db2, &trans, 0, "RELEASE SAVEPOINT A", 1, NULL);

/* Откатываемся до ТС */
isc_rollback_savepoint(status, &trans, "A", 0);

/* Коммитим распределенную транзакцию *
/ isc_commit_transaction (status, &trans);

/* Отключаемся от БД*/
isc_detach_database(status, &db1);
isc_detach_database(status, &db2);

В результате отката до ТС я ожидаю, что этот откат произойдет в обоих БД, с которыми я работаю. Затем я подтверждаю транзакцию, после чего я должен увидеть все свои данные на месте, ибо операторы DELETE были отменены. И так бы оно и было, если бы не выполненный вручную "RELEASE SAVEPOINT A". Откат ТС сначала выполнился для первого соединения и все изменения были отменены. Затем та же операция была предпринята для второго соединения и ... ой ... а ТС ведь уже и нету. Возвращаем клиенту ошибку. Но ведь откат одного из DELETE прошел успешно (!) Получаем ситуацию, когда распределенная операция нарушила свою собственную целостность и не позволяет корректно обработать данный случай. Механизм двухфазной фиксации транзакций, призванный исключить такие случаи как класс, просто не предназначен для работы с ТС. То есть новые функции InterBase 7.1 просто формируют соответствующие SQL-команды и циклически исполняют их для всех задействованных БД. В случае отказа хотя бы одной – возвращается ошибка. Которая, в общем случае, никаким образом не может охарактиризовать текущую ситуацию с точки зрения корректности операции как единого целого.

Разумеется, можно сказать, что должен использоваться только один способ работы с ТС – либо через SQL, либо через API. И что теперь вы прекрасно знаете, чем это может кончиться. Однако, новый API для работы с ТС в InterBase 7.1 может быть полностью заменен стандартными средствами сервера и является избыточным и внушающим ложные предположения о правильной работе распределенных ТС. Сервер должен либо пресекать возможность явно управлять ТС в отдельных соединениях в случае распределенных транзакций, либо вообще не декларировать их работоспособность. Отмечу, что разработчики Firebird и Yaffil выбрали последний вариант и предпочли не давать пользователям такой сомнительной возможности.
Впервые опубликовано на www.ibase.ru.

Подпишитесь на новости Firebird в России

Подписаться