Может ли фиксация Postgres существовать в процедуре, имеющей блок исключений?

#postgresql #stored-procedures #plpgsql #postgresql-11

#postgresql #хранимые процедуры #plpgsql #postgresql-11

Вопрос:

Мне трудно понять транзакции в Postgres. У меня есть процедура, которая может столкнуться с исключением. Есть части процедуры, в которых я, возможно, захочу зафиксировать свою работу до сих пор, чтобы она не была откатана, если возникнут исключения.

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

Я свел проблему к простой процедуре, приведенной ниже, которая завершается сбоем в PostgreSQL 11.2 с

 2D000 cannot commit while a subtransaction is active
PL/pgSQL function x_transaction_try() line 6 at COMMIT
  
     drop procedure if exists x_transaction_try;
    create or replace procedure x_transaction_try()
        language plpgsql
    as $$
    declare
    begin
         raise notice 'A';
         -- TODO A: do some insert or update that I want to commit no matter what
         commit;
         raise notice 'B';
         -- TODO B: do something else that might raise an exception, without rolling
         -- back the work that we did in "TODO A".
    exception when others then
      declare
        my_ex_state text;
        my_ex_message text;
        my_ex_detail text;
        my_ex_hint text;
        my_ex_ctx text;
      begin
          raise notice 'C';
          GET STACKED DIAGNOSTICS
            my_ex_state   = RETURNED_SQLSTATE,
            my_ex_message = MESSAGE_TEXT,
            my_ex_detail  = PG_EXCEPTION_DETAIL,
            my_ex_hint    = PG_EXCEPTION_HINT,
            my_ex_ctx     = PG_EXCEPTION_CONTEXT
          ;
          raise notice '% % % % %', my_ex_state, my_ex_message, my_ex_detail, my_ex_hint, my_ex_ctx;
          -- TODO C: insert this exception information in a logging table and commit
      end;
    end;
    $$;

    call x_transaction_try();
  

Почему эта хранимая процедура не работает? Почему мы никогда не видим выходные данные raise notice 'B' и вместо этого переходим в блок исключений? Возможно ли сделать то, что я описал выше, с помощью хранимой процедуры Postgres 11?

Редактировать: Это полный пример кода. Вставьте приведенный выше полный пример кода (включая оба оператора create procedure и call ) в файл sql и запустите его в базе данных Postgres 11.2 для воспроизведения. Желаемым результатом было бы, чтобы функция печатала A then B , но вместо этого она печатает A then C вместе с информацией об исключении.

Также обратите внимание, что если вы закомментируете весь блок обработки исключений таким образом, что функция вообще не будет перехватывать исключения, то функция выведет ‘A’, затем ‘B’ без возникновения исключения. Вот почему я озаглавил вопрос так, как я это сделал: «Может ли фиксация Postgres существовать в процедуре, имеющей блок исключений?»

Комментарии:

1. Проблема, должно быть, в коде, который вызывает процедуру. Можете ли вы описать стек вызовов и управление транзакциями?

2. Это полный пример кода, который вызывает нежелательное исключение. например, даже без какого-либо кода в расположениях TODO, этот полный «скрипт» не может обеспечить желаемый результат. Просто вставьте весь пример кода в файл sql и запустите его.

3. Хм, ты прав. Я посмотрю.

Ответ №1:

Семантика обработки ошибок в PL / pgSQL диктует, что:

Когда ошибка перехватывается предложением EXCEPTION … все изменения постоянного состояния базы данных в блоке откатываются.

Это реализовано с использованием подтранзакций, которые в основном такие же, как точки сохранения. Другими словами, когда вы запускаете следующий код PL / pgSQL:

 BEGIN
  PERFORM foo();
EXCEPTION WHEN others THEN
  PERFORM handle_error();
END
  

… на самом деле происходит что-то вроде этого:

 BEGIN
  SAVEPOINT a;
  PERFORM foo();
  RELEASE SAVEPOINT a;
EXCEPTION WHEN others THEN
  ROLLBACK TO SAVEPOINT a;
  PERFORM handle_error();
END
  

COMMIT Внутри блока это полностью нарушило бы это; ваши изменения стали бы постоянными, точка сохранения была бы отброшена, а обработчик исключений остался бы без возможности отката. В результате фиксации в этом контексте не разрешены, и попытка выполнить COMMIT приведет к ошибке «не удается зафиксировать, пока активна подтранзакция».

Вот почему вы видите, что ваша процедура переходит к обработчику исключений вместо выполнения raise notice 'B' : когда она достигает commit , она выдает ошибку, и обработчик ее перехватывает.

Однако обойти это довольно просто. BEGIN ... END блоки могут быть вложенными, и только блоки с EXCEPTION предложениями предполагают установку точек сохранения, поэтому вы можете просто обернуть команды до и после фиксации в их собственные обработчики исключений:

 create or replace procedure x_transaction_try() language plpgsql
as $$
declare
  my_ex_state text;
  my_ex_message text;
  my_ex_detail text;
  my_ex_hint text;
  my_ex_ctx text;
begin
  begin
    raise notice 'A';
  exception when others then
    raise notice 'C';
    GET STACKED DIAGNOSTICS
      my_ex_state   = RETURNED_SQLSTATE,
      my_ex_message = MESSAGE_TEXT,
      my_ex_detail  = PG_EXCEPTION_DETAIL,
      my_ex_hint    = PG_EXCEPTION_HINT,
      my_ex_ctx     = PG_EXCEPTION_CONTEXT
    ;
    raise notice '% % % % %', my_ex_state, my_ex_message, my_ex_detail, my_ex_hint, my_ex_ctx;
  end;

  commit;

  begin
    raise notice 'B';
  exception when others then
    raise notice 'C';
    GET STACKED DIAGNOSTICS
      my_ex_state   = RETURNED_SQLSTATE,
      my_ex_message = MESSAGE_TEXT,
      my_ex_detail  = PG_EXCEPTION_DETAIL,
      my_ex_hint    = PG_EXCEPTION_HINT,
      my_ex_ctx     = PG_EXCEPTION_CONTEXT
    ;
    raise notice '% % % % %', my_ex_state, my_ex_message, my_ex_detail, my_ex_hint, my_ex_ctx;
  end;      
end;
$$;
  

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

Ответ №2:

Проблема заключается в EXCEPTION предложении.

Это реализовано в PL / pgSQL как подтранзакция (то же самое, что SAVEPOINT в SQL), которая откатывается при достижении блока исключений.

Вы не можете COMMIT , пока активна подтранзакция.

Смотрите этот комментарий в src/backend/executor/spi.c :

 /*
 * This restriction is required by PLs implemented on top of SPI.  They
 * use subtransactions to establish exception blocks that are supposed to
 * be rolled back together if there is an error.  Terminating the
 * top-level transaction in such a block violates that idea.  A future PL
 * implementation might have different ideas about this, in which case
 * this restriction would have to be refined or the check possibly be
 * moved out of SPI into the PLs.
 */
if (IsSubTransaction())
    ereport(ERROR,
            (errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION),
             errmsg("cannot commit while a subtransaction is active")));