Может ли фиксация Postgres существовать в процедуре, имеющей исключительный блок? - PullRequest
4 голосов
/ 28 марта 2019

Мне сложно понять транзакции в 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, а затем B, но вместо этого она печатает A, затем C вместе с информацией об исключении.

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

Ответы [ 2 ]

4 голосов
/ 29 марта 2019

Семантика обработки ошибок 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

A 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 голосов
/ 29 марта 2019

Проблема в предложении 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")));
...