Closed SandPod closed 2 weeks ago
@SandPod: amazing bug report, thank you! I think I have a vague idea what may be happening here, I will investigate.
@SandPod: I've merged #386, which passes on your included test case. I'm wondering: could you please test it again before I publish it? Or I could just publish it...
@isoos My test passes with the fix in #386
Thank you for the quick turnaround on this! 🙏
Published as 3.4.2
Background
I'm integrating isolation levels for transactions in the Serverpod Framework and wanted to create a test that highlighted the difference between
REPEATABLE READ
andSERIALIZABLE
transaction isolation level.One way to prove the integration is correct and the transaction isolation levels are respected is to insert two rows in a table. And then do the following operations with two transactions (T1 and T2):
If this is done using the
REPEATABLE READ
isolation level the modification is allowed and both entries are updated. If this is done using theSERIALIZABLE
isolation level only the first modification is allowed and the second one will be rejected with a40001 serialization_failure
exception.Validation
To validate the behavior in Postgres I created two SQL scrips and ran them through two separate connections directly towards a database, starting the first script first and then running the second one.
Problem
When using the driver, I noticed that if two
SERIALIZABLE
transactions are allowed to complete in parallel aPgException
with the descriptionSession or transaction has already finished, did you forget to await a statement?
is thrown instead of the expectedServerException
with code40001
.To capture my findings, I created three tests, one validating the
REPEATABLE READ
isolation level behavior, one validating theSERIALIZABLE
isolation level behavior with special care, and then the last one where the unexpected behavior is exhibited.Tests are available as a single commit on my branch here: https://github.com/SandPod/postgresql-dart/pull/1
The last test that exhibits the unexpected behavior and is also available here:
Validating test
```dart withPostgresServer('Transaction isolation level', (server) { group('Given two rows in the database and two database connections', () { late Connection conn1; late Connection conn2; setUp(() async { conn1 = await server.newConnection(); conn2 = await server.newConnection(); await conn1.execute('CREATE TABLE t (id INT PRIMARY KEY, counter INT)'); await conn1.execute('INSERT INTO t VALUES (1, 0)'); await conn1.execute('INSERT INTO t VALUES (2, 1)'); }); tearDown(() async { await conn1.execute('DROP TABLE t;'); await conn1.close(); await conn2.close(); }); test( 'when two transactions using repeatable read isolation level' 'reads the row updated by the other transaction' 'then one transaction throws exception ', () async { final c1 = Completer(); final c2 = Completer(); final f1 = Future.microtask( () => conn1.runTx( settings: TransactionSettings( isolationLevel: IsolationLevel.serializable, ), (session) async { await session.execute('SELECT * from t WHERE id=1'); c1.complete(); await c2.future; await session .execute('UPDATE t SET counter = counter + 10 WHERE id=2'); }, ), ); final f2 = Future.microtask( () => conn2.runTx( settings: TransactionSettings( isolationLevel: IsolationLevel.serializable, ), (session) async { await session.execute('SELECT * from t WHERE id=2'); await c1.future; // If we complete both transactions in parallel, we get an unexpected // exception c2.complete(); await session .execute('UPDATE t SET counter = counter + 20 WHERE id=1'); // If we complete the first transaction after the second transaction // the correct exception is thrown // c2.complete(); }, ), ); // This test throws Severity.error Session or transaction has already // finished, did you forget to await a statement? await expectLater( () => Future.wait([f1, f2]), throwsA( isA