I am writing a web application, and I've been experimenting with wrapping SQL statements from every web request with a transaction with ISOLATION LEVEL REPEATABLE READ, to find where my web application might be doing non-repeatable reads. My plan was not to retry in case of a non-repeatable read, and just report a server-side error (500) to the user and log the information (since I expect this to be very rare).
At the same time, there are places in my code where I use explicit locking (SELECT ... FOR UPDATE) to make sure I serialize access correctly and don't cause unrepeatable reads.
Combining the two ideas together, however, is giving me unexpected results.
Below is a minimal example:
+--------------------------------------------------+--------------------------------------------------+
| Session 1 | Session 2 |
+--------------------------------------------------+--------------------------------------------------+
| BEGIN; | |
| SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; | |
| SELECT * FROM users WHERE id = 1 FOR UPDATE; | |
| (returns as expected) | |
+--------------------------------------------------+--------------------------------------------------+
| | BEGIN; |
| | SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; |
| | SELECT * FROM users WHERE id = 1 FOR UPDATE; |
| | (blocks as expected) |
+--------------------------------------------------+--------------------------------------------------+
| UPDATE users SET name = 'foobar' WHERE id = 1; | |
| COMMIT; | |
| (works as expected) | |
+--------------------------------------------------+--------------------------------------------------+
| | ERROR: could not serialize access due |
| | to concurrent update |
+--------------------------------------------------+--------------------------------------------------+
My expectation is that since session 2 didn't do any reads before that SELECT statement, and since that statement only returns after session 1 has done its update, then session 2 should see the updated version of the table, and that would make it a repeatable read.
I take it that, most likely, Postgres is taking a version when BEGIN is ran, not when it acquires the lock for the first SELECT.
My questions:
- Is my understanding correct?
- Is there a way to make Postgres behave the way I expect?
- Would this be considered a bug, or is this working as intended?