Deadlock detection in PostgreSQL and MySQL

7 Sep 2020

PostgreSQL

We need two user sessions running. We will distinguish between them by customizing their `psql` prompts.

User one:

$ psql -X -U postgres -d postgres
# \set PROMPT1 'ONE___ %x%# '
# \set PROMPT2 ''
# \pset linestyle unicode
# \pset border 2
# set idle_in_transaction_session_timeout to '5min';

User two:

$ psql -X -U postgres -d postgres
# \set PROMPT1 '___TWO %x%# '
# \set PROMPT2 ''
# \pset linestyle unicode
# \pset border 2
# set idle_in_transaction_session_timeout to '5min';

Note that %x in the prompt shows * for an open (non-committed, non-rolled-back) transaction, and ! for a transaction that had an error and needs to be abandoned (either with a commit or a rollback, it doesn't matter).

From here forward, user one and user two are identified by their prompts.

First, we need to disable autocommit, to give us manual control over transaction commits and rollbacks:

ONE___ # \set AUTOCOMMIT off
___TWO # \set AUTOCOMMIT off

Now we need to create and populate a table with data:

ONE___ # create table accounts(
    acctnum bigint primary key,
    balance bigint default 0 not null);
    commit;

ONE___ # create table accounts(
     acctnum bigint primary key,
     balance bigint default 0 not null);
CREATE TABLE
ONE___ *# commit;
COMMIT
ONE___ # 

ONE___ # insert into accounts(acctnum, balance)
    values (11111, 1000),
          (22222, 1000);
INSERT 0 2
ONE___ *# commit;
COMMIT
ONE___ # 

ONE___ # select * from accounts;
┌─────────┬─────────┐
│ acctnum │ balance │
├─────────┼─────────┤
│   11111 │    1000 │
│   22222 │    1000 │
└─────────┴─────────┘
(2 rows)

ONE___ *# commit;
COMMIT
ONE___ # 

Now let's set up a race condition.

ONE___ # update accounts set balance = balance + 100 where acctnum = 11111;
UPDATE 1
ONE___ *# 
___TWO # update accounts set balance = balance + 100 where acctnum = 22222;
UPDATE 1
___TWO *# update accounts set balance = balance + 100 where acctnum = 11111;

No prompt is not returned here! The `psql` session is blocking!

So let's continue on in session one with another update...

ONE___ *# update accounts set balance = balance + 100 where acctnum = 22222;
ERROR:  deadlock detected
DETAIL:  Process 3517 waits for ShareLock on transaction 580; blocked by process 3428.
Process 3428 waits for ShareLock on transaction 579; blocked by process 3517.
HINT:  See server log for query details.
CONTEXT:  while updating tuple (0,2) in relation "accounts"
ONE___ !#

Session one's prompt returns right away with the error. Notice the exclamation point at the prompt to indicate this transaction has had an error and must be committed or rolled back.

Meanwhile, back at user two, we get our prompt back, and we have an asterisk at our prompt, meaning we are still in a (valid) transaction that we can commit:

UPDATE 1
___TWO *#

So let's do that.

___TWO *# commit;
COMMIT
___TWO #

And let's roll back session one's bad transaction:

ONE___ !# rollback;
ROLLBACK
ONE___ #

Let's check our table:

___TWO # select * from accounts;
┌─────────┬─────────┐
│ acctnum │ balance │
├─────────┼─────────┤
│   22222 │    1100 │
│   11111 │    1100 │
└─────────┴─────────┘
(2 rows)

___TWO *# commit;
COMMIT
___TWO # 

Excellent.

MySQL!

We need two user sessions running. We will distinguish between them by customizing their `mysql` prompts.

User one:

$ mysql --user=root --password --database=mysql
mysql> prompt ONE___>\_

User two:

$ mysql --user=root --password --database=mysql
mysql> prompt ___TWO>\_

There seems to be no way to make the `mysql` prompt show the status of the current transaction. :-(

From here forward, user one and user two are identified by their prompts.

First, we need to disable autocommit, to give us manual control over transaction commits and rollbacks:

ONE___> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)

ONE___> 
___TWO> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)

___TWO> 

Now we need to create and populate a table with data:

ONE___> create table accounts(acctnum bigint primary key,
    -> balance bigint default 0 not null);
Query OK, 0 rows affected (0.02 sec)

ONE___> commit;
Query OK, 0 rows affected (0.00 sec)

ONE___> 

ONE___> insert into accounts(acctnum, balance) values
    -> (11111, 1000),
    -> (22222, 1000);
Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

ONE___> commit;
Query OK, 0 rows affected (0.01 sec)

ONE___> 

ONE___> select * from accounts;
+---------+---------+
| acctnum | balance |
+---------+---------+
|   11111 |    1000 |
|   22222 |    1000 |
+---------+---------+
2 rows in set (0.00 sec)

ONE___> commit;
Query OK, 0 rows affected (0.00 sec)

ONE___> 

Now let's set up a race condition.

ONE___> update accounts set balance = balance + 100 where acctnum = 11111;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

ONE___> 
___TWO> update accounts set balance = balance + 100 where acctnum = 22222;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

___TWO> update accounts set balance = balance + 100 where acctnum = 11111;

NO prompt is not returned here! The `mysql` session is blocking!

So let's continue on in session one with another update...

ONE___> update accounts set balance = balance + 100 where acctnum = 22222;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
ONE___> 

Session one's prompt returns right away with the error. Presumably, we need to roll back.

Meanwhile, back at user two, we get our prompt back. Presumably, we can commit.

Query OK, 1 row affected (35.47 sec)
Rows matched: 1  Changed: 1  Warnings: 0

___TWO> 

In session two, let's try to commit:

___TWO> commit;
Query OK, 0 rows affected (0.01 sec)

___TWO> 

In session one, let's try to rollback:

ONE___> rollback;
Query OK, 0 rows affected (0.00 sec)

ONE___> 

Let's check our table:

___TWO> select * from accounts;
+---------+---------+
| acctnum | balance |
+---------+---------+
|   11111 |    1100 |
|   22222 |    1100 |
+---------+---------+
2 rows in set (0.01 sec)

___TWO> commit;
Query OK, 0 rows affected (0.00 sec)

___TWO> 

That's reassuring.