The strange case of the EXCEPTION block

The strange case of the EXCEPTION block

When in a pl/pgsql function there is an exception then the function stops the execution and returns an error. When this happens all the changes made are rolled back.

It’s always possible to manage the error at application level, however there are some cases where managing the exception inside the function it may be a sensible choice. And pl/pgsql have a nice way to do that. The EXCEPTION block.

However handling the exception inside a function is not just a cosmetic thing. The way the excepion is handled have implications that may cause issues.

Trapping exceptions

In a function’s body the exception comes always after all the code in the block started with BEGIN and before the corresponding END;. It’s possible to define multiple BEGIN…EXCEPTION…END for managing different kind of exceptions.

When an error occurs in the code between the BEGIN…EXCEPTION then the execution stops and jumps to the error handlers defined into the EXCEPTION block.

The handlers are defined in the form of WHEN…THEN. After the WHEN it should follow the error code name to handle or the catch all keyword OTHERS.

For example the function

CREATE OR REPLACE FUNCTION fn_pl_division(p_dividend numeric, p_divisor numeric)
RETURNS numeric
AS
$BODY$
  DECLARE
    v_out   numeric;
  BEGIN
    BEGIN
      v_out:=(p_dividend/p_divisor)::numeric;
    EXCEPTION
      WHEN DIVISION_BY_ZERO
      THEN 
	    v_out:=NULL;
    END;
    RETURN v_out;
  END;
$BODY$
LANGUAGE plpgsql;

Handles only the DIVISION_BY_ZERO exception. Should a different error occur the function will exit with an error.

The PostgreSQL online manual have a warning for functions using EXCEPTION. They are more expensive than functions without it. However if using in such functions the DML, like INSERT or UPDATE ,the execution cost may be the last of the problems.

What happens in Functions stays in Functions

The PostgreSQL functions are single transaction, this is a good and a bad thing. It’s good because they consume just one XID whatever happens inside the function.

It’s a bad thing because there is no transaction control within the function’s body and if the function hits an error the the entire transaction is rolled back.

Now for the sake of clarity let’s create a table with one field of type integer enforcing a unique key on that field.

CREATE TABLE t_test 
(
  id integer
);

CREATE UNIQUE INDEX idx_u_t_test_id 
    ON t_test(id);

Then we create a function inserting one record at a time over a FOR LOOP of 1000.

CREATE OR REPLACE FUNCTION fn_loop_noexception()
RETURNS VOID AS
$BODY$
  DECLARE
    v_loop integer;
  BEGIN
    FOR v_loop IN 1..1000
    LOOP
      INSERT INTO t_test(id) VALUES (v_loop);
    END LOOP;
  END;
$BODY$
LANGUAGE plpgsql;

We run a VACUUM FREEZE on our datatabase in order to start with age of the datfroxenxid set to 0.

VACUUM FREEZE;
SELECT datname,age(datfrozenxid) FROM pg_database  WHERE datname=current_database();
 datname | age 
---------+-----
 test    |   0
(1 row)

When executing the function we add 1000 records to the table. Checking the XID we find that the age increased by one.

SELECT datname,age(datfrozenxid) FROM pg_database  WHERE datname=current_database();
 datname | age 
---------+-----
 test    |   1
(1 row)

SELECT count(*) FROM t_test ;
 count 
-------
  1000
(1 row)

When running the function a second time we get an error caused by the unique index.

test=# SELECT fn_loop_noexception();
2020-01-30 15:58:09.190 GMT [4456] ERROR:  duplicate key value violates unique constraint "idx_u_t_test_id"
2020-01-30 15:58:09.190 GMT [4456] DETAIL:  Key (id)=(1) already exists.
2020-01-30 15:58:09.190 GMT [4456] CONTEXT:  SQL statement "INSERT INTO t_test(id) VALUES (v_loop)"
	PL/pgSQL function fn_loop_noexception() line 7 at SQL statement
2020-01-30 15:58:09.190 GMT [4456] STATEMENT:  SELECT fn_loop_noexception();
ERROR:  duplicate key value violates unique constraint "idx_u_t_test_id"
DETAIL:  Key (id)=(1) already exists.
CONTEXT:  SQL statement "INSERT INTO t_test(id) VALUES (v_loop)"
PL/pgSQL function fn_loop_noexception() line 7 at SQL statement
test=# SELECT datname,age(datfrozenxid) FROM pg_database  WHERE datname=current_database();
 datname | age 
---------+-----
 test    |   2
(1 row)

The function’s failure consumes an XID anyway.

So we want to handle the exception, we write another function which doesn’t fails in case of UNIQUE violation.

CREATE OR REPLACE FUNCTION fn_loop_withexception()
RETURNS VOID AS
$BODY$
  DECLARE
    v_loop integer;
  BEGIN
    FOR v_loop IN 1..1000
    LOOP
      BEGIN
	INSERT INTO t_test(id) VALUES (v_loop);
      EXCEPTION
	WHEN UNIQUE_VIOLATION 
	THEN
	    NULL;
      END;
    END LOOP;
  END;
$BODY$
LANGUAGE plpgsql;

Let’s truncate the table run another vacuum freze and execute the new function.

TRUNCATE TABLE t_test ;
TRUNCATE TABLE

VACUUM FREEZE;
VACUUM
test=# SELECT datname,age(datfrozenxid) FROM pg_database  WHERE datname=current_database();
 datname | age 
---------+-----
 test    |   0
(1 row)

SELECT fn_loop_withexception();
 fn_loop_withexception 
-----------------------
 
(1 row)

SELECT datname,age(datfrozenxid) FROM pg_database  WHERE datname=current_database();
 datname | age  
---------+------
 test    | 1001
(1 row)

Well, this is a kind of shock. Whether the exception is handled or notthe DML consumes an XID every time it’s executed. All the XID consumed internally are then added to the XID consumed by the function as usual.

This kind of behaviour that I’m struggling to find documention is what caused the one and only emergency shutdown to prevent the XID wraparound I had to deal in my career.

When the cluster goes in emergency shutdown stops accepting new queries and the only way to resume the operations is to stop the cluster, start the postmaster in single user mode and run a VACUUM FREEZE on the relations at risk of XID wraparound.

Wrap up

Knowing how things work is always important. Reading the manal is always good but sometimes the discoveries happen through failures.

Therefore it is very important to be prepared for managing emergency situations.

Adding the behaviour of the exception block for DML in the PostgreSQL documentation may be a nice thing.

Further readings:

picture: Kefka Palazzo, copyright Federico Campoli